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#[gpui::test]
1320fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1321 init_test(cx, |_| {});
1322
1323 let editor = cx.add_window(|window, cx| {
1324 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1325 build_editor(buffer.clone(), window, cx)
1326 });
1327
1328 assert_eq!('🟥'.len_utf8(), 4);
1329 assert_eq!('α'.len_utf8(), 2);
1330
1331 _ = editor.update(cx, |editor, window, cx| {
1332 editor.fold_creases(
1333 vec![
1334 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1335 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1336 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1337 ],
1338 true,
1339 window,
1340 cx,
1341 );
1342 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1343
1344 editor.move_right(&MoveRight, window, cx);
1345 assert_eq!(
1346 editor.selections.display_ranges(cx),
1347 &[empty_range(0, "🟥".len())]
1348 );
1349 editor.move_right(&MoveRight, window, cx);
1350 assert_eq!(
1351 editor.selections.display_ranges(cx),
1352 &[empty_range(0, "🟥🟧".len())]
1353 );
1354 editor.move_right(&MoveRight, window, cx);
1355 assert_eq!(
1356 editor.selections.display_ranges(cx),
1357 &[empty_range(0, "🟥🟧⋯".len())]
1358 );
1359
1360 editor.move_down(&MoveDown, window, cx);
1361 assert_eq!(
1362 editor.selections.display_ranges(cx),
1363 &[empty_range(1, "ab⋯e".len())]
1364 );
1365 editor.move_left(&MoveLeft, window, cx);
1366 assert_eq!(
1367 editor.selections.display_ranges(cx),
1368 &[empty_range(1, "ab⋯".len())]
1369 );
1370 editor.move_left(&MoveLeft, window, cx);
1371 assert_eq!(
1372 editor.selections.display_ranges(cx),
1373 &[empty_range(1, "ab".len())]
1374 );
1375 editor.move_left(&MoveLeft, window, cx);
1376 assert_eq!(
1377 editor.selections.display_ranges(cx),
1378 &[empty_range(1, "a".len())]
1379 );
1380
1381 editor.move_down(&MoveDown, window, cx);
1382 assert_eq!(
1383 editor.selections.display_ranges(cx),
1384 &[empty_range(2, "α".len())]
1385 );
1386 editor.move_right(&MoveRight, window, cx);
1387 assert_eq!(
1388 editor.selections.display_ranges(cx),
1389 &[empty_range(2, "αβ".len())]
1390 );
1391 editor.move_right(&MoveRight, window, cx);
1392 assert_eq!(
1393 editor.selections.display_ranges(cx),
1394 &[empty_range(2, "αβ⋯".len())]
1395 );
1396 editor.move_right(&MoveRight, window, cx);
1397 assert_eq!(
1398 editor.selections.display_ranges(cx),
1399 &[empty_range(2, "αβ⋯ε".len())]
1400 );
1401
1402 editor.move_up(&MoveUp, window, cx);
1403 assert_eq!(
1404 editor.selections.display_ranges(cx),
1405 &[empty_range(1, "ab⋯e".len())]
1406 );
1407 editor.move_down(&MoveDown, window, cx);
1408 assert_eq!(
1409 editor.selections.display_ranges(cx),
1410 &[empty_range(2, "αβ⋯ε".len())]
1411 );
1412 editor.move_up(&MoveUp, window, cx);
1413 assert_eq!(
1414 editor.selections.display_ranges(cx),
1415 &[empty_range(1, "ab⋯e".len())]
1416 );
1417
1418 editor.move_up(&MoveUp, window, cx);
1419 assert_eq!(
1420 editor.selections.display_ranges(cx),
1421 &[empty_range(0, "🟥🟧".len())]
1422 );
1423 editor.move_left(&MoveLeft, window, cx);
1424 assert_eq!(
1425 editor.selections.display_ranges(cx),
1426 &[empty_range(0, "🟥".len())]
1427 );
1428 editor.move_left(&MoveLeft, window, cx);
1429 assert_eq!(
1430 editor.selections.display_ranges(cx),
1431 &[empty_range(0, "".len())]
1432 );
1433 });
1434}
1435
1436#[gpui::test]
1437fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1438 init_test(cx, |_| {});
1439
1440 let editor = cx.add_window(|window, cx| {
1441 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1442 build_editor(buffer.clone(), window, cx)
1443 });
1444 _ = editor.update(cx, |editor, window, cx| {
1445 editor.change_selections(None, window, cx, |s| {
1446 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1447 });
1448
1449 // moving above start of document should move selection to start of document,
1450 // but the next move down should still be at the original goal_x
1451 editor.move_up(&MoveUp, window, cx);
1452 assert_eq!(
1453 editor.selections.display_ranges(cx),
1454 &[empty_range(0, "".len())]
1455 );
1456
1457 editor.move_down(&MoveDown, window, cx);
1458 assert_eq!(
1459 editor.selections.display_ranges(cx),
1460 &[empty_range(1, "abcd".len())]
1461 );
1462
1463 editor.move_down(&MoveDown, window, cx);
1464 assert_eq!(
1465 editor.selections.display_ranges(cx),
1466 &[empty_range(2, "αβγ".len())]
1467 );
1468
1469 editor.move_down(&MoveDown, window, cx);
1470 assert_eq!(
1471 editor.selections.display_ranges(cx),
1472 &[empty_range(3, "abcd".len())]
1473 );
1474
1475 editor.move_down(&MoveDown, window, cx);
1476 assert_eq!(
1477 editor.selections.display_ranges(cx),
1478 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1479 );
1480
1481 // moving past end of document should not change goal_x
1482 editor.move_down(&MoveDown, window, cx);
1483 assert_eq!(
1484 editor.selections.display_ranges(cx),
1485 &[empty_range(5, "".len())]
1486 );
1487
1488 editor.move_down(&MoveDown, window, cx);
1489 assert_eq!(
1490 editor.selections.display_ranges(cx),
1491 &[empty_range(5, "".len())]
1492 );
1493
1494 editor.move_up(&MoveUp, window, cx);
1495 assert_eq!(
1496 editor.selections.display_ranges(cx),
1497 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1498 );
1499
1500 editor.move_up(&MoveUp, window, cx);
1501 assert_eq!(
1502 editor.selections.display_ranges(cx),
1503 &[empty_range(3, "abcd".len())]
1504 );
1505
1506 editor.move_up(&MoveUp, window, cx);
1507 assert_eq!(
1508 editor.selections.display_ranges(cx),
1509 &[empty_range(2, "αβγ".len())]
1510 );
1511 });
1512}
1513
1514#[gpui::test]
1515fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1516 init_test(cx, |_| {});
1517 let move_to_beg = MoveToBeginningOfLine {
1518 stop_at_soft_wraps: true,
1519 stop_at_indent: true,
1520 };
1521
1522 let delete_to_beg = DeleteToBeginningOfLine {
1523 stop_at_indent: false,
1524 };
1525
1526 let move_to_end = MoveToEndOfLine {
1527 stop_at_soft_wraps: true,
1528 };
1529
1530 let editor = cx.add_window(|window, cx| {
1531 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1532 build_editor(buffer, window, cx)
1533 });
1534 _ = editor.update(cx, |editor, window, cx| {
1535 editor.change_selections(None, window, cx, |s| {
1536 s.select_display_ranges([
1537 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1538 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1539 ]);
1540 });
1541 });
1542
1543 _ = editor.update(cx, |editor, window, cx| {
1544 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1545 assert_eq!(
1546 editor.selections.display_ranges(cx),
1547 &[
1548 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1549 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1550 ]
1551 );
1552 });
1553
1554 _ = editor.update(cx, |editor, window, cx| {
1555 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1556 assert_eq!(
1557 editor.selections.display_ranges(cx),
1558 &[
1559 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1560 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1561 ]
1562 );
1563 });
1564
1565 _ = editor.update(cx, |editor, window, cx| {
1566 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1567 assert_eq!(
1568 editor.selections.display_ranges(cx),
1569 &[
1570 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1571 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1572 ]
1573 );
1574 });
1575
1576 _ = editor.update(cx, |editor, window, cx| {
1577 editor.move_to_end_of_line(&move_to_end, window, cx);
1578 assert_eq!(
1579 editor.selections.display_ranges(cx),
1580 &[
1581 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1582 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1583 ]
1584 );
1585 });
1586
1587 // Moving to the end of line again is a no-op.
1588 _ = editor.update(cx, |editor, window, cx| {
1589 editor.move_to_end_of_line(&move_to_end, window, cx);
1590 assert_eq!(
1591 editor.selections.display_ranges(cx),
1592 &[
1593 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1594 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1595 ]
1596 );
1597 });
1598
1599 _ = editor.update(cx, |editor, window, cx| {
1600 editor.move_left(&MoveLeft, window, cx);
1601 editor.select_to_beginning_of_line(
1602 &SelectToBeginningOfLine {
1603 stop_at_soft_wraps: true,
1604 stop_at_indent: true,
1605 },
1606 window,
1607 cx,
1608 );
1609 assert_eq!(
1610 editor.selections.display_ranges(cx),
1611 &[
1612 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1613 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1614 ]
1615 );
1616 });
1617
1618 _ = editor.update(cx, |editor, window, cx| {
1619 editor.select_to_beginning_of_line(
1620 &SelectToBeginningOfLine {
1621 stop_at_soft_wraps: true,
1622 stop_at_indent: true,
1623 },
1624 window,
1625 cx,
1626 );
1627 assert_eq!(
1628 editor.selections.display_ranges(cx),
1629 &[
1630 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1631 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1632 ]
1633 );
1634 });
1635
1636 _ = editor.update(cx, |editor, window, cx| {
1637 editor.select_to_beginning_of_line(
1638 &SelectToBeginningOfLine {
1639 stop_at_soft_wraps: true,
1640 stop_at_indent: true,
1641 },
1642 window,
1643 cx,
1644 );
1645 assert_eq!(
1646 editor.selections.display_ranges(cx),
1647 &[
1648 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1649 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1650 ]
1651 );
1652 });
1653
1654 _ = editor.update(cx, |editor, window, cx| {
1655 editor.select_to_end_of_line(
1656 &SelectToEndOfLine {
1657 stop_at_soft_wraps: true,
1658 },
1659 window,
1660 cx,
1661 );
1662 assert_eq!(
1663 editor.selections.display_ranges(cx),
1664 &[
1665 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1666 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1667 ]
1668 );
1669 });
1670
1671 _ = editor.update(cx, |editor, window, cx| {
1672 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1673 assert_eq!(editor.display_text(cx), "ab\n de");
1674 assert_eq!(
1675 editor.selections.display_ranges(cx),
1676 &[
1677 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1678 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1679 ]
1680 );
1681 });
1682
1683 _ = editor.update(cx, |editor, window, cx| {
1684 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1685 assert_eq!(editor.display_text(cx), "\n");
1686 assert_eq!(
1687 editor.selections.display_ranges(cx),
1688 &[
1689 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1690 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1691 ]
1692 );
1693 });
1694}
1695
1696#[gpui::test]
1697fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1698 init_test(cx, |_| {});
1699 let move_to_beg = MoveToBeginningOfLine {
1700 stop_at_soft_wraps: false,
1701 stop_at_indent: false,
1702 };
1703
1704 let move_to_end = MoveToEndOfLine {
1705 stop_at_soft_wraps: false,
1706 };
1707
1708 let editor = cx.add_window(|window, cx| {
1709 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1710 build_editor(buffer, window, cx)
1711 });
1712
1713 _ = editor.update(cx, |editor, window, cx| {
1714 editor.set_wrap_width(Some(140.0.into()), cx);
1715
1716 // We expect the following lines after wrapping
1717 // ```
1718 // thequickbrownfox
1719 // jumpedoverthelazydo
1720 // gs
1721 // ```
1722 // The final `gs` was soft-wrapped onto a new line.
1723 assert_eq!(
1724 "thequickbrownfox\njumpedoverthelaz\nydogs",
1725 editor.display_text(cx),
1726 );
1727
1728 // First, let's assert behavior on the first line, that was not soft-wrapped.
1729 // Start the cursor at the `k` on the first line
1730 editor.change_selections(None, window, cx, |s| {
1731 s.select_display_ranges([
1732 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1733 ]);
1734 });
1735
1736 // Moving to the beginning of the line should put us at the beginning of the line.
1737 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1738 assert_eq!(
1739 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1740 editor.selections.display_ranges(cx)
1741 );
1742
1743 // Moving to the end of the line should put us at the end of the line.
1744 editor.move_to_end_of_line(&move_to_end, window, cx);
1745 assert_eq!(
1746 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1747 editor.selections.display_ranges(cx)
1748 );
1749
1750 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1751 // Start the cursor at the last line (`y` that was wrapped to a new line)
1752 editor.change_selections(None, window, cx, |s| {
1753 s.select_display_ranges([
1754 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1755 ]);
1756 });
1757
1758 // Moving to the beginning of the line should put us at the start of the second line of
1759 // display text, i.e., the `j`.
1760 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1761 assert_eq!(
1762 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1763 editor.selections.display_ranges(cx)
1764 );
1765
1766 // Moving to the beginning of the line again should be a no-op.
1767 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1768 assert_eq!(
1769 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1770 editor.selections.display_ranges(cx)
1771 );
1772
1773 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1774 // next display line.
1775 editor.move_to_end_of_line(&move_to_end, window, cx);
1776 assert_eq!(
1777 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1778 editor.selections.display_ranges(cx)
1779 );
1780
1781 // Moving to the end of the line again should be a no-op.
1782 editor.move_to_end_of_line(&move_to_end, window, cx);
1783 assert_eq!(
1784 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1785 editor.selections.display_ranges(cx)
1786 );
1787 });
1788}
1789
1790#[gpui::test]
1791fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1792 init_test(cx, |_| {});
1793
1794 let move_to_beg = MoveToBeginningOfLine {
1795 stop_at_soft_wraps: true,
1796 stop_at_indent: true,
1797 };
1798
1799 let select_to_beg = SelectToBeginningOfLine {
1800 stop_at_soft_wraps: true,
1801 stop_at_indent: true,
1802 };
1803
1804 let delete_to_beg = DeleteToBeginningOfLine {
1805 stop_at_indent: true,
1806 };
1807
1808 let move_to_end = MoveToEndOfLine {
1809 stop_at_soft_wraps: false,
1810 };
1811
1812 let editor = cx.add_window(|window, cx| {
1813 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1814 build_editor(buffer, window, cx)
1815 });
1816
1817 _ = editor.update(cx, |editor, window, cx| {
1818 editor.change_selections(None, window, cx, |s| {
1819 s.select_display_ranges([
1820 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1821 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1822 ]);
1823 });
1824
1825 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1826 // and the second cursor at the first non-whitespace character in the line.
1827 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1828 assert_eq!(
1829 editor.selections.display_ranges(cx),
1830 &[
1831 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1832 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1833 ]
1834 );
1835
1836 // Moving to the beginning of the line again should be a no-op for the first cursor,
1837 // and should move the second cursor to the beginning of the line.
1838 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1839 assert_eq!(
1840 editor.selections.display_ranges(cx),
1841 &[
1842 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1843 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1844 ]
1845 );
1846
1847 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1848 // and should move the second cursor back to the first non-whitespace character in the line.
1849 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1850 assert_eq!(
1851 editor.selections.display_ranges(cx),
1852 &[
1853 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1854 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1855 ]
1856 );
1857
1858 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1859 // and to the first non-whitespace character in the line for the second cursor.
1860 editor.move_to_end_of_line(&move_to_end, window, cx);
1861 editor.move_left(&MoveLeft, window, cx);
1862 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1863 assert_eq!(
1864 editor.selections.display_ranges(cx),
1865 &[
1866 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1867 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1868 ]
1869 );
1870
1871 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1872 // and should select to the beginning of the line for the second cursor.
1873 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1874 assert_eq!(
1875 editor.selections.display_ranges(cx),
1876 &[
1877 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1878 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1879 ]
1880 );
1881
1882 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1883 // and should delete to the first non-whitespace character in the line for the second cursor.
1884 editor.move_to_end_of_line(&move_to_end, window, cx);
1885 editor.move_left(&MoveLeft, window, cx);
1886 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1887 assert_eq!(editor.text(cx), "c\n f");
1888 });
1889}
1890
1891#[gpui::test]
1892fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1893 init_test(cx, |_| {});
1894
1895 let editor = cx.add_window(|window, cx| {
1896 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1897 build_editor(buffer, window, cx)
1898 });
1899 _ = editor.update(cx, |editor, window, cx| {
1900 editor.change_selections(None, window, cx, |s| {
1901 s.select_display_ranges([
1902 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1903 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1904 ])
1905 });
1906
1907 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1908 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1909
1910 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1911 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1912
1913 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1914 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1915
1916 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1917 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1918
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1921
1922 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1923 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1924
1925 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1926 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1929 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1930
1931 editor.move_right(&MoveRight, window, cx);
1932 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1933 assert_selection_ranges(
1934 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1935 editor,
1936 cx,
1937 );
1938
1939 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1940 assert_selection_ranges(
1941 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1942 editor,
1943 cx,
1944 );
1945
1946 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1947 assert_selection_ranges(
1948 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1949 editor,
1950 cx,
1951 );
1952 });
1953}
1954
1955#[gpui::test]
1956fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1957 init_test(cx, |_| {});
1958
1959 let editor = cx.add_window(|window, cx| {
1960 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1961 build_editor(buffer, window, cx)
1962 });
1963
1964 _ = editor.update(cx, |editor, window, cx| {
1965 editor.set_wrap_width(Some(140.0.into()), cx);
1966 assert_eq!(
1967 editor.display_text(cx),
1968 "use one::{\n two::three::\n four::five\n};"
1969 );
1970
1971 editor.change_selections(None, window, cx, |s| {
1972 s.select_display_ranges([
1973 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1974 ]);
1975 });
1976
1977 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1978 assert_eq!(
1979 editor.selections.display_ranges(cx),
1980 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1981 );
1982
1983 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1984 assert_eq!(
1985 editor.selections.display_ranges(cx),
1986 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1987 );
1988
1989 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1990 assert_eq!(
1991 editor.selections.display_ranges(cx),
1992 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1993 );
1994
1995 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1996 assert_eq!(
1997 editor.selections.display_ranges(cx),
1998 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
1999 );
2000
2001 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2002 assert_eq!(
2003 editor.selections.display_ranges(cx),
2004 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2005 );
2006
2007 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2008 assert_eq!(
2009 editor.selections.display_ranges(cx),
2010 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2011 );
2012 });
2013}
2014
2015#[gpui::test]
2016async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2017 init_test(cx, |_| {});
2018 let mut cx = EditorTestContext::new(cx).await;
2019
2020 let line_height = cx.editor(|editor, window, _| {
2021 editor
2022 .style()
2023 .unwrap()
2024 .text
2025 .line_height_in_pixels(window.rem_size())
2026 });
2027 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2028
2029 cx.set_state(
2030 &r#"ˇone
2031 two
2032
2033 three
2034 fourˇ
2035 five
2036
2037 six"#
2038 .unindent(),
2039 );
2040
2041 cx.update_editor(|editor, window, cx| {
2042 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2043 });
2044 cx.assert_editor_state(
2045 &r#"one
2046 two
2047 ˇ
2048 three
2049 four
2050 five
2051 ˇ
2052 six"#
2053 .unindent(),
2054 );
2055
2056 cx.update_editor(|editor, window, cx| {
2057 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2058 });
2059 cx.assert_editor_state(
2060 &r#"one
2061 two
2062
2063 three
2064 four
2065 five
2066 ˇ
2067 sixˇ"#
2068 .unindent(),
2069 );
2070
2071 cx.update_editor(|editor, window, cx| {
2072 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2073 });
2074 cx.assert_editor_state(
2075 &r#"one
2076 two
2077
2078 three
2079 four
2080 five
2081
2082 sixˇ"#
2083 .unindent(),
2084 );
2085
2086 cx.update_editor(|editor, window, cx| {
2087 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2088 });
2089 cx.assert_editor_state(
2090 &r#"one
2091 two
2092
2093 three
2094 four
2095 five
2096 ˇ
2097 six"#
2098 .unindent(),
2099 );
2100
2101 cx.update_editor(|editor, window, cx| {
2102 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2103 });
2104 cx.assert_editor_state(
2105 &r#"one
2106 two
2107 ˇ
2108 three
2109 four
2110 five
2111
2112 six"#
2113 .unindent(),
2114 );
2115
2116 cx.update_editor(|editor, window, cx| {
2117 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2118 });
2119 cx.assert_editor_state(
2120 &r#"ˇone
2121 two
2122
2123 three
2124 four
2125 five
2126
2127 six"#
2128 .unindent(),
2129 );
2130}
2131
2132#[gpui::test]
2133async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2134 init_test(cx, |_| {});
2135 let mut cx = EditorTestContext::new(cx).await;
2136 let line_height = cx.editor(|editor, window, _| {
2137 editor
2138 .style()
2139 .unwrap()
2140 .text
2141 .line_height_in_pixels(window.rem_size())
2142 });
2143 let window = cx.window;
2144 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2145
2146 cx.set_state(
2147 r#"ˇone
2148 two
2149 three
2150 four
2151 five
2152 six
2153 seven
2154 eight
2155 nine
2156 ten
2157 "#,
2158 );
2159
2160 cx.update_editor(|editor, window, cx| {
2161 assert_eq!(
2162 editor.snapshot(window, cx).scroll_position(),
2163 gpui::Point::new(0., 0.)
2164 );
2165 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2166 assert_eq!(
2167 editor.snapshot(window, cx).scroll_position(),
2168 gpui::Point::new(0., 3.)
2169 );
2170 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2171 assert_eq!(
2172 editor.snapshot(window, cx).scroll_position(),
2173 gpui::Point::new(0., 6.)
2174 );
2175 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2176 assert_eq!(
2177 editor.snapshot(window, cx).scroll_position(),
2178 gpui::Point::new(0., 3.)
2179 );
2180
2181 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2182 assert_eq!(
2183 editor.snapshot(window, cx).scroll_position(),
2184 gpui::Point::new(0., 1.)
2185 );
2186 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2187 assert_eq!(
2188 editor.snapshot(window, cx).scroll_position(),
2189 gpui::Point::new(0., 3.)
2190 );
2191 });
2192}
2193
2194#[gpui::test]
2195async fn test_autoscroll(cx: &mut TestAppContext) {
2196 init_test(cx, |_| {});
2197 let mut cx = EditorTestContext::new(cx).await;
2198
2199 let line_height = cx.update_editor(|editor, window, cx| {
2200 editor.set_vertical_scroll_margin(2, cx);
2201 editor
2202 .style()
2203 .unwrap()
2204 .text
2205 .line_height_in_pixels(window.rem_size())
2206 });
2207 let window = cx.window;
2208 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2209
2210 cx.set_state(
2211 r#"ˇone
2212 two
2213 three
2214 four
2215 five
2216 six
2217 seven
2218 eight
2219 nine
2220 ten
2221 "#,
2222 );
2223 cx.update_editor(|editor, window, cx| {
2224 assert_eq!(
2225 editor.snapshot(window, cx).scroll_position(),
2226 gpui::Point::new(0., 0.0)
2227 );
2228 });
2229
2230 // Add a cursor below the visible area. Since both cursors cannot fit
2231 // on screen, the editor autoscrolls to reveal the newest cursor, and
2232 // allows the vertical scroll margin below that cursor.
2233 cx.update_editor(|editor, window, cx| {
2234 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2235 selections.select_ranges([
2236 Point::new(0, 0)..Point::new(0, 0),
2237 Point::new(6, 0)..Point::new(6, 0),
2238 ]);
2239 })
2240 });
2241 cx.update_editor(|editor, window, cx| {
2242 assert_eq!(
2243 editor.snapshot(window, cx).scroll_position(),
2244 gpui::Point::new(0., 3.0)
2245 );
2246 });
2247
2248 // Move down. The editor cursor scrolls down to track the newest cursor.
2249 cx.update_editor(|editor, window, cx| {
2250 editor.move_down(&Default::default(), window, cx);
2251 });
2252 cx.update_editor(|editor, window, cx| {
2253 assert_eq!(
2254 editor.snapshot(window, cx).scroll_position(),
2255 gpui::Point::new(0., 4.0)
2256 );
2257 });
2258
2259 // Add a cursor above the visible area. Since both cursors fit on screen,
2260 // the editor scrolls to show both.
2261 cx.update_editor(|editor, window, cx| {
2262 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2263 selections.select_ranges([
2264 Point::new(1, 0)..Point::new(1, 0),
2265 Point::new(6, 0)..Point::new(6, 0),
2266 ]);
2267 })
2268 });
2269 cx.update_editor(|editor, window, cx| {
2270 assert_eq!(
2271 editor.snapshot(window, cx).scroll_position(),
2272 gpui::Point::new(0., 1.0)
2273 );
2274 });
2275}
2276
2277#[gpui::test]
2278async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2279 init_test(cx, |_| {});
2280 let mut cx = EditorTestContext::new(cx).await;
2281
2282 let line_height = cx.editor(|editor, window, _cx| {
2283 editor
2284 .style()
2285 .unwrap()
2286 .text
2287 .line_height_in_pixels(window.rem_size())
2288 });
2289 let window = cx.window;
2290 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2291 cx.set_state(
2292 &r#"
2293 ˇone
2294 two
2295 threeˇ
2296 four
2297 five
2298 six
2299 seven
2300 eight
2301 nine
2302 ten
2303 "#
2304 .unindent(),
2305 );
2306
2307 cx.update_editor(|editor, window, cx| {
2308 editor.move_page_down(&MovePageDown::default(), window, cx)
2309 });
2310 cx.assert_editor_state(
2311 &r#"
2312 one
2313 two
2314 three
2315 ˇfour
2316 five
2317 sixˇ
2318 seven
2319 eight
2320 nine
2321 ten
2322 "#
2323 .unindent(),
2324 );
2325
2326 cx.update_editor(|editor, window, cx| {
2327 editor.move_page_down(&MovePageDown::default(), window, cx)
2328 });
2329 cx.assert_editor_state(
2330 &r#"
2331 one
2332 two
2333 three
2334 four
2335 five
2336 six
2337 ˇseven
2338 eight
2339 nineˇ
2340 ten
2341 "#
2342 .unindent(),
2343 );
2344
2345 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2346 cx.assert_editor_state(
2347 &r#"
2348 one
2349 two
2350 three
2351 ˇfour
2352 five
2353 sixˇ
2354 seven
2355 eight
2356 nine
2357 ten
2358 "#
2359 .unindent(),
2360 );
2361
2362 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2363 cx.assert_editor_state(
2364 &r#"
2365 ˇone
2366 two
2367 threeˇ
2368 four
2369 five
2370 six
2371 seven
2372 eight
2373 nine
2374 ten
2375 "#
2376 .unindent(),
2377 );
2378
2379 // Test select collapsing
2380 cx.update_editor(|editor, window, cx| {
2381 editor.move_page_down(&MovePageDown::default(), window, cx);
2382 editor.move_page_down(&MovePageDown::default(), window, cx);
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 });
2385 cx.assert_editor_state(
2386 &r#"
2387 one
2388 two
2389 three
2390 four
2391 five
2392 six
2393 seven
2394 eight
2395 nine
2396 ˇten
2397 ˇ"#
2398 .unindent(),
2399 );
2400}
2401
2402#[gpui::test]
2403async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2404 init_test(cx, |_| {});
2405 let mut cx = EditorTestContext::new(cx).await;
2406 cx.set_state("one «two threeˇ» four");
2407 cx.update_editor(|editor, window, cx| {
2408 editor.delete_to_beginning_of_line(
2409 &DeleteToBeginningOfLine {
2410 stop_at_indent: false,
2411 },
2412 window,
2413 cx,
2414 );
2415 assert_eq!(editor.text(cx), " four");
2416 });
2417}
2418
2419#[gpui::test]
2420fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2421 init_test(cx, |_| {});
2422
2423 let editor = cx.add_window(|window, cx| {
2424 let buffer = MultiBuffer::build_simple("one two three four", cx);
2425 build_editor(buffer.clone(), window, cx)
2426 });
2427
2428 _ = editor.update(cx, |editor, window, cx| {
2429 editor.change_selections(None, window, cx, |s| {
2430 s.select_display_ranges([
2431 // an empty selection - the preceding word fragment is deleted
2432 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2433 // characters selected - they are deleted
2434 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2435 ])
2436 });
2437 editor.delete_to_previous_word_start(
2438 &DeleteToPreviousWordStart {
2439 ignore_newlines: false,
2440 },
2441 window,
2442 cx,
2443 );
2444 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2445 });
2446
2447 _ = editor.update(cx, |editor, window, cx| {
2448 editor.change_selections(None, window, cx, |s| {
2449 s.select_display_ranges([
2450 // an empty selection - the following word fragment is deleted
2451 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2452 // characters selected - they are deleted
2453 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2454 ])
2455 });
2456 editor.delete_to_next_word_end(
2457 &DeleteToNextWordEnd {
2458 ignore_newlines: false,
2459 },
2460 window,
2461 cx,
2462 );
2463 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2464 });
2465}
2466
2467#[gpui::test]
2468fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2469 init_test(cx, |_| {});
2470
2471 let editor = cx.add_window(|window, cx| {
2472 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2473 build_editor(buffer.clone(), window, cx)
2474 });
2475 let del_to_prev_word_start = DeleteToPreviousWordStart {
2476 ignore_newlines: false,
2477 };
2478 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2479 ignore_newlines: true,
2480 };
2481
2482 _ = editor.update(cx, |editor, window, cx| {
2483 editor.change_selections(None, window, cx, |s| {
2484 s.select_display_ranges([
2485 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2486 ])
2487 });
2488 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2489 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
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");
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\n");
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");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
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(), "");
2500 });
2501}
2502
2503#[gpui::test]
2504fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2505 init_test(cx, |_| {});
2506
2507 let editor = cx.add_window(|window, cx| {
2508 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2509 build_editor(buffer.clone(), window, cx)
2510 });
2511 let del_to_next_word_end = DeleteToNextWordEnd {
2512 ignore_newlines: false,
2513 };
2514 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2515 ignore_newlines: true,
2516 };
2517
2518 _ = editor.update(cx, |editor, window, cx| {
2519 editor.change_selections(None, window, cx, |s| {
2520 s.select_display_ranges([
2521 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2522 ])
2523 });
2524 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2525 assert_eq!(
2526 editor.buffer.read(cx).read(cx).text(),
2527 "one\n two\nthree\n four"
2528 );
2529 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2530 assert_eq!(
2531 editor.buffer.read(cx).read(cx).text(),
2532 "\n two\nthree\n four"
2533 );
2534 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2535 assert_eq!(
2536 editor.buffer.read(cx).read(cx).text(),
2537 "two\nthree\n four"
2538 );
2539 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2540 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2541 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2542 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\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(), "");
2545 });
2546}
2547
2548#[gpui::test]
2549fn test_newline(cx: &mut TestAppContext) {
2550 init_test(cx, |_| {});
2551
2552 let editor = cx.add_window(|window, cx| {
2553 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2554 build_editor(buffer.clone(), window, cx)
2555 });
2556
2557 _ = editor.update(cx, |editor, window, cx| {
2558 editor.change_selections(None, window, cx, |s| {
2559 s.select_display_ranges([
2560 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2561 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2562 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2563 ])
2564 });
2565
2566 editor.newline(&Newline, window, cx);
2567 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2568 });
2569}
2570
2571#[gpui::test]
2572fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2573 init_test(cx, |_| {});
2574
2575 let editor = cx.add_window(|window, cx| {
2576 let buffer = MultiBuffer::build_simple(
2577 "
2578 a
2579 b(
2580 X
2581 )
2582 c(
2583 X
2584 )
2585 "
2586 .unindent()
2587 .as_str(),
2588 cx,
2589 );
2590 let mut editor = build_editor(buffer.clone(), window, cx);
2591 editor.change_selections(None, window, cx, |s| {
2592 s.select_ranges([
2593 Point::new(2, 4)..Point::new(2, 5),
2594 Point::new(5, 4)..Point::new(5, 5),
2595 ])
2596 });
2597 editor
2598 });
2599
2600 _ = editor.update(cx, |editor, window, cx| {
2601 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2602 editor.buffer.update(cx, |buffer, cx| {
2603 buffer.edit(
2604 [
2605 (Point::new(1, 2)..Point::new(3, 0), ""),
2606 (Point::new(4, 2)..Point::new(6, 0), ""),
2607 ],
2608 None,
2609 cx,
2610 );
2611 assert_eq!(
2612 buffer.read(cx).text(),
2613 "
2614 a
2615 b()
2616 c()
2617 "
2618 .unindent()
2619 );
2620 });
2621 assert_eq!(
2622 editor.selections.ranges(cx),
2623 &[
2624 Point::new(1, 2)..Point::new(1, 2),
2625 Point::new(2, 2)..Point::new(2, 2),
2626 ],
2627 );
2628
2629 editor.newline(&Newline, window, cx);
2630 assert_eq!(
2631 editor.text(cx),
2632 "
2633 a
2634 b(
2635 )
2636 c(
2637 )
2638 "
2639 .unindent()
2640 );
2641
2642 // The selections are moved after the inserted newlines
2643 assert_eq!(
2644 editor.selections.ranges(cx),
2645 &[
2646 Point::new(2, 0)..Point::new(2, 0),
2647 Point::new(4, 0)..Point::new(4, 0),
2648 ],
2649 );
2650 });
2651}
2652
2653#[gpui::test]
2654async fn test_newline_above(cx: &mut TestAppContext) {
2655 init_test(cx, |settings| {
2656 settings.defaults.tab_size = NonZeroU32::new(4)
2657 });
2658
2659 let language = Arc::new(
2660 Language::new(
2661 LanguageConfig::default(),
2662 Some(tree_sitter_rust::LANGUAGE.into()),
2663 )
2664 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2665 .unwrap(),
2666 );
2667
2668 let mut cx = EditorTestContext::new(cx).await;
2669 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2670 cx.set_state(indoc! {"
2671 const a: ˇA = (
2672 (ˇ
2673 «const_functionˇ»(ˇ),
2674 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2675 )ˇ
2676 ˇ);ˇ
2677 "});
2678
2679 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2680 cx.assert_editor_state(indoc! {"
2681 ˇ
2682 const a: A = (
2683 ˇ
2684 (
2685 ˇ
2686 ˇ
2687 const_function(),
2688 ˇ
2689 ˇ
2690 ˇ
2691 ˇ
2692 something_else,
2693 ˇ
2694 )
2695 ˇ
2696 ˇ
2697 );
2698 "});
2699}
2700
2701#[gpui::test]
2702async fn test_newline_below(cx: &mut TestAppContext) {
2703 init_test(cx, |settings| {
2704 settings.defaults.tab_size = NonZeroU32::new(4)
2705 });
2706
2707 let language = Arc::new(
2708 Language::new(
2709 LanguageConfig::default(),
2710 Some(tree_sitter_rust::LANGUAGE.into()),
2711 )
2712 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2713 .unwrap(),
2714 );
2715
2716 let mut cx = EditorTestContext::new(cx).await;
2717 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2718 cx.set_state(indoc! {"
2719 const a: ˇA = (
2720 (ˇ
2721 «const_functionˇ»(ˇ),
2722 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2723 )ˇ
2724 ˇ);ˇ
2725 "});
2726
2727 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2728 cx.assert_editor_state(indoc! {"
2729 const a: A = (
2730 ˇ
2731 (
2732 ˇ
2733 const_function(),
2734 ˇ
2735 ˇ
2736 something_else,
2737 ˇ
2738 ˇ
2739 ˇ
2740 ˇ
2741 )
2742 ˇ
2743 );
2744 ˇ
2745 ˇ
2746 "});
2747}
2748
2749#[gpui::test]
2750async fn test_newline_comments(cx: &mut TestAppContext) {
2751 init_test(cx, |settings| {
2752 settings.defaults.tab_size = NonZeroU32::new(4)
2753 });
2754
2755 let language = Arc::new(Language::new(
2756 LanguageConfig {
2757 line_comments: vec!["//".into()],
2758 ..LanguageConfig::default()
2759 },
2760 None,
2761 ));
2762 {
2763 let mut cx = EditorTestContext::new(cx).await;
2764 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2765 cx.set_state(indoc! {"
2766 // Fooˇ
2767 "});
2768
2769 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2770 cx.assert_editor_state(indoc! {"
2771 // Foo
2772 //ˇ
2773 "});
2774 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2775 cx.set_state(indoc! {"
2776 ˇ// Foo
2777 "});
2778 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2779 cx.assert_editor_state(indoc! {"
2780
2781 ˇ// Foo
2782 "});
2783 }
2784 // Ensure that comment continuations can be disabled.
2785 update_test_language_settings(cx, |settings| {
2786 settings.defaults.extend_comment_on_newline = Some(false);
2787 });
2788 let mut cx = EditorTestContext::new(cx).await;
2789 cx.set_state(indoc! {"
2790 // Fooˇ
2791 "});
2792 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2793 cx.assert_editor_state(indoc! {"
2794 // Foo
2795 ˇ
2796 "});
2797}
2798
2799#[gpui::test]
2800fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2801 init_test(cx, |_| {});
2802
2803 let editor = cx.add_window(|window, cx| {
2804 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2805 let mut editor = build_editor(buffer.clone(), window, cx);
2806 editor.change_selections(None, window, cx, |s| {
2807 s.select_ranges([3..4, 11..12, 19..20])
2808 });
2809 editor
2810 });
2811
2812 _ = editor.update(cx, |editor, window, cx| {
2813 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2814 editor.buffer.update(cx, |buffer, cx| {
2815 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2816 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2817 });
2818 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2819
2820 editor.insert("Z", window, cx);
2821 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2822
2823 // The selections are moved after the inserted characters
2824 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2825 });
2826}
2827
2828#[gpui::test]
2829async fn test_tab(cx: &mut TestAppContext) {
2830 init_test(cx, |settings| {
2831 settings.defaults.tab_size = NonZeroU32::new(3)
2832 });
2833
2834 let mut cx = EditorTestContext::new(cx).await;
2835 cx.set_state(indoc! {"
2836 ˇabˇc
2837 ˇ🏀ˇ🏀ˇefg
2838 dˇ
2839 "});
2840 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2841 cx.assert_editor_state(indoc! {"
2842 ˇab ˇc
2843 ˇ🏀 ˇ🏀 ˇefg
2844 d ˇ
2845 "});
2846
2847 cx.set_state(indoc! {"
2848 a
2849 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2850 "});
2851 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2852 cx.assert_editor_state(indoc! {"
2853 a
2854 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2855 "});
2856}
2857
2858#[gpui::test]
2859async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2860 init_test(cx, |_| {});
2861
2862 let mut cx = EditorTestContext::new(cx).await;
2863 let language = Arc::new(
2864 Language::new(
2865 LanguageConfig::default(),
2866 Some(tree_sitter_rust::LANGUAGE.into()),
2867 )
2868 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2869 .unwrap(),
2870 );
2871 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2872
2873 // cursors that are already at the suggested indent level insert
2874 // a soft tab. cursors that are to the left of the suggested indent
2875 // auto-indent their line.
2876 cx.set_state(indoc! {"
2877 ˇ
2878 const a: B = (
2879 c(
2880 d(
2881 ˇ
2882 )
2883 ˇ
2884 ˇ )
2885 );
2886 "});
2887 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2888 cx.assert_editor_state(indoc! {"
2889 ˇ
2890 const a: B = (
2891 c(
2892 d(
2893 ˇ
2894 )
2895 ˇ
2896 ˇ)
2897 );
2898 "});
2899
2900 // handle auto-indent when there are multiple cursors on the same line
2901 cx.set_state(indoc! {"
2902 const a: B = (
2903 c(
2904 ˇ ˇ
2905 ˇ )
2906 );
2907 "});
2908 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2909 cx.assert_editor_state(indoc! {"
2910 const a: B = (
2911 c(
2912 ˇ
2913 ˇ)
2914 );
2915 "});
2916}
2917
2918#[gpui::test]
2919async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
2920 init_test(cx, |settings| {
2921 settings.defaults.tab_size = NonZeroU32::new(3)
2922 });
2923
2924 let mut cx = EditorTestContext::new(cx).await;
2925 cx.set_state(indoc! {"
2926 ˇ
2927 \t ˇ
2928 \t ˇ
2929 \t ˇ
2930 \t \t\t \t \t\t \t\t \t \t ˇ
2931 "});
2932
2933 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2934 cx.assert_editor_state(indoc! {"
2935 ˇ
2936 \t ˇ
2937 \t ˇ
2938 \t ˇ
2939 \t \t\t \t \t\t \t\t \t \t ˇ
2940 "});
2941}
2942
2943#[gpui::test]
2944async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
2945 init_test(cx, |settings| {
2946 settings.defaults.tab_size = NonZeroU32::new(4)
2947 });
2948
2949 let language = Arc::new(
2950 Language::new(
2951 LanguageConfig::default(),
2952 Some(tree_sitter_rust::LANGUAGE.into()),
2953 )
2954 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2955 .unwrap(),
2956 );
2957
2958 let mut cx = EditorTestContext::new(cx).await;
2959 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2960 cx.set_state(indoc! {"
2961 fn a() {
2962 if b {
2963 \t ˇc
2964 }
2965 }
2966 "});
2967
2968 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2969 cx.assert_editor_state(indoc! {"
2970 fn a() {
2971 if b {
2972 ˇc
2973 }
2974 }
2975 "});
2976}
2977
2978#[gpui::test]
2979async fn test_indent_outdent(cx: &mut TestAppContext) {
2980 init_test(cx, |settings| {
2981 settings.defaults.tab_size = NonZeroU32::new(4);
2982 });
2983
2984 let mut cx = EditorTestContext::new(cx).await;
2985
2986 cx.set_state(indoc! {"
2987 «oneˇ» «twoˇ»
2988 three
2989 four
2990 "});
2991 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2992 cx.assert_editor_state(indoc! {"
2993 «oneˇ» «twoˇ»
2994 three
2995 four
2996 "});
2997
2998 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
2999 cx.assert_editor_state(indoc! {"
3000 «oneˇ» «twoˇ»
3001 three
3002 four
3003 "});
3004
3005 // select across line ending
3006 cx.set_state(indoc! {"
3007 one two
3008 t«hree
3009 ˇ» four
3010 "});
3011 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3012 cx.assert_editor_state(indoc! {"
3013 one two
3014 t«hree
3015 ˇ» four
3016 "});
3017
3018 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3019 cx.assert_editor_state(indoc! {"
3020 one two
3021 t«hree
3022 ˇ» four
3023 "});
3024
3025 // Ensure that indenting/outdenting works when the cursor is at column 0.
3026 cx.set_state(indoc! {"
3027 one two
3028 ˇthree
3029 four
3030 "});
3031 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3032 cx.assert_editor_state(indoc! {"
3033 one two
3034 ˇthree
3035 four
3036 "});
3037
3038 cx.set_state(indoc! {"
3039 one two
3040 ˇ three
3041 four
3042 "});
3043 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3044 cx.assert_editor_state(indoc! {"
3045 one two
3046 ˇthree
3047 four
3048 "});
3049}
3050
3051#[gpui::test]
3052async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3053 init_test(cx, |settings| {
3054 settings.defaults.hard_tabs = Some(true);
3055 });
3056
3057 let mut cx = EditorTestContext::new(cx).await;
3058
3059 // select two ranges on one line
3060 cx.set_state(indoc! {"
3061 «oneˇ» «twoˇ»
3062 three
3063 four
3064 "});
3065 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3066 cx.assert_editor_state(indoc! {"
3067 \t«oneˇ» «twoˇ»
3068 three
3069 four
3070 "});
3071 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3072 cx.assert_editor_state(indoc! {"
3073 \t\t«oneˇ» «twoˇ»
3074 three
3075 four
3076 "});
3077 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3078 cx.assert_editor_state(indoc! {"
3079 \t«oneˇ» «twoˇ»
3080 three
3081 four
3082 "});
3083 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3084 cx.assert_editor_state(indoc! {"
3085 «oneˇ» «twoˇ»
3086 three
3087 four
3088 "});
3089
3090 // select across a line ending
3091 cx.set_state(indoc! {"
3092 one two
3093 t«hree
3094 ˇ»four
3095 "});
3096 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3097 cx.assert_editor_state(indoc! {"
3098 one two
3099 \tt«hree
3100 ˇ»four
3101 "});
3102 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3103 cx.assert_editor_state(indoc! {"
3104 one two
3105 \t\tt«hree
3106 ˇ»four
3107 "});
3108 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3109 cx.assert_editor_state(indoc! {"
3110 one two
3111 \tt«hree
3112 ˇ»four
3113 "});
3114 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3115 cx.assert_editor_state(indoc! {"
3116 one two
3117 t«hree
3118 ˇ»four
3119 "});
3120
3121 // Ensure that indenting/outdenting works when the cursor is at column 0.
3122 cx.set_state(indoc! {"
3123 one two
3124 ˇthree
3125 four
3126 "});
3127 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3128 cx.assert_editor_state(indoc! {"
3129 one two
3130 ˇthree
3131 four
3132 "});
3133 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3134 cx.assert_editor_state(indoc! {"
3135 one two
3136 \tˇthree
3137 four
3138 "});
3139 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3140 cx.assert_editor_state(indoc! {"
3141 one two
3142 ˇthree
3143 four
3144 "});
3145}
3146
3147#[gpui::test]
3148fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3149 init_test(cx, |settings| {
3150 settings.languages.extend([
3151 (
3152 "TOML".into(),
3153 LanguageSettingsContent {
3154 tab_size: NonZeroU32::new(2),
3155 ..Default::default()
3156 },
3157 ),
3158 (
3159 "Rust".into(),
3160 LanguageSettingsContent {
3161 tab_size: NonZeroU32::new(4),
3162 ..Default::default()
3163 },
3164 ),
3165 ]);
3166 });
3167
3168 let toml_language = Arc::new(Language::new(
3169 LanguageConfig {
3170 name: "TOML".into(),
3171 ..Default::default()
3172 },
3173 None,
3174 ));
3175 let rust_language = Arc::new(Language::new(
3176 LanguageConfig {
3177 name: "Rust".into(),
3178 ..Default::default()
3179 },
3180 None,
3181 ));
3182
3183 let toml_buffer =
3184 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3185 let rust_buffer =
3186 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3187 let multibuffer = cx.new(|cx| {
3188 let mut multibuffer = MultiBuffer::new(ReadWrite);
3189 multibuffer.push_excerpts(
3190 toml_buffer.clone(),
3191 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3192 cx,
3193 );
3194 multibuffer.push_excerpts(
3195 rust_buffer.clone(),
3196 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3197 cx,
3198 );
3199 multibuffer
3200 });
3201
3202 cx.add_window(|window, cx| {
3203 let mut editor = build_editor(multibuffer, window, cx);
3204
3205 assert_eq!(
3206 editor.text(cx),
3207 indoc! {"
3208 a = 1
3209 b = 2
3210
3211 const c: usize = 3;
3212 "}
3213 );
3214
3215 select_ranges(
3216 &mut editor,
3217 indoc! {"
3218 «aˇ» = 1
3219 b = 2
3220
3221 «const c:ˇ» usize = 3;
3222 "},
3223 window,
3224 cx,
3225 );
3226
3227 editor.tab(&Tab, window, cx);
3228 assert_text_with_selections(
3229 &mut editor,
3230 indoc! {"
3231 «aˇ» = 1
3232 b = 2
3233
3234 «const c:ˇ» usize = 3;
3235 "},
3236 cx,
3237 );
3238 editor.backtab(&Backtab, window, cx);
3239 assert_text_with_selections(
3240 &mut editor,
3241 indoc! {"
3242 «aˇ» = 1
3243 b = 2
3244
3245 «const c:ˇ» usize = 3;
3246 "},
3247 cx,
3248 );
3249
3250 editor
3251 });
3252}
3253
3254#[gpui::test]
3255async fn test_backspace(cx: &mut TestAppContext) {
3256 init_test(cx, |_| {});
3257
3258 let mut cx = EditorTestContext::new(cx).await;
3259
3260 // Basic backspace
3261 cx.set_state(indoc! {"
3262 onˇe two three
3263 fou«rˇ» five six
3264 seven «ˇeight nine
3265 »ten
3266 "});
3267 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3268 cx.assert_editor_state(indoc! {"
3269 oˇe two three
3270 fouˇ five six
3271 seven ˇten
3272 "});
3273
3274 // Test backspace inside and around indents
3275 cx.set_state(indoc! {"
3276 zero
3277 ˇone
3278 ˇtwo
3279 ˇ ˇ ˇ three
3280 ˇ ˇ four
3281 "});
3282 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3283 cx.assert_editor_state(indoc! {"
3284 zero
3285 ˇone
3286 ˇtwo
3287 ˇ threeˇ four
3288 "});
3289}
3290
3291#[gpui::test]
3292async fn test_delete(cx: &mut TestAppContext) {
3293 init_test(cx, |_| {});
3294
3295 let mut cx = EditorTestContext::new(cx).await;
3296 cx.set_state(indoc! {"
3297 onˇe two three
3298 fou«rˇ» five six
3299 seven «ˇeight nine
3300 »ten
3301 "});
3302 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3303 cx.assert_editor_state(indoc! {"
3304 onˇ two three
3305 fouˇ five six
3306 seven ˇten
3307 "});
3308}
3309
3310#[gpui::test]
3311fn test_delete_line(cx: &mut TestAppContext) {
3312 init_test(cx, |_| {});
3313
3314 let editor = cx.add_window(|window, cx| {
3315 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3316 build_editor(buffer, window, cx)
3317 });
3318 _ = editor.update(cx, |editor, window, cx| {
3319 editor.change_selections(None, window, cx, |s| {
3320 s.select_display_ranges([
3321 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3322 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3323 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3324 ])
3325 });
3326 editor.delete_line(&DeleteLine, window, cx);
3327 assert_eq!(editor.display_text(cx), "ghi");
3328 assert_eq!(
3329 editor.selections.display_ranges(cx),
3330 vec![
3331 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3332 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3333 ]
3334 );
3335 });
3336
3337 let editor = cx.add_window(|window, cx| {
3338 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3339 build_editor(buffer, window, cx)
3340 });
3341 _ = editor.update(cx, |editor, window, cx| {
3342 editor.change_selections(None, window, cx, |s| {
3343 s.select_display_ranges([
3344 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3345 ])
3346 });
3347 editor.delete_line(&DeleteLine, window, cx);
3348 assert_eq!(editor.display_text(cx), "ghi\n");
3349 assert_eq!(
3350 editor.selections.display_ranges(cx),
3351 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3352 );
3353 });
3354}
3355
3356#[gpui::test]
3357fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3358 init_test(cx, |_| {});
3359
3360 cx.add_window(|window, cx| {
3361 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3362 let mut editor = build_editor(buffer.clone(), window, cx);
3363 let buffer = buffer.read(cx).as_singleton().unwrap();
3364
3365 assert_eq!(
3366 editor.selections.ranges::<Point>(cx),
3367 &[Point::new(0, 0)..Point::new(0, 0)]
3368 );
3369
3370 // When on single line, replace newline at end by space
3371 editor.join_lines(&JoinLines, window, cx);
3372 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3373 assert_eq!(
3374 editor.selections.ranges::<Point>(cx),
3375 &[Point::new(0, 3)..Point::new(0, 3)]
3376 );
3377
3378 // When multiple lines are selected, remove newlines that are spanned by the selection
3379 editor.change_selections(None, window, cx, |s| {
3380 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3381 });
3382 editor.join_lines(&JoinLines, window, cx);
3383 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3384 assert_eq!(
3385 editor.selections.ranges::<Point>(cx),
3386 &[Point::new(0, 11)..Point::new(0, 11)]
3387 );
3388
3389 // Undo should be transactional
3390 editor.undo(&Undo, window, cx);
3391 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3392 assert_eq!(
3393 editor.selections.ranges::<Point>(cx),
3394 &[Point::new(0, 5)..Point::new(2, 2)]
3395 );
3396
3397 // When joining an empty line don't insert a space
3398 editor.change_selections(None, window, cx, |s| {
3399 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3400 });
3401 editor.join_lines(&JoinLines, window, cx);
3402 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3403 assert_eq!(
3404 editor.selections.ranges::<Point>(cx),
3405 [Point::new(2, 3)..Point::new(2, 3)]
3406 );
3407
3408 // We can remove trailing newlines
3409 editor.join_lines(&JoinLines, window, cx);
3410 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3411 assert_eq!(
3412 editor.selections.ranges::<Point>(cx),
3413 [Point::new(2, 3)..Point::new(2, 3)]
3414 );
3415
3416 // We don't blow up on the last line
3417 editor.join_lines(&JoinLines, window, cx);
3418 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3419 assert_eq!(
3420 editor.selections.ranges::<Point>(cx),
3421 [Point::new(2, 3)..Point::new(2, 3)]
3422 );
3423
3424 // reset to test indentation
3425 editor.buffer.update(cx, |buffer, cx| {
3426 buffer.edit(
3427 [
3428 (Point::new(1, 0)..Point::new(1, 2), " "),
3429 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3430 ],
3431 None,
3432 cx,
3433 )
3434 });
3435
3436 // We remove any leading spaces
3437 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3438 editor.change_selections(None, window, cx, |s| {
3439 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3440 });
3441 editor.join_lines(&JoinLines, window, cx);
3442 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3443
3444 // We don't insert a space for a line containing only spaces
3445 editor.join_lines(&JoinLines, window, cx);
3446 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3447
3448 // We ignore any leading tabs
3449 editor.join_lines(&JoinLines, window, cx);
3450 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3451
3452 editor
3453 });
3454}
3455
3456#[gpui::test]
3457fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3458 init_test(cx, |_| {});
3459
3460 cx.add_window(|window, cx| {
3461 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3462 let mut editor = build_editor(buffer.clone(), window, cx);
3463 let buffer = buffer.read(cx).as_singleton().unwrap();
3464
3465 editor.change_selections(None, window, cx, |s| {
3466 s.select_ranges([
3467 Point::new(0, 2)..Point::new(1, 1),
3468 Point::new(1, 2)..Point::new(1, 2),
3469 Point::new(3, 1)..Point::new(3, 2),
3470 ])
3471 });
3472
3473 editor.join_lines(&JoinLines, window, cx);
3474 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3475
3476 assert_eq!(
3477 editor.selections.ranges::<Point>(cx),
3478 [
3479 Point::new(0, 7)..Point::new(0, 7),
3480 Point::new(1, 3)..Point::new(1, 3)
3481 ]
3482 );
3483 editor
3484 });
3485}
3486
3487#[gpui::test]
3488async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3489 init_test(cx, |_| {});
3490
3491 let mut cx = EditorTestContext::new(cx).await;
3492
3493 let diff_base = r#"
3494 Line 0
3495 Line 1
3496 Line 2
3497 Line 3
3498 "#
3499 .unindent();
3500
3501 cx.set_state(
3502 &r#"
3503 ˇLine 0
3504 Line 1
3505 Line 2
3506 Line 3
3507 "#
3508 .unindent(),
3509 );
3510
3511 cx.set_head_text(&diff_base);
3512 executor.run_until_parked();
3513
3514 // Join lines
3515 cx.update_editor(|editor, window, cx| {
3516 editor.join_lines(&JoinLines, window, cx);
3517 });
3518 executor.run_until_parked();
3519
3520 cx.assert_editor_state(
3521 &r#"
3522 Line 0ˇ Line 1
3523 Line 2
3524 Line 3
3525 "#
3526 .unindent(),
3527 );
3528 // Join again
3529 cx.update_editor(|editor, window, cx| {
3530 editor.join_lines(&JoinLines, window, cx);
3531 });
3532 executor.run_until_parked();
3533
3534 cx.assert_editor_state(
3535 &r#"
3536 Line 0 Line 1ˇ Line 2
3537 Line 3
3538 "#
3539 .unindent(),
3540 );
3541}
3542
3543#[gpui::test]
3544async fn test_custom_newlines_cause_no_false_positive_diffs(
3545 executor: BackgroundExecutor,
3546 cx: &mut TestAppContext,
3547) {
3548 init_test(cx, |_| {});
3549 let mut cx = EditorTestContext::new(cx).await;
3550 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3551 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3552 executor.run_until_parked();
3553
3554 cx.update_editor(|editor, window, cx| {
3555 let snapshot = editor.snapshot(window, cx);
3556 assert_eq!(
3557 snapshot
3558 .buffer_snapshot
3559 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3560 .collect::<Vec<_>>(),
3561 Vec::new(),
3562 "Should not have any diffs for files with custom newlines"
3563 );
3564 });
3565}
3566
3567#[gpui::test]
3568async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3569 init_test(cx, |_| {});
3570
3571 let mut cx = EditorTestContext::new(cx).await;
3572
3573 // Test sort_lines_case_insensitive()
3574 cx.set_state(indoc! {"
3575 «z
3576 y
3577 x
3578 Z
3579 Y
3580 Xˇ»
3581 "});
3582 cx.update_editor(|e, window, cx| {
3583 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3584 });
3585 cx.assert_editor_state(indoc! {"
3586 «x
3587 X
3588 y
3589 Y
3590 z
3591 Zˇ»
3592 "});
3593
3594 // Test reverse_lines()
3595 cx.set_state(indoc! {"
3596 «5
3597 4
3598 3
3599 2
3600 1ˇ»
3601 "});
3602 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3603 cx.assert_editor_state(indoc! {"
3604 «1
3605 2
3606 3
3607 4
3608 5ˇ»
3609 "});
3610
3611 // Skip testing shuffle_line()
3612
3613 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3614 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3615
3616 // Don't manipulate when cursor is on single line, but expand the selection
3617 cx.set_state(indoc! {"
3618 ddˇdd
3619 ccc
3620 bb
3621 a
3622 "});
3623 cx.update_editor(|e, window, cx| {
3624 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3625 });
3626 cx.assert_editor_state(indoc! {"
3627 «ddddˇ»
3628 ccc
3629 bb
3630 a
3631 "});
3632
3633 // Basic manipulate case
3634 // Start selection moves to column 0
3635 // End of selection shrinks to fit shorter line
3636 cx.set_state(indoc! {"
3637 dd«d
3638 ccc
3639 bb
3640 aaaaaˇ»
3641 "});
3642 cx.update_editor(|e, window, cx| {
3643 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3644 });
3645 cx.assert_editor_state(indoc! {"
3646 «aaaaa
3647 bb
3648 ccc
3649 dddˇ»
3650 "});
3651
3652 // Manipulate case with newlines
3653 cx.set_state(indoc! {"
3654 dd«d
3655 ccc
3656
3657 bb
3658 aaaaa
3659
3660 ˇ»
3661 "});
3662 cx.update_editor(|e, window, cx| {
3663 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3664 });
3665 cx.assert_editor_state(indoc! {"
3666 «
3667
3668 aaaaa
3669 bb
3670 ccc
3671 dddˇ»
3672
3673 "});
3674
3675 // Adding new line
3676 cx.set_state(indoc! {"
3677 aa«a
3678 bbˇ»b
3679 "});
3680 cx.update_editor(|e, window, cx| {
3681 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3682 });
3683 cx.assert_editor_state(indoc! {"
3684 «aaa
3685 bbb
3686 added_lineˇ»
3687 "});
3688
3689 // Removing line
3690 cx.set_state(indoc! {"
3691 aa«a
3692 bbbˇ»
3693 "});
3694 cx.update_editor(|e, window, cx| {
3695 e.manipulate_lines(window, cx, |lines| {
3696 lines.pop();
3697 })
3698 });
3699 cx.assert_editor_state(indoc! {"
3700 «aaaˇ»
3701 "});
3702
3703 // Removing all lines
3704 cx.set_state(indoc! {"
3705 aa«a
3706 bbbˇ»
3707 "});
3708 cx.update_editor(|e, window, cx| {
3709 e.manipulate_lines(window, cx, |lines| {
3710 lines.drain(..);
3711 })
3712 });
3713 cx.assert_editor_state(indoc! {"
3714 ˇ
3715 "});
3716}
3717
3718#[gpui::test]
3719async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3720 init_test(cx, |_| {});
3721
3722 let mut cx = EditorTestContext::new(cx).await;
3723
3724 // Consider continuous selection as single selection
3725 cx.set_state(indoc! {"
3726 Aaa«aa
3727 cˇ»c«c
3728 bb
3729 aaaˇ»aa
3730 "});
3731 cx.update_editor(|e, window, cx| {
3732 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3733 });
3734 cx.assert_editor_state(indoc! {"
3735 «Aaaaa
3736 ccc
3737 bb
3738 aaaaaˇ»
3739 "});
3740
3741 cx.set_state(indoc! {"
3742 Aaa«aa
3743 cˇ»c«c
3744 bb
3745 aaaˇ»aa
3746 "});
3747 cx.update_editor(|e, window, cx| {
3748 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3749 });
3750 cx.assert_editor_state(indoc! {"
3751 «Aaaaa
3752 ccc
3753 bbˇ»
3754 "});
3755
3756 // Consider non continuous selection as distinct dedup operations
3757 cx.set_state(indoc! {"
3758 «aaaaa
3759 bb
3760 aaaaa
3761 aaaaaˇ»
3762
3763 aaa«aaˇ»
3764 "});
3765 cx.update_editor(|e, window, cx| {
3766 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3767 });
3768 cx.assert_editor_state(indoc! {"
3769 «aaaaa
3770 bbˇ»
3771
3772 «aaaaaˇ»
3773 "});
3774}
3775
3776#[gpui::test]
3777async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3778 init_test(cx, |_| {});
3779
3780 let mut cx = EditorTestContext::new(cx).await;
3781
3782 cx.set_state(indoc! {"
3783 «Aaa
3784 aAa
3785 Aaaˇ»
3786 "});
3787 cx.update_editor(|e, window, cx| {
3788 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3789 });
3790 cx.assert_editor_state(indoc! {"
3791 «Aaa
3792 aAaˇ»
3793 "});
3794
3795 cx.set_state(indoc! {"
3796 «Aaa
3797 aAa
3798 aaAˇ»
3799 "});
3800 cx.update_editor(|e, window, cx| {
3801 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3802 });
3803 cx.assert_editor_state(indoc! {"
3804 «Aaaˇ»
3805 "});
3806}
3807
3808#[gpui::test]
3809async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3810 init_test(cx, |_| {});
3811
3812 let mut cx = EditorTestContext::new(cx).await;
3813
3814 // Manipulate with multiple selections on a single line
3815 cx.set_state(indoc! {"
3816 dd«dd
3817 cˇ»c«c
3818 bb
3819 aaaˇ»aa
3820 "});
3821 cx.update_editor(|e, window, cx| {
3822 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3823 });
3824 cx.assert_editor_state(indoc! {"
3825 «aaaaa
3826 bb
3827 ccc
3828 ddddˇ»
3829 "});
3830
3831 // Manipulate with multiple disjoin selections
3832 cx.set_state(indoc! {"
3833 5«
3834 4
3835 3
3836 2
3837 1ˇ»
3838
3839 dd«dd
3840 ccc
3841 bb
3842 aaaˇ»aa
3843 "});
3844 cx.update_editor(|e, window, cx| {
3845 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3846 });
3847 cx.assert_editor_state(indoc! {"
3848 «1
3849 2
3850 3
3851 4
3852 5ˇ»
3853
3854 «aaaaa
3855 bb
3856 ccc
3857 ddddˇ»
3858 "});
3859
3860 // Adding lines on each selection
3861 cx.set_state(indoc! {"
3862 2«
3863 1ˇ»
3864
3865 bb«bb
3866 aaaˇ»aa
3867 "});
3868 cx.update_editor(|e, window, cx| {
3869 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3870 });
3871 cx.assert_editor_state(indoc! {"
3872 «2
3873 1
3874 added lineˇ»
3875
3876 «bbbb
3877 aaaaa
3878 added lineˇ»
3879 "});
3880
3881 // Removing lines on each selection
3882 cx.set_state(indoc! {"
3883 2«
3884 1ˇ»
3885
3886 bb«bb
3887 aaaˇ»aa
3888 "});
3889 cx.update_editor(|e, window, cx| {
3890 e.manipulate_lines(window, cx, |lines| {
3891 lines.pop();
3892 })
3893 });
3894 cx.assert_editor_state(indoc! {"
3895 «2ˇ»
3896
3897 «bbbbˇ»
3898 "});
3899}
3900
3901#[gpui::test]
3902async fn test_toggle_case(cx: &mut TestAppContext) {
3903 init_test(cx, |_| {});
3904
3905 let mut cx = EditorTestContext::new(cx).await;
3906
3907 // If all lower case -> upper case
3908 cx.set_state(indoc! {"
3909 «hello worldˇ»
3910 "});
3911 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3912 cx.assert_editor_state(indoc! {"
3913 «HELLO WORLDˇ»
3914 "});
3915
3916 // If all upper case -> lower case
3917 cx.set_state(indoc! {"
3918 «HELLO WORLDˇ»
3919 "});
3920 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3921 cx.assert_editor_state(indoc! {"
3922 «hello worldˇ»
3923 "});
3924
3925 // If any upper case characters are identified -> lower case
3926 // This matches JetBrains IDEs
3927 cx.set_state(indoc! {"
3928 «hEllo worldˇ»
3929 "});
3930 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3931 cx.assert_editor_state(indoc! {"
3932 «hello worldˇ»
3933 "});
3934}
3935
3936#[gpui::test]
3937async fn test_manipulate_text(cx: &mut TestAppContext) {
3938 init_test(cx, |_| {});
3939
3940 let mut cx = EditorTestContext::new(cx).await;
3941
3942 // Test convert_to_upper_case()
3943 cx.set_state(indoc! {"
3944 «hello worldˇ»
3945 "});
3946 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3947 cx.assert_editor_state(indoc! {"
3948 «HELLO WORLDˇ»
3949 "});
3950
3951 // Test convert_to_lower_case()
3952 cx.set_state(indoc! {"
3953 «HELLO WORLDˇ»
3954 "});
3955 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3956 cx.assert_editor_state(indoc! {"
3957 «hello worldˇ»
3958 "});
3959
3960 // Test multiple line, single selection case
3961 cx.set_state(indoc! {"
3962 «The quick brown
3963 fox jumps over
3964 the lazy dogˇ»
3965 "});
3966 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3967 cx.assert_editor_state(indoc! {"
3968 «The Quick Brown
3969 Fox Jumps Over
3970 The Lazy Dogˇ»
3971 "});
3972
3973 // Test multiple line, single selection case
3974 cx.set_state(indoc! {"
3975 «The quick brown
3976 fox jumps over
3977 the lazy dogˇ»
3978 "});
3979 cx.update_editor(|e, window, cx| {
3980 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3981 });
3982 cx.assert_editor_state(indoc! {"
3983 «TheQuickBrown
3984 FoxJumpsOver
3985 TheLazyDogˇ»
3986 "});
3987
3988 // From here on out, test more complex cases of manipulate_text()
3989
3990 // Test no selection case - should affect words cursors are in
3991 // Cursor at beginning, middle, and end of word
3992 cx.set_state(indoc! {"
3993 ˇhello big beauˇtiful worldˇ
3994 "});
3995 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3996 cx.assert_editor_state(indoc! {"
3997 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
3998 "});
3999
4000 // Test multiple selections on a single line and across multiple lines
4001 cx.set_state(indoc! {"
4002 «Theˇ» quick «brown
4003 foxˇ» jumps «overˇ»
4004 the «lazyˇ» dog
4005 "});
4006 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4007 cx.assert_editor_state(indoc! {"
4008 «THEˇ» quick «BROWN
4009 FOXˇ» jumps «OVERˇ»
4010 the «LAZYˇ» dog
4011 "});
4012
4013 // Test case where text length grows
4014 cx.set_state(indoc! {"
4015 «tschüߡ»
4016 "});
4017 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4018 cx.assert_editor_state(indoc! {"
4019 «TSCHÜSSˇ»
4020 "});
4021
4022 // Test to make sure we don't crash when text shrinks
4023 cx.set_state(indoc! {"
4024 aaa_bbbˇ
4025 "});
4026 cx.update_editor(|e, window, cx| {
4027 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4028 });
4029 cx.assert_editor_state(indoc! {"
4030 «aaaBbbˇ»
4031 "});
4032
4033 // Test to make sure we all aware of the fact that each word can grow and shrink
4034 // Final selections should be aware of this fact
4035 cx.set_state(indoc! {"
4036 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4037 "});
4038 cx.update_editor(|e, window, cx| {
4039 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4040 });
4041 cx.assert_editor_state(indoc! {"
4042 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4043 "});
4044
4045 cx.set_state(indoc! {"
4046 «hElLo, WoRld!ˇ»
4047 "});
4048 cx.update_editor(|e, window, cx| {
4049 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4050 });
4051 cx.assert_editor_state(indoc! {"
4052 «HeLlO, wOrLD!ˇ»
4053 "});
4054}
4055
4056#[gpui::test]
4057fn test_duplicate_line(cx: &mut TestAppContext) {
4058 init_test(cx, |_| {});
4059
4060 let editor = cx.add_window(|window, cx| {
4061 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4062 build_editor(buffer, window, cx)
4063 });
4064 _ = editor.update(cx, |editor, window, cx| {
4065 editor.change_selections(None, window, cx, |s| {
4066 s.select_display_ranges([
4067 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4068 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4069 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4070 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4071 ])
4072 });
4073 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4074 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4075 assert_eq!(
4076 editor.selections.display_ranges(cx),
4077 vec![
4078 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4079 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4080 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4081 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4082 ]
4083 );
4084 });
4085
4086 let editor = cx.add_window(|window, cx| {
4087 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4088 build_editor(buffer, window, cx)
4089 });
4090 _ = editor.update(cx, |editor, window, cx| {
4091 editor.change_selections(None, window, cx, |s| {
4092 s.select_display_ranges([
4093 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4094 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4095 ])
4096 });
4097 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4098 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4099 assert_eq!(
4100 editor.selections.display_ranges(cx),
4101 vec![
4102 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4103 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4104 ]
4105 );
4106 });
4107
4108 // With `move_upwards` the selections stay in place, except for
4109 // the lines inserted above them
4110 let editor = cx.add_window(|window, cx| {
4111 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4112 build_editor(buffer, window, cx)
4113 });
4114 _ = editor.update(cx, |editor, window, cx| {
4115 editor.change_selections(None, window, cx, |s| {
4116 s.select_display_ranges([
4117 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4118 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4119 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4120 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4121 ])
4122 });
4123 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4124 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4125 assert_eq!(
4126 editor.selections.display_ranges(cx),
4127 vec![
4128 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4129 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4130 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4131 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4132 ]
4133 );
4134 });
4135
4136 let editor = cx.add_window(|window, cx| {
4137 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4138 build_editor(buffer, window, cx)
4139 });
4140 _ = editor.update(cx, |editor, window, cx| {
4141 editor.change_selections(None, window, cx, |s| {
4142 s.select_display_ranges([
4143 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4144 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4145 ])
4146 });
4147 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4148 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4149 assert_eq!(
4150 editor.selections.display_ranges(cx),
4151 vec![
4152 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4153 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4154 ]
4155 );
4156 });
4157
4158 let editor = cx.add_window(|window, cx| {
4159 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4160 build_editor(buffer, window, cx)
4161 });
4162 _ = editor.update(cx, |editor, window, cx| {
4163 editor.change_selections(None, window, cx, |s| {
4164 s.select_display_ranges([
4165 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4166 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4167 ])
4168 });
4169 editor.duplicate_selection(&DuplicateSelection, window, cx);
4170 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4171 assert_eq!(
4172 editor.selections.display_ranges(cx),
4173 vec![
4174 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4175 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4176 ]
4177 );
4178 });
4179}
4180
4181#[gpui::test]
4182fn test_move_line_up_down(cx: &mut TestAppContext) {
4183 init_test(cx, |_| {});
4184
4185 let editor = cx.add_window(|window, cx| {
4186 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4187 build_editor(buffer, window, cx)
4188 });
4189 _ = editor.update(cx, |editor, window, cx| {
4190 editor.fold_creases(
4191 vec![
4192 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4193 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4194 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4195 ],
4196 true,
4197 window,
4198 cx,
4199 );
4200 editor.change_selections(None, window, cx, |s| {
4201 s.select_display_ranges([
4202 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4203 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4204 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4205 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4206 ])
4207 });
4208 assert_eq!(
4209 editor.display_text(cx),
4210 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4211 );
4212
4213 editor.move_line_up(&MoveLineUp, window, cx);
4214 assert_eq!(
4215 editor.display_text(cx),
4216 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4217 );
4218 assert_eq!(
4219 editor.selections.display_ranges(cx),
4220 vec![
4221 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4222 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4223 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4224 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4225 ]
4226 );
4227 });
4228
4229 _ = editor.update(cx, |editor, window, cx| {
4230 editor.move_line_down(&MoveLineDown, window, cx);
4231 assert_eq!(
4232 editor.display_text(cx),
4233 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4234 );
4235 assert_eq!(
4236 editor.selections.display_ranges(cx),
4237 vec![
4238 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4239 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4240 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4241 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4242 ]
4243 );
4244 });
4245
4246 _ = editor.update(cx, |editor, window, cx| {
4247 editor.move_line_down(&MoveLineDown, window, cx);
4248 assert_eq!(
4249 editor.display_text(cx),
4250 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4251 );
4252 assert_eq!(
4253 editor.selections.display_ranges(cx),
4254 vec![
4255 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4256 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4257 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4258 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4259 ]
4260 );
4261 });
4262
4263 _ = editor.update(cx, |editor, window, cx| {
4264 editor.move_line_up(&MoveLineUp, window, cx);
4265 assert_eq!(
4266 editor.display_text(cx),
4267 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4268 );
4269 assert_eq!(
4270 editor.selections.display_ranges(cx),
4271 vec![
4272 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4273 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4274 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4275 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4276 ]
4277 );
4278 });
4279}
4280
4281#[gpui::test]
4282fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4283 init_test(cx, |_| {});
4284
4285 let editor = cx.add_window(|window, cx| {
4286 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4287 build_editor(buffer, window, cx)
4288 });
4289 _ = editor.update(cx, |editor, window, cx| {
4290 let snapshot = editor.buffer.read(cx).snapshot(cx);
4291 editor.insert_blocks(
4292 [BlockProperties {
4293 style: BlockStyle::Fixed,
4294 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4295 height: Some(1),
4296 render: Arc::new(|_| div().into_any()),
4297 priority: 0,
4298 }],
4299 Some(Autoscroll::fit()),
4300 cx,
4301 );
4302 editor.change_selections(None, window, cx, |s| {
4303 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4304 });
4305 editor.move_line_down(&MoveLineDown, window, cx);
4306 });
4307}
4308
4309#[gpui::test]
4310async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4311 init_test(cx, |_| {});
4312
4313 let mut cx = EditorTestContext::new(cx).await;
4314 cx.set_state(
4315 &"
4316 ˇzero
4317 one
4318 two
4319 three
4320 four
4321 five
4322 "
4323 .unindent(),
4324 );
4325
4326 // Create a four-line block that replaces three lines of text.
4327 cx.update_editor(|editor, window, cx| {
4328 let snapshot = editor.snapshot(window, cx);
4329 let snapshot = &snapshot.buffer_snapshot;
4330 let placement = BlockPlacement::Replace(
4331 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4332 );
4333 editor.insert_blocks(
4334 [BlockProperties {
4335 placement,
4336 height: Some(4),
4337 style: BlockStyle::Sticky,
4338 render: Arc::new(|_| gpui::div().into_any_element()),
4339 priority: 0,
4340 }],
4341 None,
4342 cx,
4343 );
4344 });
4345
4346 // Move down so that the cursor touches the block.
4347 cx.update_editor(|editor, window, cx| {
4348 editor.move_down(&Default::default(), window, cx);
4349 });
4350 cx.assert_editor_state(
4351 &"
4352 zero
4353 «one
4354 two
4355 threeˇ»
4356 four
4357 five
4358 "
4359 .unindent(),
4360 );
4361
4362 // Move down past the block.
4363 cx.update_editor(|editor, window, cx| {
4364 editor.move_down(&Default::default(), window, cx);
4365 });
4366 cx.assert_editor_state(
4367 &"
4368 zero
4369 one
4370 two
4371 three
4372 ˇfour
4373 five
4374 "
4375 .unindent(),
4376 );
4377}
4378
4379#[gpui::test]
4380fn test_transpose(cx: &mut TestAppContext) {
4381 init_test(cx, |_| {});
4382
4383 _ = cx.add_window(|window, cx| {
4384 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4385 editor.set_style(EditorStyle::default(), window, cx);
4386 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4387 editor.transpose(&Default::default(), window, cx);
4388 assert_eq!(editor.text(cx), "bac");
4389 assert_eq!(editor.selections.ranges(cx), [2..2]);
4390
4391 editor.transpose(&Default::default(), window, cx);
4392 assert_eq!(editor.text(cx), "bca");
4393 assert_eq!(editor.selections.ranges(cx), [3..3]);
4394
4395 editor.transpose(&Default::default(), window, cx);
4396 assert_eq!(editor.text(cx), "bac");
4397 assert_eq!(editor.selections.ranges(cx), [3..3]);
4398
4399 editor
4400 });
4401
4402 _ = cx.add_window(|window, cx| {
4403 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4404 editor.set_style(EditorStyle::default(), window, cx);
4405 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4406 editor.transpose(&Default::default(), window, cx);
4407 assert_eq!(editor.text(cx), "acb\nde");
4408 assert_eq!(editor.selections.ranges(cx), [3..3]);
4409
4410 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4411 editor.transpose(&Default::default(), window, cx);
4412 assert_eq!(editor.text(cx), "acbd\ne");
4413 assert_eq!(editor.selections.ranges(cx), [5..5]);
4414
4415 editor.transpose(&Default::default(), window, cx);
4416 assert_eq!(editor.text(cx), "acbde\n");
4417 assert_eq!(editor.selections.ranges(cx), [6..6]);
4418
4419 editor.transpose(&Default::default(), window, cx);
4420 assert_eq!(editor.text(cx), "acbd\ne");
4421 assert_eq!(editor.selections.ranges(cx), [6..6]);
4422
4423 editor
4424 });
4425
4426 _ = cx.add_window(|window, cx| {
4427 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4428 editor.set_style(EditorStyle::default(), window, cx);
4429 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4430 editor.transpose(&Default::default(), window, cx);
4431 assert_eq!(editor.text(cx), "bacd\ne");
4432 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4433
4434 editor.transpose(&Default::default(), window, cx);
4435 assert_eq!(editor.text(cx), "bcade\n");
4436 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4437
4438 editor.transpose(&Default::default(), window, cx);
4439 assert_eq!(editor.text(cx), "bcda\ne");
4440 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4441
4442 editor.transpose(&Default::default(), window, cx);
4443 assert_eq!(editor.text(cx), "bcade\n");
4444 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4445
4446 editor.transpose(&Default::default(), window, cx);
4447 assert_eq!(editor.text(cx), "bcaed\n");
4448 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4449
4450 editor
4451 });
4452
4453 _ = cx.add_window(|window, cx| {
4454 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4455 editor.set_style(EditorStyle::default(), window, cx);
4456 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4457 editor.transpose(&Default::default(), window, cx);
4458 assert_eq!(editor.text(cx), "🏀🍐✋");
4459 assert_eq!(editor.selections.ranges(cx), [8..8]);
4460
4461 editor.transpose(&Default::default(), window, cx);
4462 assert_eq!(editor.text(cx), "🏀✋🍐");
4463 assert_eq!(editor.selections.ranges(cx), [11..11]);
4464
4465 editor.transpose(&Default::default(), window, cx);
4466 assert_eq!(editor.text(cx), "🏀🍐✋");
4467 assert_eq!(editor.selections.ranges(cx), [11..11]);
4468
4469 editor
4470 });
4471}
4472
4473#[gpui::test]
4474async fn test_rewrap(cx: &mut TestAppContext) {
4475 init_test(cx, |settings| {
4476 settings.languages.extend([
4477 (
4478 "Markdown".into(),
4479 LanguageSettingsContent {
4480 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4481 ..Default::default()
4482 },
4483 ),
4484 (
4485 "Plain Text".into(),
4486 LanguageSettingsContent {
4487 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4488 ..Default::default()
4489 },
4490 ),
4491 ])
4492 });
4493
4494 let mut cx = EditorTestContext::new(cx).await;
4495
4496 let language_with_c_comments = Arc::new(Language::new(
4497 LanguageConfig {
4498 line_comments: vec!["// ".into()],
4499 ..LanguageConfig::default()
4500 },
4501 None,
4502 ));
4503 let language_with_pound_comments = Arc::new(Language::new(
4504 LanguageConfig {
4505 line_comments: vec!["# ".into()],
4506 ..LanguageConfig::default()
4507 },
4508 None,
4509 ));
4510 let markdown_language = Arc::new(Language::new(
4511 LanguageConfig {
4512 name: "Markdown".into(),
4513 ..LanguageConfig::default()
4514 },
4515 None,
4516 ));
4517 let language_with_doc_comments = Arc::new(Language::new(
4518 LanguageConfig {
4519 line_comments: vec!["// ".into(), "/// ".into()],
4520 ..LanguageConfig::default()
4521 },
4522 Some(tree_sitter_rust::LANGUAGE.into()),
4523 ));
4524
4525 let plaintext_language = Arc::new(Language::new(
4526 LanguageConfig {
4527 name: "Plain Text".into(),
4528 ..LanguageConfig::default()
4529 },
4530 None,
4531 ));
4532
4533 assert_rewrap(
4534 indoc! {"
4535 // ˇ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.
4536 "},
4537 indoc! {"
4538 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4539 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4540 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4541 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4542 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4543 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4544 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4545 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4546 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4547 // porttitor id. Aliquam id accumsan eros.
4548 "},
4549 language_with_c_comments.clone(),
4550 &mut cx,
4551 );
4552
4553 // Test that rewrapping works inside of a selection
4554 assert_rewrap(
4555 indoc! {"
4556 «// 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.ˇ»
4557 "},
4558 indoc! {"
4559 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4560 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4561 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4562 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4563 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4564 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4565 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4566 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4567 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4568 // porttitor id. Aliquam id accumsan eros.ˇ»
4569 "},
4570 language_with_c_comments.clone(),
4571 &mut cx,
4572 );
4573
4574 // Test that cursors that expand to the same region are collapsed.
4575 assert_rewrap(
4576 indoc! {"
4577 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4578 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4579 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4580 // ˇ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.
4581 "},
4582 indoc! {"
4583 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4584 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4585 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4586 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4587 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4588 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4589 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4590 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4591 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4592 // porttitor id. Aliquam id accumsan eros.
4593 "},
4594 language_with_c_comments.clone(),
4595 &mut cx,
4596 );
4597
4598 // Test that non-contiguous selections are treated separately.
4599 assert_rewrap(
4600 indoc! {"
4601 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4602 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4603 //
4604 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4605 // ˇ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.
4606 "},
4607 indoc! {"
4608 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4609 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4610 // auctor, eu lacinia sapien scelerisque.
4611 //
4612 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4613 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4614 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4615 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4616 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4617 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4618 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4619 "},
4620 language_with_c_comments.clone(),
4621 &mut cx,
4622 );
4623
4624 // Test that different comment prefixes are supported.
4625 assert_rewrap(
4626 indoc! {"
4627 # ˇ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.
4628 "},
4629 indoc! {"
4630 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4631 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4632 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4633 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4634 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4635 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4636 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4637 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4638 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4639 # accumsan eros.
4640 "},
4641 language_with_pound_comments.clone(),
4642 &mut cx,
4643 );
4644
4645 // Test that rewrapping is ignored outside of comments in most languages.
4646 assert_rewrap(
4647 indoc! {"
4648 /// Adds two numbers.
4649 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4650 fn add(a: u32, b: u32) -> u32 {
4651 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ˇ
4652 }
4653 "},
4654 indoc! {"
4655 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4656 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4657 fn add(a: u32, b: u32) -> u32 {
4658 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ˇ
4659 }
4660 "},
4661 language_with_doc_comments.clone(),
4662 &mut cx,
4663 );
4664
4665 // Test that rewrapping works in Markdown and Plain Text languages.
4666 assert_rewrap(
4667 indoc! {"
4668 # Hello
4669
4670 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.
4671 "},
4672 indoc! {"
4673 # Hello
4674
4675 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4676 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4677 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4678 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4679 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4680 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4681 Integer sit amet scelerisque nisi.
4682 "},
4683 markdown_language,
4684 &mut cx,
4685 );
4686
4687 assert_rewrap(
4688 indoc! {"
4689 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.
4690 "},
4691 indoc! {"
4692 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4693 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4694 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4695 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4696 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4697 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4698 Integer sit amet scelerisque nisi.
4699 "},
4700 plaintext_language,
4701 &mut cx,
4702 );
4703
4704 // Test rewrapping unaligned comments in a selection.
4705 assert_rewrap(
4706 indoc! {"
4707 fn foo() {
4708 if true {
4709 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4710 // Praesent semper egestas tellus id dignissim.ˇ»
4711 do_something();
4712 } else {
4713 //
4714 }
4715 }
4716 "},
4717 indoc! {"
4718 fn foo() {
4719 if true {
4720 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4721 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4722 // egestas tellus id dignissim.ˇ»
4723 do_something();
4724 } else {
4725 //
4726 }
4727 }
4728 "},
4729 language_with_doc_comments.clone(),
4730 &mut cx,
4731 );
4732
4733 assert_rewrap(
4734 indoc! {"
4735 fn foo() {
4736 if true {
4737 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4738 // Praesent semper egestas tellus id dignissim.»
4739 do_something();
4740 } else {
4741 //
4742 }
4743
4744 }
4745 "},
4746 indoc! {"
4747 fn foo() {
4748 if true {
4749 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4750 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4751 // egestas tellus id dignissim.»
4752 do_something();
4753 } else {
4754 //
4755 }
4756
4757 }
4758 "},
4759 language_with_doc_comments.clone(),
4760 &mut cx,
4761 );
4762
4763 #[track_caller]
4764 fn assert_rewrap(
4765 unwrapped_text: &str,
4766 wrapped_text: &str,
4767 language: Arc<Language>,
4768 cx: &mut EditorTestContext,
4769 ) {
4770 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4771 cx.set_state(unwrapped_text);
4772 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4773 cx.assert_editor_state(wrapped_text);
4774 }
4775}
4776
4777#[gpui::test]
4778async fn test_hard_wrap(cx: &mut TestAppContext) {
4779 init_test(cx, |_| {});
4780 let mut cx = EditorTestContext::new(cx).await;
4781
4782 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4783 cx.update_editor(|editor, _, cx| {
4784 editor.set_hard_wrap(Some(14), cx);
4785 });
4786
4787 cx.set_state(indoc!(
4788 "
4789 one two three ˇ
4790 "
4791 ));
4792 cx.simulate_input("four");
4793 cx.run_until_parked();
4794
4795 cx.assert_editor_state(indoc!(
4796 "
4797 one two three
4798 fourˇ
4799 "
4800 ));
4801
4802 cx.update_editor(|editor, window, cx| {
4803 editor.newline(&Default::default(), window, cx);
4804 });
4805 cx.run_until_parked();
4806 cx.assert_editor_state(indoc!(
4807 "
4808 one two three
4809 four
4810 ˇ
4811 "
4812 ));
4813
4814 cx.simulate_input("five");
4815 cx.run_until_parked();
4816 cx.assert_editor_state(indoc!(
4817 "
4818 one two three
4819 four
4820 fiveˇ
4821 "
4822 ));
4823
4824 cx.update_editor(|editor, window, cx| {
4825 editor.newline(&Default::default(), window, cx);
4826 });
4827 cx.run_until_parked();
4828 cx.simulate_input("# ");
4829 cx.run_until_parked();
4830 cx.assert_editor_state(indoc!(
4831 "
4832 one two three
4833 four
4834 five
4835 # ˇ
4836 "
4837 ));
4838
4839 cx.update_editor(|editor, window, cx| {
4840 editor.newline(&Default::default(), window, cx);
4841 });
4842 cx.run_until_parked();
4843 cx.assert_editor_state(indoc!(
4844 "
4845 one two three
4846 four
4847 five
4848 #\x20
4849 #ˇ
4850 "
4851 ));
4852
4853 cx.simulate_input(" 6");
4854 cx.run_until_parked();
4855 cx.assert_editor_state(indoc!(
4856 "
4857 one two three
4858 four
4859 five
4860 #
4861 # 6ˇ
4862 "
4863 ));
4864}
4865
4866#[gpui::test]
4867async fn test_clipboard(cx: &mut TestAppContext) {
4868 init_test(cx, |_| {});
4869
4870 let mut cx = EditorTestContext::new(cx).await;
4871
4872 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4873 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4874 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4875
4876 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4877 cx.set_state("two ˇfour ˇsix ˇ");
4878 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4879 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4880
4881 // Paste again but with only two cursors. Since the number of cursors doesn't
4882 // match the number of slices in the clipboard, the entire clipboard text
4883 // is pasted at each cursor.
4884 cx.set_state("ˇtwo one✅ four three six five ˇ");
4885 cx.update_editor(|e, window, cx| {
4886 e.handle_input("( ", window, cx);
4887 e.paste(&Paste, window, cx);
4888 e.handle_input(") ", window, cx);
4889 });
4890 cx.assert_editor_state(
4891 &([
4892 "( one✅ ",
4893 "three ",
4894 "five ) ˇtwo one✅ four three six five ( one✅ ",
4895 "three ",
4896 "five ) ˇ",
4897 ]
4898 .join("\n")),
4899 );
4900
4901 // Cut with three selections, one of which is full-line.
4902 cx.set_state(indoc! {"
4903 1«2ˇ»3
4904 4ˇ567
4905 «8ˇ»9"});
4906 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4907 cx.assert_editor_state(indoc! {"
4908 1ˇ3
4909 ˇ9"});
4910
4911 // Paste with three selections, noticing how the copied selection that was full-line
4912 // gets inserted before the second cursor.
4913 cx.set_state(indoc! {"
4914 1ˇ3
4915 9ˇ
4916 «oˇ»ne"});
4917 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4918 cx.assert_editor_state(indoc! {"
4919 12ˇ3
4920 4567
4921 9ˇ
4922 8ˇne"});
4923
4924 // Copy with a single cursor only, which writes the whole line into the clipboard.
4925 cx.set_state(indoc! {"
4926 The quick brown
4927 fox juˇmps over
4928 the lazy dog"});
4929 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4930 assert_eq!(
4931 cx.read_from_clipboard()
4932 .and_then(|item| item.text().as_deref().map(str::to_string)),
4933 Some("fox jumps over\n".to_string())
4934 );
4935
4936 // Paste with three selections, noticing how the copied full-line selection is inserted
4937 // before the empty selections but replaces the selection that is non-empty.
4938 cx.set_state(indoc! {"
4939 Tˇhe quick brown
4940 «foˇ»x jumps over
4941 tˇhe lazy dog"});
4942 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4943 cx.assert_editor_state(indoc! {"
4944 fox jumps over
4945 Tˇhe quick brown
4946 fox jumps over
4947 ˇx jumps over
4948 fox jumps over
4949 tˇhe lazy dog"});
4950}
4951
4952#[gpui::test]
4953async fn test_copy_trim(cx: &mut TestAppContext) {
4954 init_test(cx, |_| {});
4955
4956 let mut cx = EditorTestContext::new(cx).await;
4957 cx.set_state(
4958 r#" «for selection in selections.iter() {
4959 let mut start = selection.start;
4960 let mut end = selection.end;
4961 let is_entire_line = selection.is_empty();
4962 if is_entire_line {
4963 start = Point::new(start.row, 0);ˇ»
4964 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4965 }
4966 "#,
4967 );
4968 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4969 assert_eq!(
4970 cx.read_from_clipboard()
4971 .and_then(|item| item.text().as_deref().map(str::to_string)),
4972 Some(
4973 "for selection in selections.iter() {
4974 let mut start = selection.start;
4975 let mut end = selection.end;
4976 let is_entire_line = selection.is_empty();
4977 if is_entire_line {
4978 start = Point::new(start.row, 0);"
4979 .to_string()
4980 ),
4981 "Regular copying preserves all indentation selected",
4982 );
4983 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4984 assert_eq!(
4985 cx.read_from_clipboard()
4986 .and_then(|item| item.text().as_deref().map(str::to_string)),
4987 Some(
4988 "for selection in selections.iter() {
4989let mut start = selection.start;
4990let mut end = selection.end;
4991let is_entire_line = selection.is_empty();
4992if is_entire_line {
4993 start = Point::new(start.row, 0);"
4994 .to_string()
4995 ),
4996 "Copying with stripping should strip all leading whitespaces"
4997 );
4998
4999 cx.set_state(
5000 r#" « for selection in selections.iter() {
5001 let mut start = selection.start;
5002 let mut end = selection.end;
5003 let is_entire_line = selection.is_empty();
5004 if is_entire_line {
5005 start = Point::new(start.row, 0);ˇ»
5006 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5007 }
5008 "#,
5009 );
5010 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5011 assert_eq!(
5012 cx.read_from_clipboard()
5013 .and_then(|item| item.text().as_deref().map(str::to_string)),
5014 Some(
5015 " for selection in selections.iter() {
5016 let mut start = selection.start;
5017 let mut end = selection.end;
5018 let is_entire_line = selection.is_empty();
5019 if is_entire_line {
5020 start = Point::new(start.row, 0);"
5021 .to_string()
5022 ),
5023 "Regular copying preserves all indentation selected",
5024 );
5025 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5026 assert_eq!(
5027 cx.read_from_clipboard()
5028 .and_then(|item| item.text().as_deref().map(str::to_string)),
5029 Some(
5030 "for selection in selections.iter() {
5031let mut start = selection.start;
5032let mut end = selection.end;
5033let is_entire_line = selection.is_empty();
5034if is_entire_line {
5035 start = Point::new(start.row, 0);"
5036 .to_string()
5037 ),
5038 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5039 );
5040
5041 cx.set_state(
5042 r#" «ˇ for selection in selections.iter() {
5043 let mut start = selection.start;
5044 let mut end = selection.end;
5045 let is_entire_line = selection.is_empty();
5046 if is_entire_line {
5047 start = Point::new(start.row, 0);»
5048 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5049 }
5050 "#,
5051 );
5052 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5053 assert_eq!(
5054 cx.read_from_clipboard()
5055 .and_then(|item| item.text().as_deref().map(str::to_string)),
5056 Some(
5057 " for selection in selections.iter() {
5058 let mut start = selection.start;
5059 let mut end = selection.end;
5060 let is_entire_line = selection.is_empty();
5061 if is_entire_line {
5062 start = Point::new(start.row, 0);"
5063 .to_string()
5064 ),
5065 "Regular copying for reverse selection works the same",
5066 );
5067 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5068 assert_eq!(
5069 cx.read_from_clipboard()
5070 .and_then(|item| item.text().as_deref().map(str::to_string)),
5071 Some(
5072 "for selection in selections.iter() {
5073let mut start = selection.start;
5074let mut end = selection.end;
5075let is_entire_line = selection.is_empty();
5076if is_entire_line {
5077 start = Point::new(start.row, 0);"
5078 .to_string()
5079 ),
5080 "Copying with stripping for reverse selection works the same"
5081 );
5082
5083 cx.set_state(
5084 r#" for selection «in selections.iter() {
5085 let mut start = selection.start;
5086 let mut end = selection.end;
5087 let is_entire_line = selection.is_empty();
5088 if is_entire_line {
5089 start = Point::new(start.row, 0);ˇ»
5090 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5091 }
5092 "#,
5093 );
5094 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5095 assert_eq!(
5096 cx.read_from_clipboard()
5097 .and_then(|item| item.text().as_deref().map(str::to_string)),
5098 Some(
5099 "in selections.iter() {
5100 let mut start = selection.start;
5101 let mut end = selection.end;
5102 let is_entire_line = selection.is_empty();
5103 if is_entire_line {
5104 start = Point::new(start.row, 0);"
5105 .to_string()
5106 ),
5107 "When selecting past the indent, the copying works as usual",
5108 );
5109 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5110 assert_eq!(
5111 cx.read_from_clipboard()
5112 .and_then(|item| item.text().as_deref().map(str::to_string)),
5113 Some(
5114 "in selections.iter() {
5115 let mut start = selection.start;
5116 let mut end = selection.end;
5117 let is_entire_line = selection.is_empty();
5118 if is_entire_line {
5119 start = Point::new(start.row, 0);"
5120 .to_string()
5121 ),
5122 "When selecting past the indent, nothing is trimmed"
5123 );
5124
5125 cx.set_state(
5126 r#" «for selection in selections.iter() {
5127 let mut start = selection.start;
5128
5129 let mut end = selection.end;
5130 let is_entire_line = selection.is_empty();
5131 if is_entire_line {
5132 start = Point::new(start.row, 0);
5133ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5134 }
5135 "#,
5136 );
5137 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5138 assert_eq!(
5139 cx.read_from_clipboard()
5140 .and_then(|item| item.text().as_deref().map(str::to_string)),
5141 Some(
5142 "for selection in selections.iter() {
5143let mut start = selection.start;
5144
5145let mut end = selection.end;
5146let is_entire_line = selection.is_empty();
5147if is_entire_line {
5148 start = Point::new(start.row, 0);
5149"
5150 .to_string()
5151 ),
5152 "Copying with stripping should ignore empty lines"
5153 );
5154}
5155
5156#[gpui::test]
5157async fn test_paste_multiline(cx: &mut TestAppContext) {
5158 init_test(cx, |_| {});
5159
5160 let mut cx = EditorTestContext::new(cx).await;
5161 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5162
5163 // Cut an indented block, without the leading whitespace.
5164 cx.set_state(indoc! {"
5165 const a: B = (
5166 c(),
5167 «d(
5168 e,
5169 f
5170 )ˇ»
5171 );
5172 "});
5173 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5174 cx.assert_editor_state(indoc! {"
5175 const a: B = (
5176 c(),
5177 ˇ
5178 );
5179 "});
5180
5181 // Paste it at the same position.
5182 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5183 cx.assert_editor_state(indoc! {"
5184 const a: B = (
5185 c(),
5186 d(
5187 e,
5188 f
5189 )ˇ
5190 );
5191 "});
5192
5193 // Paste it at a line with a lower indent level.
5194 cx.set_state(indoc! {"
5195 ˇ
5196 const a: B = (
5197 c(),
5198 );
5199 "});
5200 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5201 cx.assert_editor_state(indoc! {"
5202 d(
5203 e,
5204 f
5205 )ˇ
5206 const a: B = (
5207 c(),
5208 );
5209 "});
5210
5211 // Cut an indented block, with the leading whitespace.
5212 cx.set_state(indoc! {"
5213 const a: B = (
5214 c(),
5215 « d(
5216 e,
5217 f
5218 )
5219 ˇ»);
5220 "});
5221 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5222 cx.assert_editor_state(indoc! {"
5223 const a: B = (
5224 c(),
5225 ˇ);
5226 "});
5227
5228 // Paste it at the same position.
5229 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5230 cx.assert_editor_state(indoc! {"
5231 const a: B = (
5232 c(),
5233 d(
5234 e,
5235 f
5236 )
5237 ˇ);
5238 "});
5239
5240 // Paste it at a line with a higher indent level.
5241 cx.set_state(indoc! {"
5242 const a: B = (
5243 c(),
5244 d(
5245 e,
5246 fˇ
5247 )
5248 );
5249 "});
5250 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5251 cx.assert_editor_state(indoc! {"
5252 const a: B = (
5253 c(),
5254 d(
5255 e,
5256 f d(
5257 e,
5258 f
5259 )
5260 ˇ
5261 )
5262 );
5263 "});
5264
5265 // Copy an indented block, starting mid-line
5266 cx.set_state(indoc! {"
5267 const a: B = (
5268 c(),
5269 somethin«g(
5270 e,
5271 f
5272 )ˇ»
5273 );
5274 "});
5275 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5276
5277 // Paste it on a line with a lower indent level
5278 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5279 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5280 cx.assert_editor_state(indoc! {"
5281 const a: B = (
5282 c(),
5283 something(
5284 e,
5285 f
5286 )
5287 );
5288 g(
5289 e,
5290 f
5291 )ˇ"});
5292}
5293
5294#[gpui::test]
5295async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5296 init_test(cx, |_| {});
5297
5298 cx.write_to_clipboard(ClipboardItem::new_string(
5299 " d(\n e\n );\n".into(),
5300 ));
5301
5302 let mut cx = EditorTestContext::new(cx).await;
5303 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5304
5305 cx.set_state(indoc! {"
5306 fn a() {
5307 b();
5308 if c() {
5309 ˇ
5310 }
5311 }
5312 "});
5313
5314 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5315 cx.assert_editor_state(indoc! {"
5316 fn a() {
5317 b();
5318 if c() {
5319 d(
5320 e
5321 );
5322 ˇ
5323 }
5324 }
5325 "});
5326
5327 cx.set_state(indoc! {"
5328 fn a() {
5329 b();
5330 ˇ
5331 }
5332 "});
5333
5334 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5335 cx.assert_editor_state(indoc! {"
5336 fn a() {
5337 b();
5338 d(
5339 e
5340 );
5341 ˇ
5342 }
5343 "});
5344}
5345
5346#[gpui::test]
5347fn test_select_all(cx: &mut TestAppContext) {
5348 init_test(cx, |_| {});
5349
5350 let editor = cx.add_window(|window, cx| {
5351 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5352 build_editor(buffer, window, cx)
5353 });
5354 _ = editor.update(cx, |editor, window, cx| {
5355 editor.select_all(&SelectAll, window, cx);
5356 assert_eq!(
5357 editor.selections.display_ranges(cx),
5358 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5359 );
5360 });
5361}
5362
5363#[gpui::test]
5364fn test_select_line(cx: &mut TestAppContext) {
5365 init_test(cx, |_| {});
5366
5367 let editor = cx.add_window(|window, cx| {
5368 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5369 build_editor(buffer, window, cx)
5370 });
5371 _ = editor.update(cx, |editor, window, cx| {
5372 editor.change_selections(None, window, cx, |s| {
5373 s.select_display_ranges([
5374 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5375 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5376 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5377 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5378 ])
5379 });
5380 editor.select_line(&SelectLine, window, cx);
5381 assert_eq!(
5382 editor.selections.display_ranges(cx),
5383 vec![
5384 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5385 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5386 ]
5387 );
5388 });
5389
5390 _ = editor.update(cx, |editor, window, cx| {
5391 editor.select_line(&SelectLine, window, cx);
5392 assert_eq!(
5393 editor.selections.display_ranges(cx),
5394 vec![
5395 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5396 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5397 ]
5398 );
5399 });
5400
5401 _ = editor.update(cx, |editor, window, cx| {
5402 editor.select_line(&SelectLine, window, cx);
5403 assert_eq!(
5404 editor.selections.display_ranges(cx),
5405 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5406 );
5407 });
5408}
5409
5410#[gpui::test]
5411async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5412 init_test(cx, |_| {});
5413 let mut cx = EditorTestContext::new(cx).await;
5414
5415 #[track_caller]
5416 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5417 cx.set_state(initial_state);
5418 cx.update_editor(|e, window, cx| {
5419 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5420 });
5421 cx.assert_editor_state(expected_state);
5422 }
5423
5424 // Selection starts and ends at the middle of lines, left-to-right
5425 test(
5426 &mut cx,
5427 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5428 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5429 );
5430 // Same thing, right-to-left
5431 test(
5432 &mut cx,
5433 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5434 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5435 );
5436
5437 // Whole buffer, left-to-right, last line *doesn't* end with newline
5438 test(
5439 &mut cx,
5440 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5441 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5442 );
5443 // Same thing, right-to-left
5444 test(
5445 &mut cx,
5446 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5447 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5448 );
5449
5450 // Whole buffer, left-to-right, last line ends with newline
5451 test(
5452 &mut cx,
5453 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5454 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5455 );
5456 // Same thing, right-to-left
5457 test(
5458 &mut cx,
5459 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5460 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5461 );
5462
5463 // Starts at the end of a line, ends at the start of another
5464 test(
5465 &mut cx,
5466 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5467 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5468 );
5469}
5470
5471#[gpui::test]
5472async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5473 init_test(cx, |_| {});
5474
5475 let editor = cx.add_window(|window, cx| {
5476 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5477 build_editor(buffer, window, cx)
5478 });
5479
5480 // setup
5481 _ = editor.update(cx, |editor, window, cx| {
5482 editor.fold_creases(
5483 vec![
5484 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5485 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5486 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5487 ],
5488 true,
5489 window,
5490 cx,
5491 );
5492 assert_eq!(
5493 editor.display_text(cx),
5494 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5495 );
5496 });
5497
5498 _ = editor.update(cx, |editor, window, cx| {
5499 editor.change_selections(None, window, cx, |s| {
5500 s.select_display_ranges([
5501 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5502 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5503 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5504 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5505 ])
5506 });
5507 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5508 assert_eq!(
5509 editor.display_text(cx),
5510 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5511 );
5512 });
5513 EditorTestContext::for_editor(editor, cx)
5514 .await
5515 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5516
5517 _ = editor.update(cx, |editor, window, cx| {
5518 editor.change_selections(None, window, cx, |s| {
5519 s.select_display_ranges([
5520 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5521 ])
5522 });
5523 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5524 assert_eq!(
5525 editor.display_text(cx),
5526 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5527 );
5528 assert_eq!(
5529 editor.selections.display_ranges(cx),
5530 [
5531 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5532 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5533 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5534 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5535 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5536 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5537 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5538 ]
5539 );
5540 });
5541 EditorTestContext::for_editor(editor, cx)
5542 .await
5543 .assert_editor_state(
5544 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5545 );
5546}
5547
5548#[gpui::test]
5549async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5550 init_test(cx, |_| {});
5551
5552 let mut cx = EditorTestContext::new(cx).await;
5553
5554 cx.set_state(indoc!(
5555 r#"abc
5556 defˇghi
5557
5558 jk
5559 nlmo
5560 "#
5561 ));
5562
5563 cx.update_editor(|editor, window, cx| {
5564 editor.add_selection_above(&Default::default(), window, cx);
5565 });
5566
5567 cx.assert_editor_state(indoc!(
5568 r#"abcˇ
5569 defˇghi
5570
5571 jk
5572 nlmo
5573 "#
5574 ));
5575
5576 cx.update_editor(|editor, window, cx| {
5577 editor.add_selection_above(&Default::default(), window, cx);
5578 });
5579
5580 cx.assert_editor_state(indoc!(
5581 r#"abcˇ
5582 defˇghi
5583
5584 jk
5585 nlmo
5586 "#
5587 ));
5588
5589 cx.update_editor(|editor, window, cx| {
5590 editor.add_selection_below(&Default::default(), window, cx);
5591 });
5592
5593 cx.assert_editor_state(indoc!(
5594 r#"abc
5595 defˇghi
5596
5597 jk
5598 nlmo
5599 "#
5600 ));
5601
5602 cx.update_editor(|editor, window, cx| {
5603 editor.undo_selection(&Default::default(), window, cx);
5604 });
5605
5606 cx.assert_editor_state(indoc!(
5607 r#"abcˇ
5608 defˇghi
5609
5610 jk
5611 nlmo
5612 "#
5613 ));
5614
5615 cx.update_editor(|editor, window, cx| {
5616 editor.redo_selection(&Default::default(), window, cx);
5617 });
5618
5619 cx.assert_editor_state(indoc!(
5620 r#"abc
5621 defˇghi
5622
5623 jk
5624 nlmo
5625 "#
5626 ));
5627
5628 cx.update_editor(|editor, window, cx| {
5629 editor.add_selection_below(&Default::default(), window, cx);
5630 });
5631
5632 cx.assert_editor_state(indoc!(
5633 r#"abc
5634 defˇghi
5635
5636 jk
5637 nlmˇo
5638 "#
5639 ));
5640
5641 cx.update_editor(|editor, window, cx| {
5642 editor.add_selection_below(&Default::default(), window, cx);
5643 });
5644
5645 cx.assert_editor_state(indoc!(
5646 r#"abc
5647 defˇghi
5648
5649 jk
5650 nlmˇo
5651 "#
5652 ));
5653
5654 // change selections
5655 cx.set_state(indoc!(
5656 r#"abc
5657 def«ˇg»hi
5658
5659 jk
5660 nlmo
5661 "#
5662 ));
5663
5664 cx.update_editor(|editor, window, cx| {
5665 editor.add_selection_below(&Default::default(), window, cx);
5666 });
5667
5668 cx.assert_editor_state(indoc!(
5669 r#"abc
5670 def«ˇg»hi
5671
5672 jk
5673 nlm«ˇo»
5674 "#
5675 ));
5676
5677 cx.update_editor(|editor, window, cx| {
5678 editor.add_selection_below(&Default::default(), window, cx);
5679 });
5680
5681 cx.assert_editor_state(indoc!(
5682 r#"abc
5683 def«ˇg»hi
5684
5685 jk
5686 nlm«ˇo»
5687 "#
5688 ));
5689
5690 cx.update_editor(|editor, window, cx| {
5691 editor.add_selection_above(&Default::default(), window, cx);
5692 });
5693
5694 cx.assert_editor_state(indoc!(
5695 r#"abc
5696 def«ˇg»hi
5697
5698 jk
5699 nlmo
5700 "#
5701 ));
5702
5703 cx.update_editor(|editor, window, cx| {
5704 editor.add_selection_above(&Default::default(), window, cx);
5705 });
5706
5707 cx.assert_editor_state(indoc!(
5708 r#"abc
5709 def«ˇg»hi
5710
5711 jk
5712 nlmo
5713 "#
5714 ));
5715
5716 // Change selections again
5717 cx.set_state(indoc!(
5718 r#"a«bc
5719 defgˇ»hi
5720
5721 jk
5722 nlmo
5723 "#
5724 ));
5725
5726 cx.update_editor(|editor, window, cx| {
5727 editor.add_selection_below(&Default::default(), window, cx);
5728 });
5729
5730 cx.assert_editor_state(indoc!(
5731 r#"a«bcˇ»
5732 d«efgˇ»hi
5733
5734 j«kˇ»
5735 nlmo
5736 "#
5737 ));
5738
5739 cx.update_editor(|editor, window, cx| {
5740 editor.add_selection_below(&Default::default(), window, cx);
5741 });
5742 cx.assert_editor_state(indoc!(
5743 r#"a«bcˇ»
5744 d«efgˇ»hi
5745
5746 j«kˇ»
5747 n«lmoˇ»
5748 "#
5749 ));
5750 cx.update_editor(|editor, window, cx| {
5751 editor.add_selection_above(&Default::default(), window, cx);
5752 });
5753
5754 cx.assert_editor_state(indoc!(
5755 r#"a«bcˇ»
5756 d«efgˇ»hi
5757
5758 j«kˇ»
5759 nlmo
5760 "#
5761 ));
5762
5763 // Change selections again
5764 cx.set_state(indoc!(
5765 r#"abc
5766 d«ˇefghi
5767
5768 jk
5769 nlm»o
5770 "#
5771 ));
5772
5773 cx.update_editor(|editor, window, cx| {
5774 editor.add_selection_above(&Default::default(), window, cx);
5775 });
5776
5777 cx.assert_editor_state(indoc!(
5778 r#"a«ˇbc»
5779 d«ˇef»ghi
5780
5781 j«ˇk»
5782 n«ˇlm»o
5783 "#
5784 ));
5785
5786 cx.update_editor(|editor, window, cx| {
5787 editor.add_selection_below(&Default::default(), window, cx);
5788 });
5789
5790 cx.assert_editor_state(indoc!(
5791 r#"abc
5792 d«ˇef»ghi
5793
5794 j«ˇk»
5795 n«ˇlm»o
5796 "#
5797 ));
5798}
5799
5800#[gpui::test]
5801async fn test_select_next(cx: &mut TestAppContext) {
5802 init_test(cx, |_| {});
5803
5804 let mut cx = EditorTestContext::new(cx).await;
5805 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5806
5807 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5808 .unwrap();
5809 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5810
5811 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5812 .unwrap();
5813 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5814
5815 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5816 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5817
5818 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5819 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5820
5821 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5822 .unwrap();
5823 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5824
5825 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5826 .unwrap();
5827 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5828}
5829
5830#[gpui::test]
5831async fn test_select_all_matches(cx: &mut TestAppContext) {
5832 init_test(cx, |_| {});
5833
5834 let mut cx = EditorTestContext::new(cx).await;
5835
5836 // Test caret-only selections
5837 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5838 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5839 .unwrap();
5840 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5841
5842 // Test left-to-right selections
5843 cx.set_state("abc\n«abcˇ»\nabc");
5844 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5845 .unwrap();
5846 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5847
5848 // Test right-to-left selections
5849 cx.set_state("abc\n«ˇabc»\nabc");
5850 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5851 .unwrap();
5852 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5853
5854 // Test selecting whitespace with caret selection
5855 cx.set_state("abc\nˇ abc\nabc");
5856 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5857 .unwrap();
5858 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5859
5860 // Test selecting whitespace with left-to-right selection
5861 cx.set_state("abc\n«ˇ »abc\nabc");
5862 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5863 .unwrap();
5864 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5865
5866 // Test no matches with right-to-left selection
5867 cx.set_state("abc\n« ˇ»abc\nabc");
5868 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5869 .unwrap();
5870 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5871}
5872
5873#[gpui::test]
5874async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
5875 init_test(cx, |_| {});
5876
5877 let mut cx = EditorTestContext::new(cx).await;
5878
5879 let large_body_1 = "\nd".repeat(200);
5880 let large_body_2 = "\ne".repeat(200);
5881
5882 cx.set_state(&format!(
5883 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
5884 ));
5885 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
5886 let scroll_position = editor.scroll_position(cx);
5887 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
5888 scroll_position
5889 });
5890
5891 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5892 .unwrap();
5893 cx.assert_editor_state(&format!(
5894 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
5895 ));
5896 let scroll_position_after_selection =
5897 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
5898 assert_eq!(
5899 initial_scroll_position, scroll_position_after_selection,
5900 "Scroll position should not change after selecting all matches"
5901 );
5902}
5903
5904#[gpui::test]
5905async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
5906 init_test(cx, |_| {});
5907
5908 let mut cx = EditorLspTestContext::new_rust(
5909 lsp::ServerCapabilities {
5910 document_formatting_provider: Some(lsp::OneOf::Left(true)),
5911 ..Default::default()
5912 },
5913 cx,
5914 )
5915 .await;
5916
5917 cx.set_state(indoc! {"
5918 line 1
5919 line 2
5920 linˇe 3
5921 line 4
5922 line 5
5923 "});
5924
5925 // Make an edit
5926 cx.update_editor(|editor, window, cx| {
5927 editor.handle_input("X", window, cx);
5928 });
5929
5930 // Move cursor to a different position
5931 cx.update_editor(|editor, window, cx| {
5932 editor.change_selections(None, window, cx, |s| {
5933 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
5934 });
5935 });
5936
5937 cx.assert_editor_state(indoc! {"
5938 line 1
5939 line 2
5940 linXe 3
5941 line 4
5942 liˇne 5
5943 "});
5944
5945 cx.lsp
5946 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
5947 Ok(Some(vec![lsp::TextEdit::new(
5948 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
5949 "PREFIX ".to_string(),
5950 )]))
5951 });
5952
5953 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
5954 .unwrap()
5955 .await
5956 .unwrap();
5957
5958 cx.assert_editor_state(indoc! {"
5959 PREFIX line 1
5960 line 2
5961 linXe 3
5962 line 4
5963 liˇne 5
5964 "});
5965
5966 // Undo formatting
5967 cx.update_editor(|editor, window, cx| {
5968 editor.undo(&Default::default(), window, cx);
5969 });
5970
5971 // Verify cursor moved back to position after edit
5972 cx.assert_editor_state(indoc! {"
5973 line 1
5974 line 2
5975 linXˇe 3
5976 line 4
5977 line 5
5978 "});
5979}
5980
5981#[gpui::test]
5982async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5983 init_test(cx, |_| {});
5984
5985 let mut cx = EditorTestContext::new(cx).await;
5986 cx.set_state(
5987 r#"let foo = 2;
5988lˇet foo = 2;
5989let fooˇ = 2;
5990let foo = 2;
5991let foo = ˇ2;"#,
5992 );
5993
5994 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5995 .unwrap();
5996 cx.assert_editor_state(
5997 r#"let foo = 2;
5998«letˇ» foo = 2;
5999let «fooˇ» = 2;
6000let foo = 2;
6001let foo = «2ˇ»;"#,
6002 );
6003
6004 // noop for multiple selections with different contents
6005 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6006 .unwrap();
6007 cx.assert_editor_state(
6008 r#"let foo = 2;
6009«letˇ» foo = 2;
6010let «fooˇ» = 2;
6011let foo = 2;
6012let foo = «2ˇ»;"#,
6013 );
6014}
6015
6016#[gpui::test]
6017async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6018 init_test(cx, |_| {});
6019
6020 let mut cx =
6021 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6022
6023 cx.assert_editor_state(indoc! {"
6024 ˇbbb
6025 ccc
6026
6027 bbb
6028 ccc
6029 "});
6030 cx.dispatch_action(SelectPrevious::default());
6031 cx.assert_editor_state(indoc! {"
6032 «bbbˇ»
6033 ccc
6034
6035 bbb
6036 ccc
6037 "});
6038 cx.dispatch_action(SelectPrevious::default());
6039 cx.assert_editor_state(indoc! {"
6040 «bbbˇ»
6041 ccc
6042
6043 «bbbˇ»
6044 ccc
6045 "});
6046}
6047
6048#[gpui::test]
6049async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6050 init_test(cx, |_| {});
6051
6052 let mut cx = EditorTestContext::new(cx).await;
6053 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6054
6055 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6056 .unwrap();
6057 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6058
6059 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6060 .unwrap();
6061 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6062
6063 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6064 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6065
6066 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6067 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6068
6069 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6070 .unwrap();
6071 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6072
6073 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6074 .unwrap();
6075 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
6076
6077 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6078 .unwrap();
6079 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
6080}
6081
6082#[gpui::test]
6083async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6084 init_test(cx, |_| {});
6085
6086 let mut cx = EditorTestContext::new(cx).await;
6087 cx.set_state("aˇ");
6088
6089 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6090 .unwrap();
6091 cx.assert_editor_state("«aˇ»");
6092 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6093 .unwrap();
6094 cx.assert_editor_state("«aˇ»");
6095}
6096
6097#[gpui::test]
6098async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6099 init_test(cx, |_| {});
6100
6101 let mut cx = EditorTestContext::new(cx).await;
6102 cx.set_state(
6103 r#"let foo = 2;
6104lˇet foo = 2;
6105let fooˇ = 2;
6106let foo = 2;
6107let foo = ˇ2;"#,
6108 );
6109
6110 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6111 .unwrap();
6112 cx.assert_editor_state(
6113 r#"let foo = 2;
6114«letˇ» foo = 2;
6115let «fooˇ» = 2;
6116let foo = 2;
6117let foo = «2ˇ»;"#,
6118 );
6119
6120 // noop for multiple selections with different contents
6121 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6122 .unwrap();
6123 cx.assert_editor_state(
6124 r#"let foo = 2;
6125«letˇ» foo = 2;
6126let «fooˇ» = 2;
6127let foo = 2;
6128let foo = «2ˇ»;"#,
6129 );
6130}
6131
6132#[gpui::test]
6133async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6134 init_test(cx, |_| {});
6135
6136 let mut cx = EditorTestContext::new(cx).await;
6137 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6138
6139 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6140 .unwrap();
6141 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
6142
6143 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6144 .unwrap();
6145 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
6146
6147 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6148 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
6149
6150 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6151 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
6152
6153 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6154 .unwrap();
6155 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
6156
6157 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6158 .unwrap();
6159 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
6160}
6161
6162#[gpui::test]
6163async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6164 init_test(cx, |_| {});
6165
6166 let language = Arc::new(Language::new(
6167 LanguageConfig::default(),
6168 Some(tree_sitter_rust::LANGUAGE.into()),
6169 ));
6170
6171 let text = r#"
6172 use mod1::mod2::{mod3, mod4};
6173
6174 fn fn_1(param1: bool, param2: &str) {
6175 let var1 = "text";
6176 }
6177 "#
6178 .unindent();
6179
6180 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6181 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6182 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6183
6184 editor
6185 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6186 .await;
6187
6188 editor.update_in(cx, |editor, window, cx| {
6189 editor.change_selections(None, window, cx, |s| {
6190 s.select_display_ranges([
6191 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6192 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6193 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6194 ]);
6195 });
6196 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6197 });
6198 editor.update(cx, |editor, cx| {
6199 assert_text_with_selections(
6200 editor,
6201 indoc! {r#"
6202 use mod1::mod2::{mod3, «mod4ˇ»};
6203
6204 fn fn_1«ˇ(param1: bool, param2: &str)» {
6205 let var1 = "«ˇtext»";
6206 }
6207 "#},
6208 cx,
6209 );
6210 });
6211
6212 editor.update_in(cx, |editor, window, cx| {
6213 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6214 });
6215 editor.update(cx, |editor, cx| {
6216 assert_text_with_selections(
6217 editor,
6218 indoc! {r#"
6219 use mod1::mod2::«{mod3, mod4}ˇ»;
6220
6221 «ˇfn fn_1(param1: bool, param2: &str) {
6222 let var1 = "text";
6223 }»
6224 "#},
6225 cx,
6226 );
6227 });
6228
6229 editor.update_in(cx, |editor, window, cx| {
6230 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6231 });
6232 assert_eq!(
6233 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6234 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6235 );
6236
6237 // Trying to expand the selected syntax node one more time has no effect.
6238 editor.update_in(cx, |editor, window, cx| {
6239 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6240 });
6241 assert_eq!(
6242 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6243 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6244 );
6245
6246 editor.update_in(cx, |editor, window, cx| {
6247 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6248 });
6249 editor.update(cx, |editor, cx| {
6250 assert_text_with_selections(
6251 editor,
6252 indoc! {r#"
6253 use mod1::mod2::«{mod3, mod4}ˇ»;
6254
6255 «ˇfn fn_1(param1: bool, param2: &str) {
6256 let var1 = "text";
6257 }»
6258 "#},
6259 cx,
6260 );
6261 });
6262
6263 editor.update_in(cx, |editor, window, cx| {
6264 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6265 });
6266 editor.update(cx, |editor, cx| {
6267 assert_text_with_selections(
6268 editor,
6269 indoc! {r#"
6270 use mod1::mod2::{mod3, «mod4ˇ»};
6271
6272 fn fn_1«ˇ(param1: bool, param2: &str)» {
6273 let var1 = "«ˇtext»";
6274 }
6275 "#},
6276 cx,
6277 );
6278 });
6279
6280 editor.update_in(cx, |editor, window, cx| {
6281 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6282 });
6283 editor.update(cx, |editor, cx| {
6284 assert_text_with_selections(
6285 editor,
6286 indoc! {r#"
6287 use mod1::mod2::{mod3, mo«ˇ»d4};
6288
6289 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6290 let var1 = "te«ˇ»xt";
6291 }
6292 "#},
6293 cx,
6294 );
6295 });
6296
6297 // Trying to shrink the selected syntax node one more time has no effect.
6298 editor.update_in(cx, |editor, window, cx| {
6299 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6300 });
6301 editor.update_in(cx, |editor, _, cx| {
6302 assert_text_with_selections(
6303 editor,
6304 indoc! {r#"
6305 use mod1::mod2::{mod3, mo«ˇ»d4};
6306
6307 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6308 let var1 = "te«ˇ»xt";
6309 }
6310 "#},
6311 cx,
6312 );
6313 });
6314
6315 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6316 // a fold.
6317 editor.update_in(cx, |editor, window, cx| {
6318 editor.fold_creases(
6319 vec![
6320 Crease::simple(
6321 Point::new(0, 21)..Point::new(0, 24),
6322 FoldPlaceholder::test(),
6323 ),
6324 Crease::simple(
6325 Point::new(3, 20)..Point::new(3, 22),
6326 FoldPlaceholder::test(),
6327 ),
6328 ],
6329 true,
6330 window,
6331 cx,
6332 );
6333 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6334 });
6335 editor.update(cx, |editor, cx| {
6336 assert_text_with_selections(
6337 editor,
6338 indoc! {r#"
6339 use mod1::mod2::«{mod3, mod4}ˇ»;
6340
6341 fn fn_1«ˇ(param1: bool, param2: &str)» {
6342 «ˇlet var1 = "text";»
6343 }
6344 "#},
6345 cx,
6346 );
6347 });
6348}
6349
6350#[gpui::test]
6351async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6352 init_test(cx, |_| {});
6353
6354 let base_text = r#"
6355 impl A {
6356 // this is an uncommitted comment
6357
6358 fn b() {
6359 c();
6360 }
6361
6362 // this is another uncommitted comment
6363
6364 fn d() {
6365 // e
6366 // f
6367 }
6368 }
6369
6370 fn g() {
6371 // h
6372 }
6373 "#
6374 .unindent();
6375
6376 let text = r#"
6377 ˇimpl A {
6378
6379 fn b() {
6380 c();
6381 }
6382
6383 fn d() {
6384 // e
6385 // f
6386 }
6387 }
6388
6389 fn g() {
6390 // h
6391 }
6392 "#
6393 .unindent();
6394
6395 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6396 cx.set_state(&text);
6397 cx.set_head_text(&base_text);
6398 cx.update_editor(|editor, window, cx| {
6399 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6400 });
6401
6402 cx.assert_state_with_diff(
6403 "
6404 ˇimpl A {
6405 - // this is an uncommitted comment
6406
6407 fn b() {
6408 c();
6409 }
6410
6411 - // this is another uncommitted comment
6412 -
6413 fn d() {
6414 // e
6415 // f
6416 }
6417 }
6418
6419 fn g() {
6420 // h
6421 }
6422 "
6423 .unindent(),
6424 );
6425
6426 let expected_display_text = "
6427 impl A {
6428 // this is an uncommitted comment
6429
6430 fn b() {
6431 ⋯
6432 }
6433
6434 // this is another uncommitted comment
6435
6436 fn d() {
6437 ⋯
6438 }
6439 }
6440
6441 fn g() {
6442 ⋯
6443 }
6444 "
6445 .unindent();
6446
6447 cx.update_editor(|editor, window, cx| {
6448 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6449 assert_eq!(editor.display_text(cx), expected_display_text);
6450 });
6451}
6452
6453#[gpui::test]
6454async fn test_autoindent(cx: &mut TestAppContext) {
6455 init_test(cx, |_| {});
6456
6457 let language = Arc::new(
6458 Language::new(
6459 LanguageConfig {
6460 brackets: BracketPairConfig {
6461 pairs: vec![
6462 BracketPair {
6463 start: "{".to_string(),
6464 end: "}".to_string(),
6465 close: false,
6466 surround: false,
6467 newline: true,
6468 },
6469 BracketPair {
6470 start: "(".to_string(),
6471 end: ")".to_string(),
6472 close: false,
6473 surround: false,
6474 newline: true,
6475 },
6476 ],
6477 ..Default::default()
6478 },
6479 ..Default::default()
6480 },
6481 Some(tree_sitter_rust::LANGUAGE.into()),
6482 )
6483 .with_indents_query(
6484 r#"
6485 (_ "(" ")" @end) @indent
6486 (_ "{" "}" @end) @indent
6487 "#,
6488 )
6489 .unwrap(),
6490 );
6491
6492 let text = "fn a() {}";
6493
6494 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6495 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6496 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6497 editor
6498 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6499 .await;
6500
6501 editor.update_in(cx, |editor, window, cx| {
6502 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6503 editor.newline(&Newline, window, cx);
6504 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6505 assert_eq!(
6506 editor.selections.ranges(cx),
6507 &[
6508 Point::new(1, 4)..Point::new(1, 4),
6509 Point::new(3, 4)..Point::new(3, 4),
6510 Point::new(5, 0)..Point::new(5, 0)
6511 ]
6512 );
6513 });
6514}
6515
6516#[gpui::test]
6517async fn test_autoindent_selections(cx: &mut TestAppContext) {
6518 init_test(cx, |_| {});
6519
6520 {
6521 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6522 cx.set_state(indoc! {"
6523 impl A {
6524
6525 fn b() {}
6526
6527 «fn c() {
6528
6529 }ˇ»
6530 }
6531 "});
6532
6533 cx.update_editor(|editor, window, cx| {
6534 editor.autoindent(&Default::default(), window, cx);
6535 });
6536
6537 cx.assert_editor_state(indoc! {"
6538 impl A {
6539
6540 fn b() {}
6541
6542 «fn c() {
6543
6544 }ˇ»
6545 }
6546 "});
6547 }
6548
6549 {
6550 let mut cx = EditorTestContext::new_multibuffer(
6551 cx,
6552 [indoc! { "
6553 impl A {
6554 «
6555 // a
6556 fn b(){}
6557 »
6558 «
6559 }
6560 fn c(){}
6561 »
6562 "}],
6563 );
6564
6565 let buffer = cx.update_editor(|editor, _, cx| {
6566 let buffer = editor.buffer().update(cx, |buffer, _| {
6567 buffer.all_buffers().iter().next().unwrap().clone()
6568 });
6569 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6570 buffer
6571 });
6572
6573 cx.run_until_parked();
6574 cx.update_editor(|editor, window, cx| {
6575 editor.select_all(&Default::default(), window, cx);
6576 editor.autoindent(&Default::default(), window, cx)
6577 });
6578 cx.run_until_parked();
6579
6580 cx.update(|_, cx| {
6581 assert_eq!(
6582 buffer.read(cx).text(),
6583 indoc! { "
6584 impl A {
6585
6586 // a
6587 fn b(){}
6588
6589
6590 }
6591 fn c(){}
6592
6593 " }
6594 )
6595 });
6596 }
6597}
6598
6599#[gpui::test]
6600async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6601 init_test(cx, |_| {});
6602
6603 let mut cx = EditorTestContext::new(cx).await;
6604
6605 let language = Arc::new(Language::new(
6606 LanguageConfig {
6607 brackets: BracketPairConfig {
6608 pairs: vec![
6609 BracketPair {
6610 start: "{".to_string(),
6611 end: "}".to_string(),
6612 close: true,
6613 surround: true,
6614 newline: true,
6615 },
6616 BracketPair {
6617 start: "(".to_string(),
6618 end: ")".to_string(),
6619 close: true,
6620 surround: true,
6621 newline: true,
6622 },
6623 BracketPair {
6624 start: "/*".to_string(),
6625 end: " */".to_string(),
6626 close: true,
6627 surround: true,
6628 newline: true,
6629 },
6630 BracketPair {
6631 start: "[".to_string(),
6632 end: "]".to_string(),
6633 close: false,
6634 surround: false,
6635 newline: true,
6636 },
6637 BracketPair {
6638 start: "\"".to_string(),
6639 end: "\"".to_string(),
6640 close: true,
6641 surround: true,
6642 newline: false,
6643 },
6644 BracketPair {
6645 start: "<".to_string(),
6646 end: ">".to_string(),
6647 close: false,
6648 surround: true,
6649 newline: true,
6650 },
6651 ],
6652 ..Default::default()
6653 },
6654 autoclose_before: "})]".to_string(),
6655 ..Default::default()
6656 },
6657 Some(tree_sitter_rust::LANGUAGE.into()),
6658 ));
6659
6660 cx.language_registry().add(language.clone());
6661 cx.update_buffer(|buffer, cx| {
6662 buffer.set_language(Some(language), cx);
6663 });
6664
6665 cx.set_state(
6666 &r#"
6667 🏀ˇ
6668 εˇ
6669 ❤️ˇ
6670 "#
6671 .unindent(),
6672 );
6673
6674 // autoclose multiple nested brackets at multiple cursors
6675 cx.update_editor(|editor, window, cx| {
6676 editor.handle_input("{", window, cx);
6677 editor.handle_input("{", window, cx);
6678 editor.handle_input("{", window, cx);
6679 });
6680 cx.assert_editor_state(
6681 &"
6682 🏀{{{ˇ}}}
6683 ε{{{ˇ}}}
6684 ❤️{{{ˇ}}}
6685 "
6686 .unindent(),
6687 );
6688
6689 // insert a different closing bracket
6690 cx.update_editor(|editor, window, cx| {
6691 editor.handle_input(")", window, cx);
6692 });
6693 cx.assert_editor_state(
6694 &"
6695 🏀{{{)ˇ}}}
6696 ε{{{)ˇ}}}
6697 ❤️{{{)ˇ}}}
6698 "
6699 .unindent(),
6700 );
6701
6702 // skip over the auto-closed brackets when typing a closing bracket
6703 cx.update_editor(|editor, window, cx| {
6704 editor.move_right(&MoveRight, window, cx);
6705 editor.handle_input("}", window, cx);
6706 editor.handle_input("}", window, cx);
6707 editor.handle_input("}", window, cx);
6708 });
6709 cx.assert_editor_state(
6710 &"
6711 🏀{{{)}}}}ˇ
6712 ε{{{)}}}}ˇ
6713 ❤️{{{)}}}}ˇ
6714 "
6715 .unindent(),
6716 );
6717
6718 // autoclose multi-character pairs
6719 cx.set_state(
6720 &"
6721 ˇ
6722 ˇ
6723 "
6724 .unindent(),
6725 );
6726 cx.update_editor(|editor, window, cx| {
6727 editor.handle_input("/", window, cx);
6728 editor.handle_input("*", window, cx);
6729 });
6730 cx.assert_editor_state(
6731 &"
6732 /*ˇ */
6733 /*ˇ */
6734 "
6735 .unindent(),
6736 );
6737
6738 // one cursor autocloses a multi-character pair, one cursor
6739 // does not autoclose.
6740 cx.set_state(
6741 &"
6742 /ˇ
6743 ˇ
6744 "
6745 .unindent(),
6746 );
6747 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6748 cx.assert_editor_state(
6749 &"
6750 /*ˇ */
6751 *ˇ
6752 "
6753 .unindent(),
6754 );
6755
6756 // Don't autoclose if the next character isn't whitespace and isn't
6757 // listed in the language's "autoclose_before" section.
6758 cx.set_state("ˇa b");
6759 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6760 cx.assert_editor_state("{ˇa b");
6761
6762 // Don't autoclose if `close` is false for the bracket pair
6763 cx.set_state("ˇ");
6764 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6765 cx.assert_editor_state("[ˇ");
6766
6767 // Surround with brackets if text is selected
6768 cx.set_state("«aˇ» b");
6769 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6770 cx.assert_editor_state("{«aˇ»} b");
6771
6772 // Autoclose when not immediately after a word character
6773 cx.set_state("a ˇ");
6774 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6775 cx.assert_editor_state("a \"ˇ\"");
6776
6777 // Autoclose pair where the start and end characters are the same
6778 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6779 cx.assert_editor_state("a \"\"ˇ");
6780
6781 // Don't autoclose when immediately after a word character
6782 cx.set_state("aˇ");
6783 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6784 cx.assert_editor_state("a\"ˇ");
6785
6786 // Do autoclose when after a non-word character
6787 cx.set_state("{ˇ");
6788 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6789 cx.assert_editor_state("{\"ˇ\"");
6790
6791 // Non identical pairs autoclose regardless of preceding character
6792 cx.set_state("aˇ");
6793 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6794 cx.assert_editor_state("a{ˇ}");
6795
6796 // Don't autoclose pair if autoclose is disabled
6797 cx.set_state("ˇ");
6798 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6799 cx.assert_editor_state("<ˇ");
6800
6801 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6802 cx.set_state("«aˇ» b");
6803 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6804 cx.assert_editor_state("<«aˇ»> b");
6805}
6806
6807#[gpui::test]
6808async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6809 init_test(cx, |settings| {
6810 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6811 });
6812
6813 let mut cx = EditorTestContext::new(cx).await;
6814
6815 let language = Arc::new(Language::new(
6816 LanguageConfig {
6817 brackets: BracketPairConfig {
6818 pairs: vec![
6819 BracketPair {
6820 start: "{".to_string(),
6821 end: "}".to_string(),
6822 close: true,
6823 surround: true,
6824 newline: true,
6825 },
6826 BracketPair {
6827 start: "(".to_string(),
6828 end: ")".to_string(),
6829 close: true,
6830 surround: true,
6831 newline: true,
6832 },
6833 BracketPair {
6834 start: "[".to_string(),
6835 end: "]".to_string(),
6836 close: false,
6837 surround: false,
6838 newline: true,
6839 },
6840 ],
6841 ..Default::default()
6842 },
6843 autoclose_before: "})]".to_string(),
6844 ..Default::default()
6845 },
6846 Some(tree_sitter_rust::LANGUAGE.into()),
6847 ));
6848
6849 cx.language_registry().add(language.clone());
6850 cx.update_buffer(|buffer, cx| {
6851 buffer.set_language(Some(language), cx);
6852 });
6853
6854 cx.set_state(
6855 &"
6856 ˇ
6857 ˇ
6858 ˇ
6859 "
6860 .unindent(),
6861 );
6862
6863 // ensure only matching closing brackets are skipped over
6864 cx.update_editor(|editor, window, cx| {
6865 editor.handle_input("}", window, cx);
6866 editor.move_left(&MoveLeft, window, cx);
6867 editor.handle_input(")", window, cx);
6868 editor.move_left(&MoveLeft, window, cx);
6869 });
6870 cx.assert_editor_state(
6871 &"
6872 ˇ)}
6873 ˇ)}
6874 ˇ)}
6875 "
6876 .unindent(),
6877 );
6878
6879 // skip-over closing brackets at multiple cursors
6880 cx.update_editor(|editor, window, cx| {
6881 editor.handle_input(")", window, cx);
6882 editor.handle_input("}", window, cx);
6883 });
6884 cx.assert_editor_state(
6885 &"
6886 )}ˇ
6887 )}ˇ
6888 )}ˇ
6889 "
6890 .unindent(),
6891 );
6892
6893 // ignore non-close brackets
6894 cx.update_editor(|editor, window, cx| {
6895 editor.handle_input("]", window, cx);
6896 editor.move_left(&MoveLeft, window, cx);
6897 editor.handle_input("]", window, cx);
6898 });
6899 cx.assert_editor_state(
6900 &"
6901 )}]ˇ]
6902 )}]ˇ]
6903 )}]ˇ]
6904 "
6905 .unindent(),
6906 );
6907}
6908
6909#[gpui::test]
6910async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6911 init_test(cx, |_| {});
6912
6913 let mut cx = EditorTestContext::new(cx).await;
6914
6915 let html_language = Arc::new(
6916 Language::new(
6917 LanguageConfig {
6918 name: "HTML".into(),
6919 brackets: BracketPairConfig {
6920 pairs: vec![
6921 BracketPair {
6922 start: "<".into(),
6923 end: ">".into(),
6924 close: true,
6925 ..Default::default()
6926 },
6927 BracketPair {
6928 start: "{".into(),
6929 end: "}".into(),
6930 close: true,
6931 ..Default::default()
6932 },
6933 BracketPair {
6934 start: "(".into(),
6935 end: ")".into(),
6936 close: true,
6937 ..Default::default()
6938 },
6939 ],
6940 ..Default::default()
6941 },
6942 autoclose_before: "})]>".into(),
6943 ..Default::default()
6944 },
6945 Some(tree_sitter_html::LANGUAGE.into()),
6946 )
6947 .with_injection_query(
6948 r#"
6949 (script_element
6950 (raw_text) @injection.content
6951 (#set! injection.language "javascript"))
6952 "#,
6953 )
6954 .unwrap(),
6955 );
6956
6957 let javascript_language = Arc::new(Language::new(
6958 LanguageConfig {
6959 name: "JavaScript".into(),
6960 brackets: BracketPairConfig {
6961 pairs: vec![
6962 BracketPair {
6963 start: "/*".into(),
6964 end: " */".into(),
6965 close: true,
6966 ..Default::default()
6967 },
6968 BracketPair {
6969 start: "{".into(),
6970 end: "}".into(),
6971 close: true,
6972 ..Default::default()
6973 },
6974 BracketPair {
6975 start: "(".into(),
6976 end: ")".into(),
6977 close: true,
6978 ..Default::default()
6979 },
6980 ],
6981 ..Default::default()
6982 },
6983 autoclose_before: "})]>".into(),
6984 ..Default::default()
6985 },
6986 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6987 ));
6988
6989 cx.language_registry().add(html_language.clone());
6990 cx.language_registry().add(javascript_language.clone());
6991
6992 cx.update_buffer(|buffer, cx| {
6993 buffer.set_language(Some(html_language), cx);
6994 });
6995
6996 cx.set_state(
6997 &r#"
6998 <body>ˇ
6999 <script>
7000 var x = 1;ˇ
7001 </script>
7002 </body>ˇ
7003 "#
7004 .unindent(),
7005 );
7006
7007 // Precondition: different languages are active at different locations.
7008 cx.update_editor(|editor, window, cx| {
7009 let snapshot = editor.snapshot(window, cx);
7010 let cursors = editor.selections.ranges::<usize>(cx);
7011 let languages = cursors
7012 .iter()
7013 .map(|c| snapshot.language_at(c.start).unwrap().name())
7014 .collect::<Vec<_>>();
7015 assert_eq!(
7016 languages,
7017 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7018 );
7019 });
7020
7021 // Angle brackets autoclose in HTML, but not JavaScript.
7022 cx.update_editor(|editor, window, cx| {
7023 editor.handle_input("<", window, cx);
7024 editor.handle_input("a", window, cx);
7025 });
7026 cx.assert_editor_state(
7027 &r#"
7028 <body><aˇ>
7029 <script>
7030 var x = 1;<aˇ
7031 </script>
7032 </body><aˇ>
7033 "#
7034 .unindent(),
7035 );
7036
7037 // Curly braces and parens autoclose in both HTML and JavaScript.
7038 cx.update_editor(|editor, window, cx| {
7039 editor.handle_input(" b=", window, cx);
7040 editor.handle_input("{", window, cx);
7041 editor.handle_input("c", window, cx);
7042 editor.handle_input("(", window, cx);
7043 });
7044 cx.assert_editor_state(
7045 &r#"
7046 <body><a b={c(ˇ)}>
7047 <script>
7048 var x = 1;<a b={c(ˇ)}
7049 </script>
7050 </body><a b={c(ˇ)}>
7051 "#
7052 .unindent(),
7053 );
7054
7055 // Brackets that were already autoclosed are skipped.
7056 cx.update_editor(|editor, window, cx| {
7057 editor.handle_input(")", window, cx);
7058 editor.handle_input("d", window, cx);
7059 editor.handle_input("}", window, cx);
7060 });
7061 cx.assert_editor_state(
7062 &r#"
7063 <body><a b={c()d}ˇ>
7064 <script>
7065 var x = 1;<a b={c()d}ˇ
7066 </script>
7067 </body><a b={c()d}ˇ>
7068 "#
7069 .unindent(),
7070 );
7071 cx.update_editor(|editor, window, cx| {
7072 editor.handle_input(">", window, cx);
7073 });
7074 cx.assert_editor_state(
7075 &r#"
7076 <body><a b={c()d}>ˇ
7077 <script>
7078 var x = 1;<a b={c()d}>ˇ
7079 </script>
7080 </body><a b={c()d}>ˇ
7081 "#
7082 .unindent(),
7083 );
7084
7085 // Reset
7086 cx.set_state(
7087 &r#"
7088 <body>ˇ
7089 <script>
7090 var x = 1;ˇ
7091 </script>
7092 </body>ˇ
7093 "#
7094 .unindent(),
7095 );
7096
7097 cx.update_editor(|editor, window, cx| {
7098 editor.handle_input("<", window, cx);
7099 });
7100 cx.assert_editor_state(
7101 &r#"
7102 <body><ˇ>
7103 <script>
7104 var x = 1;<ˇ
7105 </script>
7106 </body><ˇ>
7107 "#
7108 .unindent(),
7109 );
7110
7111 // When backspacing, the closing angle brackets are removed.
7112 cx.update_editor(|editor, window, cx| {
7113 editor.backspace(&Backspace, window, cx);
7114 });
7115 cx.assert_editor_state(
7116 &r#"
7117 <body>ˇ
7118 <script>
7119 var x = 1;ˇ
7120 </script>
7121 </body>ˇ
7122 "#
7123 .unindent(),
7124 );
7125
7126 // Block comments autoclose in JavaScript, but not HTML.
7127 cx.update_editor(|editor, window, cx| {
7128 editor.handle_input("/", window, cx);
7129 editor.handle_input("*", window, cx);
7130 });
7131 cx.assert_editor_state(
7132 &r#"
7133 <body>/*ˇ
7134 <script>
7135 var x = 1;/*ˇ */
7136 </script>
7137 </body>/*ˇ
7138 "#
7139 .unindent(),
7140 );
7141}
7142
7143#[gpui::test]
7144async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7145 init_test(cx, |_| {});
7146
7147 let mut cx = EditorTestContext::new(cx).await;
7148
7149 let rust_language = Arc::new(
7150 Language::new(
7151 LanguageConfig {
7152 name: "Rust".into(),
7153 brackets: serde_json::from_value(json!([
7154 { "start": "{", "end": "}", "close": true, "newline": true },
7155 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7156 ]))
7157 .unwrap(),
7158 autoclose_before: "})]>".into(),
7159 ..Default::default()
7160 },
7161 Some(tree_sitter_rust::LANGUAGE.into()),
7162 )
7163 .with_override_query("(string_literal) @string")
7164 .unwrap(),
7165 );
7166
7167 cx.language_registry().add(rust_language.clone());
7168 cx.update_buffer(|buffer, cx| {
7169 buffer.set_language(Some(rust_language), cx);
7170 });
7171
7172 cx.set_state(
7173 &r#"
7174 let x = ˇ
7175 "#
7176 .unindent(),
7177 );
7178
7179 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7180 cx.update_editor(|editor, window, cx| {
7181 editor.handle_input("\"", window, cx);
7182 });
7183 cx.assert_editor_state(
7184 &r#"
7185 let x = "ˇ"
7186 "#
7187 .unindent(),
7188 );
7189
7190 // Inserting another quotation mark. The cursor moves across the existing
7191 // automatically-inserted quotation mark.
7192 cx.update_editor(|editor, window, cx| {
7193 editor.handle_input("\"", window, cx);
7194 });
7195 cx.assert_editor_state(
7196 &r#"
7197 let x = ""ˇ
7198 "#
7199 .unindent(),
7200 );
7201
7202 // Reset
7203 cx.set_state(
7204 &r#"
7205 let x = ˇ
7206 "#
7207 .unindent(),
7208 );
7209
7210 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7211 cx.update_editor(|editor, window, cx| {
7212 editor.handle_input("\"", window, cx);
7213 editor.handle_input(" ", window, cx);
7214 editor.move_left(&Default::default(), window, cx);
7215 editor.handle_input("\\", window, cx);
7216 editor.handle_input("\"", window, cx);
7217 });
7218 cx.assert_editor_state(
7219 &r#"
7220 let x = "\"ˇ "
7221 "#
7222 .unindent(),
7223 );
7224
7225 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7226 // mark. Nothing is inserted.
7227 cx.update_editor(|editor, window, cx| {
7228 editor.move_right(&Default::default(), window, cx);
7229 editor.handle_input("\"", window, cx);
7230 });
7231 cx.assert_editor_state(
7232 &r#"
7233 let x = "\" "ˇ
7234 "#
7235 .unindent(),
7236 );
7237}
7238
7239#[gpui::test]
7240async fn test_surround_with_pair(cx: &mut TestAppContext) {
7241 init_test(cx, |_| {});
7242
7243 let language = Arc::new(Language::new(
7244 LanguageConfig {
7245 brackets: BracketPairConfig {
7246 pairs: vec![
7247 BracketPair {
7248 start: "{".to_string(),
7249 end: "}".to_string(),
7250 close: true,
7251 surround: true,
7252 newline: true,
7253 },
7254 BracketPair {
7255 start: "/* ".to_string(),
7256 end: "*/".to_string(),
7257 close: true,
7258 surround: true,
7259 ..Default::default()
7260 },
7261 ],
7262 ..Default::default()
7263 },
7264 ..Default::default()
7265 },
7266 Some(tree_sitter_rust::LANGUAGE.into()),
7267 ));
7268
7269 let text = r#"
7270 a
7271 b
7272 c
7273 "#
7274 .unindent();
7275
7276 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7277 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7278 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7279 editor
7280 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7281 .await;
7282
7283 editor.update_in(cx, |editor, window, cx| {
7284 editor.change_selections(None, window, cx, |s| {
7285 s.select_display_ranges([
7286 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7287 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7288 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7289 ])
7290 });
7291
7292 editor.handle_input("{", window, cx);
7293 editor.handle_input("{", window, cx);
7294 editor.handle_input("{", window, cx);
7295 assert_eq!(
7296 editor.text(cx),
7297 "
7298 {{{a}}}
7299 {{{b}}}
7300 {{{c}}}
7301 "
7302 .unindent()
7303 );
7304 assert_eq!(
7305 editor.selections.display_ranges(cx),
7306 [
7307 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7308 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7309 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7310 ]
7311 );
7312
7313 editor.undo(&Undo, window, cx);
7314 editor.undo(&Undo, window, cx);
7315 editor.undo(&Undo, window, cx);
7316 assert_eq!(
7317 editor.text(cx),
7318 "
7319 a
7320 b
7321 c
7322 "
7323 .unindent()
7324 );
7325 assert_eq!(
7326 editor.selections.display_ranges(cx),
7327 [
7328 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7329 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7330 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7331 ]
7332 );
7333
7334 // Ensure inserting the first character of a multi-byte bracket pair
7335 // doesn't surround the selections with the bracket.
7336 editor.handle_input("/", window, cx);
7337 assert_eq!(
7338 editor.text(cx),
7339 "
7340 /
7341 /
7342 /
7343 "
7344 .unindent()
7345 );
7346 assert_eq!(
7347 editor.selections.display_ranges(cx),
7348 [
7349 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7350 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7351 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7352 ]
7353 );
7354
7355 editor.undo(&Undo, window, cx);
7356 assert_eq!(
7357 editor.text(cx),
7358 "
7359 a
7360 b
7361 c
7362 "
7363 .unindent()
7364 );
7365 assert_eq!(
7366 editor.selections.display_ranges(cx),
7367 [
7368 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7369 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7370 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7371 ]
7372 );
7373
7374 // Ensure inserting the last character of a multi-byte bracket pair
7375 // doesn't surround the selections with the bracket.
7376 editor.handle_input("*", window, cx);
7377 assert_eq!(
7378 editor.text(cx),
7379 "
7380 *
7381 *
7382 *
7383 "
7384 .unindent()
7385 );
7386 assert_eq!(
7387 editor.selections.display_ranges(cx),
7388 [
7389 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7390 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7391 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7392 ]
7393 );
7394 });
7395}
7396
7397#[gpui::test]
7398async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7399 init_test(cx, |_| {});
7400
7401 let language = Arc::new(Language::new(
7402 LanguageConfig {
7403 brackets: BracketPairConfig {
7404 pairs: vec![BracketPair {
7405 start: "{".to_string(),
7406 end: "}".to_string(),
7407 close: true,
7408 surround: true,
7409 newline: true,
7410 }],
7411 ..Default::default()
7412 },
7413 autoclose_before: "}".to_string(),
7414 ..Default::default()
7415 },
7416 Some(tree_sitter_rust::LANGUAGE.into()),
7417 ));
7418
7419 let text = r#"
7420 a
7421 b
7422 c
7423 "#
7424 .unindent();
7425
7426 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7427 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7428 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7429 editor
7430 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7431 .await;
7432
7433 editor.update_in(cx, |editor, window, cx| {
7434 editor.change_selections(None, window, cx, |s| {
7435 s.select_ranges([
7436 Point::new(0, 1)..Point::new(0, 1),
7437 Point::new(1, 1)..Point::new(1, 1),
7438 Point::new(2, 1)..Point::new(2, 1),
7439 ])
7440 });
7441
7442 editor.handle_input("{", window, cx);
7443 editor.handle_input("{", window, cx);
7444 editor.handle_input("_", window, cx);
7445 assert_eq!(
7446 editor.text(cx),
7447 "
7448 a{{_}}
7449 b{{_}}
7450 c{{_}}
7451 "
7452 .unindent()
7453 );
7454 assert_eq!(
7455 editor.selections.ranges::<Point>(cx),
7456 [
7457 Point::new(0, 4)..Point::new(0, 4),
7458 Point::new(1, 4)..Point::new(1, 4),
7459 Point::new(2, 4)..Point::new(2, 4)
7460 ]
7461 );
7462
7463 editor.backspace(&Default::default(), window, cx);
7464 editor.backspace(&Default::default(), window, cx);
7465 assert_eq!(
7466 editor.text(cx),
7467 "
7468 a{}
7469 b{}
7470 c{}
7471 "
7472 .unindent()
7473 );
7474 assert_eq!(
7475 editor.selections.ranges::<Point>(cx),
7476 [
7477 Point::new(0, 2)..Point::new(0, 2),
7478 Point::new(1, 2)..Point::new(1, 2),
7479 Point::new(2, 2)..Point::new(2, 2)
7480 ]
7481 );
7482
7483 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7484 assert_eq!(
7485 editor.text(cx),
7486 "
7487 a
7488 b
7489 c
7490 "
7491 .unindent()
7492 );
7493 assert_eq!(
7494 editor.selections.ranges::<Point>(cx),
7495 [
7496 Point::new(0, 1)..Point::new(0, 1),
7497 Point::new(1, 1)..Point::new(1, 1),
7498 Point::new(2, 1)..Point::new(2, 1)
7499 ]
7500 );
7501 });
7502}
7503
7504#[gpui::test]
7505async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7506 init_test(cx, |settings| {
7507 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7508 });
7509
7510 let mut cx = EditorTestContext::new(cx).await;
7511
7512 let language = Arc::new(Language::new(
7513 LanguageConfig {
7514 brackets: BracketPairConfig {
7515 pairs: vec![
7516 BracketPair {
7517 start: "{".to_string(),
7518 end: "}".to_string(),
7519 close: true,
7520 surround: true,
7521 newline: true,
7522 },
7523 BracketPair {
7524 start: "(".to_string(),
7525 end: ")".to_string(),
7526 close: true,
7527 surround: true,
7528 newline: true,
7529 },
7530 BracketPair {
7531 start: "[".to_string(),
7532 end: "]".to_string(),
7533 close: false,
7534 surround: true,
7535 newline: true,
7536 },
7537 ],
7538 ..Default::default()
7539 },
7540 autoclose_before: "})]".to_string(),
7541 ..Default::default()
7542 },
7543 Some(tree_sitter_rust::LANGUAGE.into()),
7544 ));
7545
7546 cx.language_registry().add(language.clone());
7547 cx.update_buffer(|buffer, cx| {
7548 buffer.set_language(Some(language), cx);
7549 });
7550
7551 cx.set_state(
7552 &"
7553 {(ˇ)}
7554 [[ˇ]]
7555 {(ˇ)}
7556 "
7557 .unindent(),
7558 );
7559
7560 cx.update_editor(|editor, window, cx| {
7561 editor.backspace(&Default::default(), window, cx);
7562 editor.backspace(&Default::default(), window, cx);
7563 });
7564
7565 cx.assert_editor_state(
7566 &"
7567 ˇ
7568 ˇ]]
7569 ˇ
7570 "
7571 .unindent(),
7572 );
7573
7574 cx.update_editor(|editor, window, cx| {
7575 editor.handle_input("{", window, cx);
7576 editor.handle_input("{", window, cx);
7577 editor.move_right(&MoveRight, window, cx);
7578 editor.move_right(&MoveRight, window, cx);
7579 editor.move_left(&MoveLeft, window, cx);
7580 editor.move_left(&MoveLeft, window, cx);
7581 editor.backspace(&Default::default(), window, cx);
7582 });
7583
7584 cx.assert_editor_state(
7585 &"
7586 {ˇ}
7587 {ˇ}]]
7588 {ˇ}
7589 "
7590 .unindent(),
7591 );
7592
7593 cx.update_editor(|editor, window, cx| {
7594 editor.backspace(&Default::default(), window, cx);
7595 });
7596
7597 cx.assert_editor_state(
7598 &"
7599 ˇ
7600 ˇ]]
7601 ˇ
7602 "
7603 .unindent(),
7604 );
7605}
7606
7607#[gpui::test]
7608async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7609 init_test(cx, |_| {});
7610
7611 let language = Arc::new(Language::new(
7612 LanguageConfig::default(),
7613 Some(tree_sitter_rust::LANGUAGE.into()),
7614 ));
7615
7616 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7617 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7618 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7619 editor
7620 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7621 .await;
7622
7623 editor.update_in(cx, |editor, window, cx| {
7624 editor.set_auto_replace_emoji_shortcode(true);
7625
7626 editor.handle_input("Hello ", window, cx);
7627 editor.handle_input(":wave", window, cx);
7628 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7629
7630 editor.handle_input(":", window, cx);
7631 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7632
7633 editor.handle_input(" :smile", window, cx);
7634 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7635
7636 editor.handle_input(":", window, cx);
7637 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7638
7639 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7640 editor.handle_input(":wave", window, cx);
7641 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7642
7643 editor.handle_input(":", window, cx);
7644 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7645
7646 editor.handle_input(":1", window, cx);
7647 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7648
7649 editor.handle_input(":", window, cx);
7650 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7651
7652 // Ensure shortcode does not get replaced when it is part of a word
7653 editor.handle_input(" Test:wave", window, cx);
7654 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7655
7656 editor.handle_input(":", window, cx);
7657 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7658
7659 editor.set_auto_replace_emoji_shortcode(false);
7660
7661 // Ensure shortcode does not get replaced when auto replace is off
7662 editor.handle_input(" :wave", window, cx);
7663 assert_eq!(
7664 editor.text(cx),
7665 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7666 );
7667
7668 editor.handle_input(":", window, cx);
7669 assert_eq!(
7670 editor.text(cx),
7671 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7672 );
7673 });
7674}
7675
7676#[gpui::test]
7677async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7678 init_test(cx, |_| {});
7679
7680 let (text, insertion_ranges) = marked_text_ranges(
7681 indoc! {"
7682 ˇ
7683 "},
7684 false,
7685 );
7686
7687 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7688 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7689
7690 _ = editor.update_in(cx, |editor, window, cx| {
7691 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7692
7693 editor
7694 .insert_snippet(&insertion_ranges, snippet, window, cx)
7695 .unwrap();
7696
7697 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7698 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7699 assert_eq!(editor.text(cx), expected_text);
7700 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7701 }
7702
7703 assert(
7704 editor,
7705 cx,
7706 indoc! {"
7707 type «» =•
7708 "},
7709 );
7710
7711 assert!(editor.context_menu_visible(), "There should be a matches");
7712 });
7713}
7714
7715#[gpui::test]
7716async fn test_snippets(cx: &mut TestAppContext) {
7717 init_test(cx, |_| {});
7718
7719 let (text, insertion_ranges) = marked_text_ranges(
7720 indoc! {"
7721 a.ˇ b
7722 a.ˇ b
7723 a.ˇ b
7724 "},
7725 false,
7726 );
7727
7728 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7729 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7730
7731 editor.update_in(cx, |editor, window, cx| {
7732 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7733
7734 editor
7735 .insert_snippet(&insertion_ranges, snippet, window, cx)
7736 .unwrap();
7737
7738 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7739 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7740 assert_eq!(editor.text(cx), expected_text);
7741 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7742 }
7743
7744 assert(
7745 editor,
7746 cx,
7747 indoc! {"
7748 a.f(«one», two, «three») b
7749 a.f(«one», two, «three») b
7750 a.f(«one», two, «three») b
7751 "},
7752 );
7753
7754 // Can't move earlier than the first tab stop
7755 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7756 assert(
7757 editor,
7758 cx,
7759 indoc! {"
7760 a.f(«one», two, «three») b
7761 a.f(«one», two, «three») b
7762 a.f(«one», two, «three») b
7763 "},
7764 );
7765
7766 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7767 assert(
7768 editor,
7769 cx,
7770 indoc! {"
7771 a.f(one, «two», three) b
7772 a.f(one, «two», three) b
7773 a.f(one, «two», three) b
7774 "},
7775 );
7776
7777 editor.move_to_prev_snippet_tabstop(window, cx);
7778 assert(
7779 editor,
7780 cx,
7781 indoc! {"
7782 a.f(«one», two, «three») b
7783 a.f(«one», two, «three») b
7784 a.f(«one», two, «three») b
7785 "},
7786 );
7787
7788 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7789 assert(
7790 editor,
7791 cx,
7792 indoc! {"
7793 a.f(one, «two», three) b
7794 a.f(one, «two», three) b
7795 a.f(one, «two», three) b
7796 "},
7797 );
7798 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7799 assert(
7800 editor,
7801 cx,
7802 indoc! {"
7803 a.f(one, two, three)ˇ b
7804 a.f(one, two, three)ˇ b
7805 a.f(one, two, three)ˇ b
7806 "},
7807 );
7808
7809 // As soon as the last tab stop is reached, snippet state is gone
7810 editor.move_to_prev_snippet_tabstop(window, cx);
7811 assert(
7812 editor,
7813 cx,
7814 indoc! {"
7815 a.f(one, two, three)ˇ b
7816 a.f(one, two, three)ˇ b
7817 a.f(one, two, three)ˇ b
7818 "},
7819 );
7820 });
7821}
7822
7823#[gpui::test]
7824async fn test_document_format_during_save(cx: &mut TestAppContext) {
7825 init_test(cx, |_| {});
7826
7827 let fs = FakeFs::new(cx.executor());
7828 fs.insert_file(path!("/file.rs"), Default::default()).await;
7829
7830 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7831
7832 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7833 language_registry.add(rust_lang());
7834 let mut fake_servers = language_registry.register_fake_lsp(
7835 "Rust",
7836 FakeLspAdapter {
7837 capabilities: lsp::ServerCapabilities {
7838 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7839 ..Default::default()
7840 },
7841 ..Default::default()
7842 },
7843 );
7844
7845 let buffer = project
7846 .update(cx, |project, cx| {
7847 project.open_local_buffer(path!("/file.rs"), cx)
7848 })
7849 .await
7850 .unwrap();
7851
7852 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7853 let (editor, cx) = cx.add_window_view(|window, cx| {
7854 build_editor_with_project(project.clone(), buffer, window, cx)
7855 });
7856 editor.update_in(cx, |editor, window, cx| {
7857 editor.set_text("one\ntwo\nthree\n", window, cx)
7858 });
7859 assert!(cx.read(|cx| editor.is_dirty(cx)));
7860
7861 cx.executor().start_waiting();
7862 let fake_server = fake_servers.next().await.unwrap();
7863
7864 {
7865 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7866 move |params, _| async move {
7867 assert_eq!(
7868 params.text_document.uri,
7869 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7870 );
7871 assert_eq!(params.options.tab_size, 4);
7872 Ok(Some(vec![lsp::TextEdit::new(
7873 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7874 ", ".to_string(),
7875 )]))
7876 },
7877 );
7878 let save = editor
7879 .update_in(cx, |editor, window, cx| {
7880 editor.save(true, project.clone(), window, cx)
7881 })
7882 .unwrap();
7883 cx.executor().start_waiting();
7884 save.await;
7885
7886 assert_eq!(
7887 editor.update(cx, |editor, cx| editor.text(cx)),
7888 "one, two\nthree\n"
7889 );
7890 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7891 }
7892
7893 {
7894 editor.update_in(cx, |editor, window, cx| {
7895 editor.set_text("one\ntwo\nthree\n", window, cx)
7896 });
7897 assert!(cx.read(|cx| editor.is_dirty(cx)));
7898
7899 // Ensure we can still save even if formatting hangs.
7900 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7901 move |params, _| async move {
7902 assert_eq!(
7903 params.text_document.uri,
7904 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7905 );
7906 futures::future::pending::<()>().await;
7907 unreachable!()
7908 },
7909 );
7910 let save = editor
7911 .update_in(cx, |editor, window, cx| {
7912 editor.save(true, project.clone(), window, cx)
7913 })
7914 .unwrap();
7915 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7916 cx.executor().start_waiting();
7917 save.await;
7918 assert_eq!(
7919 editor.update(cx, |editor, cx| editor.text(cx)),
7920 "one\ntwo\nthree\n"
7921 );
7922 }
7923
7924 // For non-dirty buffer, no formatting request should be sent
7925 {
7926 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7927
7928 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
7929 panic!("Should not be invoked on non-dirty buffer");
7930 });
7931 let save = editor
7932 .update_in(cx, |editor, window, cx| {
7933 editor.save(true, project.clone(), window, cx)
7934 })
7935 .unwrap();
7936 cx.executor().start_waiting();
7937 save.await;
7938 }
7939
7940 // Set rust language override and assert overridden tabsize is sent to language server
7941 update_test_language_settings(cx, |settings| {
7942 settings.languages.insert(
7943 "Rust".into(),
7944 LanguageSettingsContent {
7945 tab_size: NonZeroU32::new(8),
7946 ..Default::default()
7947 },
7948 );
7949 });
7950
7951 {
7952 editor.update_in(cx, |editor, window, cx| {
7953 editor.set_text("somehting_new\n", window, cx)
7954 });
7955 assert!(cx.read(|cx| editor.is_dirty(cx)));
7956 let _formatting_request_signal = fake_server
7957 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7958 assert_eq!(
7959 params.text_document.uri,
7960 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7961 );
7962 assert_eq!(params.options.tab_size, 8);
7963 Ok(Some(vec![]))
7964 });
7965 let save = editor
7966 .update_in(cx, |editor, window, cx| {
7967 editor.save(true, project.clone(), window, cx)
7968 })
7969 .unwrap();
7970 cx.executor().start_waiting();
7971 save.await;
7972 }
7973}
7974
7975#[gpui::test]
7976async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7977 init_test(cx, |_| {});
7978
7979 let cols = 4;
7980 let rows = 10;
7981 let sample_text_1 = sample_text(rows, cols, 'a');
7982 assert_eq!(
7983 sample_text_1,
7984 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7985 );
7986 let sample_text_2 = sample_text(rows, cols, 'l');
7987 assert_eq!(
7988 sample_text_2,
7989 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7990 );
7991 let sample_text_3 = sample_text(rows, cols, 'v');
7992 assert_eq!(
7993 sample_text_3,
7994 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7995 );
7996
7997 let fs = FakeFs::new(cx.executor());
7998 fs.insert_tree(
7999 path!("/a"),
8000 json!({
8001 "main.rs": sample_text_1,
8002 "other.rs": sample_text_2,
8003 "lib.rs": sample_text_3,
8004 }),
8005 )
8006 .await;
8007
8008 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8009 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8010 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8011
8012 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8013 language_registry.add(rust_lang());
8014 let mut fake_servers = language_registry.register_fake_lsp(
8015 "Rust",
8016 FakeLspAdapter {
8017 capabilities: lsp::ServerCapabilities {
8018 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8019 ..Default::default()
8020 },
8021 ..Default::default()
8022 },
8023 );
8024
8025 let worktree = project.update(cx, |project, cx| {
8026 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8027 assert_eq!(worktrees.len(), 1);
8028 worktrees.pop().unwrap()
8029 });
8030 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8031
8032 let buffer_1 = project
8033 .update(cx, |project, cx| {
8034 project.open_buffer((worktree_id, "main.rs"), cx)
8035 })
8036 .await
8037 .unwrap();
8038 let buffer_2 = project
8039 .update(cx, |project, cx| {
8040 project.open_buffer((worktree_id, "other.rs"), cx)
8041 })
8042 .await
8043 .unwrap();
8044 let buffer_3 = project
8045 .update(cx, |project, cx| {
8046 project.open_buffer((worktree_id, "lib.rs"), cx)
8047 })
8048 .await
8049 .unwrap();
8050
8051 let multi_buffer = cx.new(|cx| {
8052 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8053 multi_buffer.push_excerpts(
8054 buffer_1.clone(),
8055 [
8056 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8057 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8058 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8059 ],
8060 cx,
8061 );
8062 multi_buffer.push_excerpts(
8063 buffer_2.clone(),
8064 [
8065 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8066 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8067 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8068 ],
8069 cx,
8070 );
8071 multi_buffer.push_excerpts(
8072 buffer_3.clone(),
8073 [
8074 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8075 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8076 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8077 ],
8078 cx,
8079 );
8080 multi_buffer
8081 });
8082 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8083 Editor::new(
8084 EditorMode::full(),
8085 multi_buffer,
8086 Some(project.clone()),
8087 window,
8088 cx,
8089 )
8090 });
8091
8092 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8093 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8094 s.select_ranges(Some(1..2))
8095 });
8096 editor.insert("|one|two|three|", window, cx);
8097 });
8098 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8099 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8100 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8101 s.select_ranges(Some(60..70))
8102 });
8103 editor.insert("|four|five|six|", window, cx);
8104 });
8105 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8106
8107 // First two buffers should be edited, but not the third one.
8108 assert_eq!(
8109 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8110 "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}",
8111 );
8112 buffer_1.update(cx, |buffer, _| {
8113 assert!(buffer.is_dirty());
8114 assert_eq!(
8115 buffer.text(),
8116 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8117 )
8118 });
8119 buffer_2.update(cx, |buffer, _| {
8120 assert!(buffer.is_dirty());
8121 assert_eq!(
8122 buffer.text(),
8123 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8124 )
8125 });
8126 buffer_3.update(cx, |buffer, _| {
8127 assert!(!buffer.is_dirty());
8128 assert_eq!(buffer.text(), sample_text_3,)
8129 });
8130 cx.executor().run_until_parked();
8131
8132 cx.executor().start_waiting();
8133 let save = multi_buffer_editor
8134 .update_in(cx, |editor, window, cx| {
8135 editor.save(true, project.clone(), window, cx)
8136 })
8137 .unwrap();
8138
8139 let fake_server = fake_servers.next().await.unwrap();
8140 fake_server
8141 .server
8142 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8143 Ok(Some(vec![lsp::TextEdit::new(
8144 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8145 format!("[{} formatted]", params.text_document.uri),
8146 )]))
8147 })
8148 .detach();
8149 save.await;
8150
8151 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8152 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8153 assert_eq!(
8154 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8155 uri!(
8156 "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}"
8157 ),
8158 );
8159 buffer_1.update(cx, |buffer, _| {
8160 assert!(!buffer.is_dirty());
8161 assert_eq!(
8162 buffer.text(),
8163 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8164 )
8165 });
8166 buffer_2.update(cx, |buffer, _| {
8167 assert!(!buffer.is_dirty());
8168 assert_eq!(
8169 buffer.text(),
8170 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8171 )
8172 });
8173 buffer_3.update(cx, |buffer, _| {
8174 assert!(!buffer.is_dirty());
8175 assert_eq!(buffer.text(), sample_text_3,)
8176 });
8177}
8178
8179#[gpui::test]
8180async fn test_range_format_during_save(cx: &mut TestAppContext) {
8181 init_test(cx, |_| {});
8182
8183 let fs = FakeFs::new(cx.executor());
8184 fs.insert_file(path!("/file.rs"), Default::default()).await;
8185
8186 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8187
8188 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8189 language_registry.add(rust_lang());
8190 let mut fake_servers = language_registry.register_fake_lsp(
8191 "Rust",
8192 FakeLspAdapter {
8193 capabilities: lsp::ServerCapabilities {
8194 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8195 ..Default::default()
8196 },
8197 ..Default::default()
8198 },
8199 );
8200
8201 let buffer = project
8202 .update(cx, |project, cx| {
8203 project.open_local_buffer(path!("/file.rs"), cx)
8204 })
8205 .await
8206 .unwrap();
8207
8208 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8209 let (editor, cx) = cx.add_window_view(|window, cx| {
8210 build_editor_with_project(project.clone(), buffer, window, cx)
8211 });
8212 editor.update_in(cx, |editor, window, cx| {
8213 editor.set_text("one\ntwo\nthree\n", window, cx)
8214 });
8215 assert!(cx.read(|cx| editor.is_dirty(cx)));
8216
8217 cx.executor().start_waiting();
8218 let fake_server = fake_servers.next().await.unwrap();
8219
8220 let save = editor
8221 .update_in(cx, |editor, window, cx| {
8222 editor.save(true, project.clone(), window, cx)
8223 })
8224 .unwrap();
8225 fake_server
8226 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8227 assert_eq!(
8228 params.text_document.uri,
8229 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8230 );
8231 assert_eq!(params.options.tab_size, 4);
8232 Ok(Some(vec![lsp::TextEdit::new(
8233 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8234 ", ".to_string(),
8235 )]))
8236 })
8237 .next()
8238 .await;
8239 cx.executor().start_waiting();
8240 save.await;
8241 assert_eq!(
8242 editor.update(cx, |editor, cx| editor.text(cx)),
8243 "one, two\nthree\n"
8244 );
8245 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8246
8247 editor.update_in(cx, |editor, window, cx| {
8248 editor.set_text("one\ntwo\nthree\n", window, cx)
8249 });
8250 assert!(cx.read(|cx| editor.is_dirty(cx)));
8251
8252 // Ensure we can still save even if formatting hangs.
8253 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8254 move |params, _| async move {
8255 assert_eq!(
8256 params.text_document.uri,
8257 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8258 );
8259 futures::future::pending::<()>().await;
8260 unreachable!()
8261 },
8262 );
8263 let save = editor
8264 .update_in(cx, |editor, window, cx| {
8265 editor.save(true, project.clone(), window, cx)
8266 })
8267 .unwrap();
8268 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8269 cx.executor().start_waiting();
8270 save.await;
8271 assert_eq!(
8272 editor.update(cx, |editor, cx| editor.text(cx)),
8273 "one\ntwo\nthree\n"
8274 );
8275 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8276
8277 // For non-dirty buffer, no formatting request should be sent
8278 let save = editor
8279 .update_in(cx, |editor, window, cx| {
8280 editor.save(true, project.clone(), window, cx)
8281 })
8282 .unwrap();
8283 let _pending_format_request = fake_server
8284 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8285 panic!("Should not be invoked on non-dirty buffer");
8286 })
8287 .next();
8288 cx.executor().start_waiting();
8289 save.await;
8290
8291 // Set Rust language override and assert overridden tabsize is sent to language server
8292 update_test_language_settings(cx, |settings| {
8293 settings.languages.insert(
8294 "Rust".into(),
8295 LanguageSettingsContent {
8296 tab_size: NonZeroU32::new(8),
8297 ..Default::default()
8298 },
8299 );
8300 });
8301
8302 editor.update_in(cx, |editor, window, cx| {
8303 editor.set_text("somehting_new\n", window, cx)
8304 });
8305 assert!(cx.read(|cx| editor.is_dirty(cx)));
8306 let save = editor
8307 .update_in(cx, |editor, window, cx| {
8308 editor.save(true, project.clone(), window, cx)
8309 })
8310 .unwrap();
8311 fake_server
8312 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8313 assert_eq!(
8314 params.text_document.uri,
8315 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8316 );
8317 assert_eq!(params.options.tab_size, 8);
8318 Ok(Some(vec![]))
8319 })
8320 .next()
8321 .await;
8322 cx.executor().start_waiting();
8323 save.await;
8324}
8325
8326#[gpui::test]
8327async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8328 init_test(cx, |settings| {
8329 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8330 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8331 ))
8332 });
8333
8334 let fs = FakeFs::new(cx.executor());
8335 fs.insert_file(path!("/file.rs"), Default::default()).await;
8336
8337 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8338
8339 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8340 language_registry.add(Arc::new(Language::new(
8341 LanguageConfig {
8342 name: "Rust".into(),
8343 matcher: LanguageMatcher {
8344 path_suffixes: vec!["rs".to_string()],
8345 ..Default::default()
8346 },
8347 ..LanguageConfig::default()
8348 },
8349 Some(tree_sitter_rust::LANGUAGE.into()),
8350 )));
8351 update_test_language_settings(cx, |settings| {
8352 // Enable Prettier formatting for the same buffer, and ensure
8353 // LSP is called instead of Prettier.
8354 settings.defaults.prettier = Some(PrettierSettings {
8355 allowed: true,
8356 ..PrettierSettings::default()
8357 });
8358 });
8359 let mut fake_servers = language_registry.register_fake_lsp(
8360 "Rust",
8361 FakeLspAdapter {
8362 capabilities: lsp::ServerCapabilities {
8363 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8364 ..Default::default()
8365 },
8366 ..Default::default()
8367 },
8368 );
8369
8370 let buffer = project
8371 .update(cx, |project, cx| {
8372 project.open_local_buffer(path!("/file.rs"), cx)
8373 })
8374 .await
8375 .unwrap();
8376
8377 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8378 let (editor, cx) = cx.add_window_view(|window, cx| {
8379 build_editor_with_project(project.clone(), buffer, window, cx)
8380 });
8381 editor.update_in(cx, |editor, window, cx| {
8382 editor.set_text("one\ntwo\nthree\n", window, cx)
8383 });
8384
8385 cx.executor().start_waiting();
8386 let fake_server = fake_servers.next().await.unwrap();
8387
8388 let format = editor
8389 .update_in(cx, |editor, window, cx| {
8390 editor.perform_format(
8391 project.clone(),
8392 FormatTrigger::Manual,
8393 FormatTarget::Buffers,
8394 window,
8395 cx,
8396 )
8397 })
8398 .unwrap();
8399 fake_server
8400 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8401 assert_eq!(
8402 params.text_document.uri,
8403 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8404 );
8405 assert_eq!(params.options.tab_size, 4);
8406 Ok(Some(vec![lsp::TextEdit::new(
8407 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8408 ", ".to_string(),
8409 )]))
8410 })
8411 .next()
8412 .await;
8413 cx.executor().start_waiting();
8414 format.await;
8415 assert_eq!(
8416 editor.update(cx, |editor, cx| editor.text(cx)),
8417 "one, two\nthree\n"
8418 );
8419
8420 editor.update_in(cx, |editor, window, cx| {
8421 editor.set_text("one\ntwo\nthree\n", window, cx)
8422 });
8423 // Ensure we don't lock if formatting hangs.
8424 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8425 move |params, _| async move {
8426 assert_eq!(
8427 params.text_document.uri,
8428 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8429 );
8430 futures::future::pending::<()>().await;
8431 unreachable!()
8432 },
8433 );
8434 let format = editor
8435 .update_in(cx, |editor, window, cx| {
8436 editor.perform_format(
8437 project,
8438 FormatTrigger::Manual,
8439 FormatTarget::Buffers,
8440 window,
8441 cx,
8442 )
8443 })
8444 .unwrap();
8445 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8446 cx.executor().start_waiting();
8447 format.await;
8448 assert_eq!(
8449 editor.update(cx, |editor, cx| editor.text(cx)),
8450 "one\ntwo\nthree\n"
8451 );
8452}
8453
8454#[gpui::test]
8455async fn test_multiple_formatters(cx: &mut TestAppContext) {
8456 init_test(cx, |settings| {
8457 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
8458 settings.defaults.formatter =
8459 Some(language_settings::SelectedFormatter::List(FormatterList(
8460 vec![
8461 Formatter::LanguageServer { name: None },
8462 Formatter::CodeActions(
8463 [
8464 ("code-action-1".into(), true),
8465 ("code-action-2".into(), true),
8466 ]
8467 .into_iter()
8468 .collect(),
8469 ),
8470 ]
8471 .into(),
8472 )))
8473 });
8474
8475 let fs = FakeFs::new(cx.executor());
8476 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
8477 .await;
8478
8479 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8480 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8481 language_registry.add(rust_lang());
8482
8483 let mut fake_servers = language_registry.register_fake_lsp(
8484 "Rust",
8485 FakeLspAdapter {
8486 capabilities: lsp::ServerCapabilities {
8487 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8488 execute_command_provider: Some(lsp::ExecuteCommandOptions {
8489 commands: vec!["the-command-for-code-action-1".into()],
8490 ..Default::default()
8491 }),
8492 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8493 ..Default::default()
8494 },
8495 ..Default::default()
8496 },
8497 );
8498
8499 let buffer = project
8500 .update(cx, |project, cx| {
8501 project.open_local_buffer(path!("/file.rs"), cx)
8502 })
8503 .await
8504 .unwrap();
8505
8506 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8507 let (editor, cx) = cx.add_window_view(|window, cx| {
8508 build_editor_with_project(project.clone(), buffer, window, cx)
8509 });
8510
8511 cx.executor().start_waiting();
8512
8513 let fake_server = fake_servers.next().await.unwrap();
8514 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8515 move |_params, _| async move {
8516 Ok(Some(vec![lsp::TextEdit::new(
8517 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8518 "applied-formatting\n".to_string(),
8519 )]))
8520 },
8521 );
8522 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8523 move |params, _| async move {
8524 assert_eq!(
8525 params.context.only,
8526 Some(vec!["code-action-1".into(), "code-action-2".into()])
8527 );
8528 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
8529 Ok(Some(vec![
8530 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8531 kind: Some("code-action-1".into()),
8532 edit: Some(lsp::WorkspaceEdit::new(
8533 [(
8534 uri.clone(),
8535 vec![lsp::TextEdit::new(
8536 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8537 "applied-code-action-1-edit\n".to_string(),
8538 )],
8539 )]
8540 .into_iter()
8541 .collect(),
8542 )),
8543 command: Some(lsp::Command {
8544 command: "the-command-for-code-action-1".into(),
8545 ..Default::default()
8546 }),
8547 ..Default::default()
8548 }),
8549 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8550 kind: Some("code-action-2".into()),
8551 edit: Some(lsp::WorkspaceEdit::new(
8552 [(
8553 uri.clone(),
8554 vec![lsp::TextEdit::new(
8555 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8556 "applied-code-action-2-edit\n".to_string(),
8557 )],
8558 )]
8559 .into_iter()
8560 .collect(),
8561 )),
8562 ..Default::default()
8563 }),
8564 ]))
8565 },
8566 );
8567
8568 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
8569 move |params, _| async move { Ok(params) }
8570 });
8571
8572 let command_lock = Arc::new(futures::lock::Mutex::new(()));
8573 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
8574 let fake = fake_server.clone();
8575 let lock = command_lock.clone();
8576 move |params, _| {
8577 assert_eq!(params.command, "the-command-for-code-action-1");
8578 let fake = fake.clone();
8579 let lock = lock.clone();
8580 async move {
8581 lock.lock().await;
8582 fake.server
8583 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
8584 label: None,
8585 edit: lsp::WorkspaceEdit {
8586 changes: Some(
8587 [(
8588 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
8589 vec![lsp::TextEdit {
8590 range: lsp::Range::new(
8591 lsp::Position::new(0, 0),
8592 lsp::Position::new(0, 0),
8593 ),
8594 new_text: "applied-code-action-1-command\n".into(),
8595 }],
8596 )]
8597 .into_iter()
8598 .collect(),
8599 ),
8600 ..Default::default()
8601 },
8602 })
8603 .await
8604 .unwrap();
8605 Ok(Some(json!(null)))
8606 }
8607 }
8608 });
8609
8610 cx.executor().start_waiting();
8611 editor
8612 .update_in(cx, |editor, window, cx| {
8613 editor.perform_format(
8614 project.clone(),
8615 FormatTrigger::Manual,
8616 FormatTarget::Buffers,
8617 window,
8618 cx,
8619 )
8620 })
8621 .unwrap()
8622 .await;
8623 editor.update(cx, |editor, cx| {
8624 assert_eq!(
8625 editor.text(cx),
8626 r#"
8627 applied-code-action-2-edit
8628 applied-code-action-1-command
8629 applied-code-action-1-edit
8630 applied-formatting
8631 one
8632 two
8633 three
8634 "#
8635 .unindent()
8636 );
8637 });
8638
8639 editor.update_in(cx, |editor, window, cx| {
8640 editor.undo(&Default::default(), window, cx);
8641 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8642 });
8643
8644 // Perform a manual edit while waiting for an LSP command
8645 // that's being run as part of a formatting code action.
8646 let lock_guard = command_lock.lock().await;
8647 let format = editor
8648 .update_in(cx, |editor, window, cx| {
8649 editor.perform_format(
8650 project.clone(),
8651 FormatTrigger::Manual,
8652 FormatTarget::Buffers,
8653 window,
8654 cx,
8655 )
8656 })
8657 .unwrap();
8658 cx.run_until_parked();
8659 editor.update(cx, |editor, cx| {
8660 assert_eq!(
8661 editor.text(cx),
8662 r#"
8663 applied-code-action-1-edit
8664 applied-formatting
8665 one
8666 two
8667 three
8668 "#
8669 .unindent()
8670 );
8671
8672 editor.buffer.update(cx, |buffer, cx| {
8673 let ix = buffer.len(cx);
8674 buffer.edit([(ix..ix, "edited\n")], None, cx);
8675 });
8676 });
8677
8678 // Allow the LSP command to proceed. Because the buffer was edited,
8679 // the second code action will not be run.
8680 drop(lock_guard);
8681 format.await;
8682 editor.update_in(cx, |editor, window, cx| {
8683 assert_eq!(
8684 editor.text(cx),
8685 r#"
8686 applied-code-action-1-command
8687 applied-code-action-1-edit
8688 applied-formatting
8689 one
8690 two
8691 three
8692 edited
8693 "#
8694 .unindent()
8695 );
8696
8697 // The manual edit is undone first, because it is the last thing the user did
8698 // (even though the command completed afterwards).
8699 editor.undo(&Default::default(), window, cx);
8700 assert_eq!(
8701 editor.text(cx),
8702 r#"
8703 applied-code-action-1-command
8704 applied-code-action-1-edit
8705 applied-formatting
8706 one
8707 two
8708 three
8709 "#
8710 .unindent()
8711 );
8712
8713 // All the formatting (including the command, which completed after the manual edit)
8714 // is undone together.
8715 editor.undo(&Default::default(), window, cx);
8716 assert_eq!(editor.text(cx), "one \ntwo \nthree");
8717 });
8718}
8719
8720#[gpui::test]
8721async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8722 init_test(cx, |settings| {
8723 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8724 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8725 ))
8726 });
8727
8728 let fs = FakeFs::new(cx.executor());
8729 fs.insert_file(path!("/file.ts"), Default::default()).await;
8730
8731 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8732
8733 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8734 language_registry.add(Arc::new(Language::new(
8735 LanguageConfig {
8736 name: "TypeScript".into(),
8737 matcher: LanguageMatcher {
8738 path_suffixes: vec!["ts".to_string()],
8739 ..Default::default()
8740 },
8741 ..LanguageConfig::default()
8742 },
8743 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8744 )));
8745 update_test_language_settings(cx, |settings| {
8746 settings.defaults.prettier = Some(PrettierSettings {
8747 allowed: true,
8748 ..PrettierSettings::default()
8749 });
8750 });
8751 let mut fake_servers = language_registry.register_fake_lsp(
8752 "TypeScript",
8753 FakeLspAdapter {
8754 capabilities: lsp::ServerCapabilities {
8755 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8756 ..Default::default()
8757 },
8758 ..Default::default()
8759 },
8760 );
8761
8762 let buffer = project
8763 .update(cx, |project, cx| {
8764 project.open_local_buffer(path!("/file.ts"), cx)
8765 })
8766 .await
8767 .unwrap();
8768
8769 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8770 let (editor, cx) = cx.add_window_view(|window, cx| {
8771 build_editor_with_project(project.clone(), buffer, window, cx)
8772 });
8773 editor.update_in(cx, |editor, window, cx| {
8774 editor.set_text(
8775 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8776 window,
8777 cx,
8778 )
8779 });
8780
8781 cx.executor().start_waiting();
8782 let fake_server = fake_servers.next().await.unwrap();
8783
8784 let format = editor
8785 .update_in(cx, |editor, window, cx| {
8786 editor.perform_code_action_kind(
8787 project.clone(),
8788 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8789 window,
8790 cx,
8791 )
8792 })
8793 .unwrap();
8794 fake_server
8795 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8796 assert_eq!(
8797 params.text_document.uri,
8798 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8799 );
8800 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8801 lsp::CodeAction {
8802 title: "Organize Imports".to_string(),
8803 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8804 edit: Some(lsp::WorkspaceEdit {
8805 changes: Some(
8806 [(
8807 params.text_document.uri.clone(),
8808 vec![lsp::TextEdit::new(
8809 lsp::Range::new(
8810 lsp::Position::new(1, 0),
8811 lsp::Position::new(2, 0),
8812 ),
8813 "".to_string(),
8814 )],
8815 )]
8816 .into_iter()
8817 .collect(),
8818 ),
8819 ..Default::default()
8820 }),
8821 ..Default::default()
8822 },
8823 )]))
8824 })
8825 .next()
8826 .await;
8827 cx.executor().start_waiting();
8828 format.await;
8829 assert_eq!(
8830 editor.update(cx, |editor, cx| editor.text(cx)),
8831 "import { a } from 'module';\n\nconst x = a;\n"
8832 );
8833
8834 editor.update_in(cx, |editor, window, cx| {
8835 editor.set_text(
8836 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8837 window,
8838 cx,
8839 )
8840 });
8841 // Ensure we don't lock if code action hangs.
8842 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8843 move |params, _| async move {
8844 assert_eq!(
8845 params.text_document.uri,
8846 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8847 );
8848 futures::future::pending::<()>().await;
8849 unreachable!()
8850 },
8851 );
8852 let format = editor
8853 .update_in(cx, |editor, window, cx| {
8854 editor.perform_code_action_kind(
8855 project,
8856 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8857 window,
8858 cx,
8859 )
8860 })
8861 .unwrap();
8862 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8863 cx.executor().start_waiting();
8864 format.await;
8865 assert_eq!(
8866 editor.update(cx, |editor, cx| editor.text(cx)),
8867 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8868 );
8869}
8870
8871#[gpui::test]
8872async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8873 init_test(cx, |_| {});
8874
8875 let mut cx = EditorLspTestContext::new_rust(
8876 lsp::ServerCapabilities {
8877 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8878 ..Default::default()
8879 },
8880 cx,
8881 )
8882 .await;
8883
8884 cx.set_state(indoc! {"
8885 one.twoˇ
8886 "});
8887
8888 // The format request takes a long time. When it completes, it inserts
8889 // a newline and an indent before the `.`
8890 cx.lsp
8891 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
8892 let executor = cx.background_executor().clone();
8893 async move {
8894 executor.timer(Duration::from_millis(100)).await;
8895 Ok(Some(vec![lsp::TextEdit {
8896 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8897 new_text: "\n ".into(),
8898 }]))
8899 }
8900 });
8901
8902 // Submit a format request.
8903 let format_1 = cx
8904 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8905 .unwrap();
8906 cx.executor().run_until_parked();
8907
8908 // Submit a second format request.
8909 let format_2 = cx
8910 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8911 .unwrap();
8912 cx.executor().run_until_parked();
8913
8914 // Wait for both format requests to complete
8915 cx.executor().advance_clock(Duration::from_millis(200));
8916 cx.executor().start_waiting();
8917 format_1.await.unwrap();
8918 cx.executor().start_waiting();
8919 format_2.await.unwrap();
8920
8921 // The formatting edits only happens once.
8922 cx.assert_editor_state(indoc! {"
8923 one
8924 .twoˇ
8925 "});
8926}
8927
8928#[gpui::test]
8929async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8930 init_test(cx, |settings| {
8931 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8932 });
8933
8934 let mut cx = EditorLspTestContext::new_rust(
8935 lsp::ServerCapabilities {
8936 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8937 ..Default::default()
8938 },
8939 cx,
8940 )
8941 .await;
8942
8943 // Set up a buffer white some trailing whitespace and no trailing newline.
8944 cx.set_state(
8945 &[
8946 "one ", //
8947 "twoˇ", //
8948 "three ", //
8949 "four", //
8950 ]
8951 .join("\n"),
8952 );
8953
8954 // Submit a format request.
8955 let format = cx
8956 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8957 .unwrap();
8958
8959 // Record which buffer changes have been sent to the language server
8960 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8961 cx.lsp
8962 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8963 let buffer_changes = buffer_changes.clone();
8964 move |params, _| {
8965 buffer_changes.lock().extend(
8966 params
8967 .content_changes
8968 .into_iter()
8969 .map(|e| (e.range.unwrap(), e.text)),
8970 );
8971 }
8972 });
8973
8974 // Handle formatting requests to the language server.
8975 cx.lsp
8976 .set_request_handler::<lsp::request::Formatting, _, _>({
8977 let buffer_changes = buffer_changes.clone();
8978 move |_, _| {
8979 // When formatting is requested, trailing whitespace has already been stripped,
8980 // and the trailing newline has already been added.
8981 assert_eq!(
8982 &buffer_changes.lock()[1..],
8983 &[
8984 (
8985 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8986 "".into()
8987 ),
8988 (
8989 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8990 "".into()
8991 ),
8992 (
8993 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8994 "\n".into()
8995 ),
8996 ]
8997 );
8998
8999 // Insert blank lines between each line of the buffer.
9000 async move {
9001 Ok(Some(vec![
9002 lsp::TextEdit {
9003 range: lsp::Range::new(
9004 lsp::Position::new(1, 0),
9005 lsp::Position::new(1, 0),
9006 ),
9007 new_text: "\n".into(),
9008 },
9009 lsp::TextEdit {
9010 range: lsp::Range::new(
9011 lsp::Position::new(2, 0),
9012 lsp::Position::new(2, 0),
9013 ),
9014 new_text: "\n".into(),
9015 },
9016 ]))
9017 }
9018 }
9019 });
9020
9021 // After formatting the buffer, the trailing whitespace is stripped,
9022 // a newline is appended, and the edits provided by the language server
9023 // have been applied.
9024 format.await.unwrap();
9025 cx.assert_editor_state(
9026 &[
9027 "one", //
9028 "", //
9029 "twoˇ", //
9030 "", //
9031 "three", //
9032 "four", //
9033 "", //
9034 ]
9035 .join("\n"),
9036 );
9037
9038 // Undoing the formatting undoes the trailing whitespace removal, the
9039 // trailing newline, and the LSP edits.
9040 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9041 cx.assert_editor_state(
9042 &[
9043 "one ", //
9044 "twoˇ", //
9045 "three ", //
9046 "four", //
9047 ]
9048 .join("\n"),
9049 );
9050}
9051
9052#[gpui::test]
9053async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9054 cx: &mut TestAppContext,
9055) {
9056 init_test(cx, |_| {});
9057
9058 cx.update(|cx| {
9059 cx.update_global::<SettingsStore, _>(|settings, cx| {
9060 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9061 settings.auto_signature_help = Some(true);
9062 });
9063 });
9064 });
9065
9066 let mut cx = EditorLspTestContext::new_rust(
9067 lsp::ServerCapabilities {
9068 signature_help_provider: Some(lsp::SignatureHelpOptions {
9069 ..Default::default()
9070 }),
9071 ..Default::default()
9072 },
9073 cx,
9074 )
9075 .await;
9076
9077 let language = Language::new(
9078 LanguageConfig {
9079 name: "Rust".into(),
9080 brackets: BracketPairConfig {
9081 pairs: vec![
9082 BracketPair {
9083 start: "{".to_string(),
9084 end: "}".to_string(),
9085 close: true,
9086 surround: true,
9087 newline: true,
9088 },
9089 BracketPair {
9090 start: "(".to_string(),
9091 end: ")".to_string(),
9092 close: true,
9093 surround: true,
9094 newline: true,
9095 },
9096 BracketPair {
9097 start: "/*".to_string(),
9098 end: " */".to_string(),
9099 close: true,
9100 surround: true,
9101 newline: true,
9102 },
9103 BracketPair {
9104 start: "[".to_string(),
9105 end: "]".to_string(),
9106 close: false,
9107 surround: false,
9108 newline: true,
9109 },
9110 BracketPair {
9111 start: "\"".to_string(),
9112 end: "\"".to_string(),
9113 close: true,
9114 surround: true,
9115 newline: false,
9116 },
9117 BracketPair {
9118 start: "<".to_string(),
9119 end: ">".to_string(),
9120 close: false,
9121 surround: true,
9122 newline: true,
9123 },
9124 ],
9125 ..Default::default()
9126 },
9127 autoclose_before: "})]".to_string(),
9128 ..Default::default()
9129 },
9130 Some(tree_sitter_rust::LANGUAGE.into()),
9131 );
9132 let language = Arc::new(language);
9133
9134 cx.language_registry().add(language.clone());
9135 cx.update_buffer(|buffer, cx| {
9136 buffer.set_language(Some(language), cx);
9137 });
9138
9139 cx.set_state(
9140 &r#"
9141 fn main() {
9142 sampleˇ
9143 }
9144 "#
9145 .unindent(),
9146 );
9147
9148 cx.update_editor(|editor, window, cx| {
9149 editor.handle_input("(", window, cx);
9150 });
9151 cx.assert_editor_state(
9152 &"
9153 fn main() {
9154 sample(ˇ)
9155 }
9156 "
9157 .unindent(),
9158 );
9159
9160 let mocked_response = lsp::SignatureHelp {
9161 signatures: vec![lsp::SignatureInformation {
9162 label: "fn sample(param1: u8, param2: u8)".to_string(),
9163 documentation: None,
9164 parameters: Some(vec![
9165 lsp::ParameterInformation {
9166 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9167 documentation: None,
9168 },
9169 lsp::ParameterInformation {
9170 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9171 documentation: None,
9172 },
9173 ]),
9174 active_parameter: None,
9175 }],
9176 active_signature: Some(0),
9177 active_parameter: Some(0),
9178 };
9179 handle_signature_help_request(&mut cx, mocked_response).await;
9180
9181 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9182 .await;
9183
9184 cx.editor(|editor, _, _| {
9185 let signature_help_state = editor.signature_help_state.popover().cloned();
9186 assert_eq!(
9187 signature_help_state.unwrap().label,
9188 "param1: u8, param2: u8"
9189 );
9190 });
9191}
9192
9193#[gpui::test]
9194async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9195 init_test(cx, |_| {});
9196
9197 cx.update(|cx| {
9198 cx.update_global::<SettingsStore, _>(|settings, cx| {
9199 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9200 settings.auto_signature_help = Some(false);
9201 settings.show_signature_help_after_edits = Some(false);
9202 });
9203 });
9204 });
9205
9206 let mut cx = EditorLspTestContext::new_rust(
9207 lsp::ServerCapabilities {
9208 signature_help_provider: Some(lsp::SignatureHelpOptions {
9209 ..Default::default()
9210 }),
9211 ..Default::default()
9212 },
9213 cx,
9214 )
9215 .await;
9216
9217 let language = Language::new(
9218 LanguageConfig {
9219 name: "Rust".into(),
9220 brackets: BracketPairConfig {
9221 pairs: vec![
9222 BracketPair {
9223 start: "{".to_string(),
9224 end: "}".to_string(),
9225 close: true,
9226 surround: true,
9227 newline: true,
9228 },
9229 BracketPair {
9230 start: "(".to_string(),
9231 end: ")".to_string(),
9232 close: true,
9233 surround: true,
9234 newline: true,
9235 },
9236 BracketPair {
9237 start: "/*".to_string(),
9238 end: " */".to_string(),
9239 close: true,
9240 surround: true,
9241 newline: true,
9242 },
9243 BracketPair {
9244 start: "[".to_string(),
9245 end: "]".to_string(),
9246 close: false,
9247 surround: false,
9248 newline: true,
9249 },
9250 BracketPair {
9251 start: "\"".to_string(),
9252 end: "\"".to_string(),
9253 close: true,
9254 surround: true,
9255 newline: false,
9256 },
9257 BracketPair {
9258 start: "<".to_string(),
9259 end: ">".to_string(),
9260 close: false,
9261 surround: true,
9262 newline: true,
9263 },
9264 ],
9265 ..Default::default()
9266 },
9267 autoclose_before: "})]".to_string(),
9268 ..Default::default()
9269 },
9270 Some(tree_sitter_rust::LANGUAGE.into()),
9271 );
9272 let language = Arc::new(language);
9273
9274 cx.language_registry().add(language.clone());
9275 cx.update_buffer(|buffer, cx| {
9276 buffer.set_language(Some(language), cx);
9277 });
9278
9279 // Ensure that signature_help is not called when no signature help is enabled.
9280 cx.set_state(
9281 &r#"
9282 fn main() {
9283 sampleˇ
9284 }
9285 "#
9286 .unindent(),
9287 );
9288 cx.update_editor(|editor, window, cx| {
9289 editor.handle_input("(", window, cx);
9290 });
9291 cx.assert_editor_state(
9292 &"
9293 fn main() {
9294 sample(ˇ)
9295 }
9296 "
9297 .unindent(),
9298 );
9299 cx.editor(|editor, _, _| {
9300 assert!(editor.signature_help_state.task().is_none());
9301 });
9302
9303 let mocked_response = lsp::SignatureHelp {
9304 signatures: vec![lsp::SignatureInformation {
9305 label: "fn sample(param1: u8, param2: u8)".to_string(),
9306 documentation: None,
9307 parameters: Some(vec![
9308 lsp::ParameterInformation {
9309 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9310 documentation: None,
9311 },
9312 lsp::ParameterInformation {
9313 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9314 documentation: None,
9315 },
9316 ]),
9317 active_parameter: None,
9318 }],
9319 active_signature: Some(0),
9320 active_parameter: Some(0),
9321 };
9322
9323 // Ensure that signature_help is called when enabled afte edits
9324 cx.update(|_, cx| {
9325 cx.update_global::<SettingsStore, _>(|settings, cx| {
9326 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9327 settings.auto_signature_help = Some(false);
9328 settings.show_signature_help_after_edits = Some(true);
9329 });
9330 });
9331 });
9332 cx.set_state(
9333 &r#"
9334 fn main() {
9335 sampleˇ
9336 }
9337 "#
9338 .unindent(),
9339 );
9340 cx.update_editor(|editor, window, cx| {
9341 editor.handle_input("(", window, cx);
9342 });
9343 cx.assert_editor_state(
9344 &"
9345 fn main() {
9346 sample(ˇ)
9347 }
9348 "
9349 .unindent(),
9350 );
9351 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9352 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9353 .await;
9354 cx.update_editor(|editor, _, _| {
9355 let signature_help_state = editor.signature_help_state.popover().cloned();
9356 assert!(signature_help_state.is_some());
9357 assert_eq!(
9358 signature_help_state.unwrap().label,
9359 "param1: u8, param2: u8"
9360 );
9361 editor.signature_help_state = SignatureHelpState::default();
9362 });
9363
9364 // Ensure that signature_help is called when auto signature help override is enabled
9365 cx.update(|_, cx| {
9366 cx.update_global::<SettingsStore, _>(|settings, cx| {
9367 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9368 settings.auto_signature_help = Some(true);
9369 settings.show_signature_help_after_edits = Some(false);
9370 });
9371 });
9372 });
9373 cx.set_state(
9374 &r#"
9375 fn main() {
9376 sampleˇ
9377 }
9378 "#
9379 .unindent(),
9380 );
9381 cx.update_editor(|editor, window, cx| {
9382 editor.handle_input("(", window, cx);
9383 });
9384 cx.assert_editor_state(
9385 &"
9386 fn main() {
9387 sample(ˇ)
9388 }
9389 "
9390 .unindent(),
9391 );
9392 handle_signature_help_request(&mut cx, mocked_response).await;
9393 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9394 .await;
9395 cx.editor(|editor, _, _| {
9396 let signature_help_state = editor.signature_help_state.popover().cloned();
9397 assert!(signature_help_state.is_some());
9398 assert_eq!(
9399 signature_help_state.unwrap().label,
9400 "param1: u8, param2: u8"
9401 );
9402 });
9403}
9404
9405#[gpui::test]
9406async fn test_signature_help(cx: &mut TestAppContext) {
9407 init_test(cx, |_| {});
9408 cx.update(|cx| {
9409 cx.update_global::<SettingsStore, _>(|settings, cx| {
9410 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9411 settings.auto_signature_help = Some(true);
9412 });
9413 });
9414 });
9415
9416 let mut cx = EditorLspTestContext::new_rust(
9417 lsp::ServerCapabilities {
9418 signature_help_provider: Some(lsp::SignatureHelpOptions {
9419 ..Default::default()
9420 }),
9421 ..Default::default()
9422 },
9423 cx,
9424 )
9425 .await;
9426
9427 // A test that directly calls `show_signature_help`
9428 cx.update_editor(|editor, window, cx| {
9429 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9430 });
9431
9432 let mocked_response = lsp::SignatureHelp {
9433 signatures: vec![lsp::SignatureInformation {
9434 label: "fn sample(param1: u8, param2: u8)".to_string(),
9435 documentation: None,
9436 parameters: Some(vec![
9437 lsp::ParameterInformation {
9438 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9439 documentation: None,
9440 },
9441 lsp::ParameterInformation {
9442 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9443 documentation: None,
9444 },
9445 ]),
9446 active_parameter: None,
9447 }],
9448 active_signature: Some(0),
9449 active_parameter: Some(0),
9450 };
9451 handle_signature_help_request(&mut cx, mocked_response).await;
9452
9453 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9454 .await;
9455
9456 cx.editor(|editor, _, _| {
9457 let signature_help_state = editor.signature_help_state.popover().cloned();
9458 assert!(signature_help_state.is_some());
9459 assert_eq!(
9460 signature_help_state.unwrap().label,
9461 "param1: u8, param2: u8"
9462 );
9463 });
9464
9465 // When exiting outside from inside the brackets, `signature_help` is closed.
9466 cx.set_state(indoc! {"
9467 fn main() {
9468 sample(ˇ);
9469 }
9470
9471 fn sample(param1: u8, param2: u8) {}
9472 "});
9473
9474 cx.update_editor(|editor, window, cx| {
9475 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9476 });
9477
9478 let mocked_response = lsp::SignatureHelp {
9479 signatures: Vec::new(),
9480 active_signature: None,
9481 active_parameter: None,
9482 };
9483 handle_signature_help_request(&mut cx, mocked_response).await;
9484
9485 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9486 .await;
9487
9488 cx.editor(|editor, _, _| {
9489 assert!(!editor.signature_help_state.is_shown());
9490 });
9491
9492 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9493 cx.set_state(indoc! {"
9494 fn main() {
9495 sample(ˇ);
9496 }
9497
9498 fn sample(param1: u8, param2: u8) {}
9499 "});
9500
9501 let mocked_response = lsp::SignatureHelp {
9502 signatures: vec![lsp::SignatureInformation {
9503 label: "fn sample(param1: u8, param2: u8)".to_string(),
9504 documentation: None,
9505 parameters: Some(vec![
9506 lsp::ParameterInformation {
9507 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9508 documentation: None,
9509 },
9510 lsp::ParameterInformation {
9511 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9512 documentation: None,
9513 },
9514 ]),
9515 active_parameter: None,
9516 }],
9517 active_signature: Some(0),
9518 active_parameter: Some(0),
9519 };
9520 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9521 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9522 .await;
9523 cx.editor(|editor, _, _| {
9524 assert!(editor.signature_help_state.is_shown());
9525 });
9526
9527 // Restore the popover with more parameter input
9528 cx.set_state(indoc! {"
9529 fn main() {
9530 sample(param1, param2ˇ);
9531 }
9532
9533 fn sample(param1: u8, param2: u8) {}
9534 "});
9535
9536 let mocked_response = lsp::SignatureHelp {
9537 signatures: vec![lsp::SignatureInformation {
9538 label: "fn sample(param1: u8, param2: u8)".to_string(),
9539 documentation: None,
9540 parameters: Some(vec![
9541 lsp::ParameterInformation {
9542 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9543 documentation: None,
9544 },
9545 lsp::ParameterInformation {
9546 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9547 documentation: None,
9548 },
9549 ]),
9550 active_parameter: None,
9551 }],
9552 active_signature: Some(0),
9553 active_parameter: Some(1),
9554 };
9555 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9556 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9557 .await;
9558
9559 // When selecting a range, the popover is gone.
9560 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9561 cx.update_editor(|editor, window, cx| {
9562 editor.change_selections(None, window, cx, |s| {
9563 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9564 })
9565 });
9566 cx.assert_editor_state(indoc! {"
9567 fn main() {
9568 sample(param1, «ˇparam2»);
9569 }
9570
9571 fn sample(param1: u8, param2: u8) {}
9572 "});
9573 cx.editor(|editor, _, _| {
9574 assert!(!editor.signature_help_state.is_shown());
9575 });
9576
9577 // When unselecting again, the popover is back if within the brackets.
9578 cx.update_editor(|editor, window, cx| {
9579 editor.change_selections(None, window, cx, |s| {
9580 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9581 })
9582 });
9583 cx.assert_editor_state(indoc! {"
9584 fn main() {
9585 sample(param1, ˇparam2);
9586 }
9587
9588 fn sample(param1: u8, param2: u8) {}
9589 "});
9590 handle_signature_help_request(&mut cx, mocked_response).await;
9591 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9592 .await;
9593 cx.editor(|editor, _, _| {
9594 assert!(editor.signature_help_state.is_shown());
9595 });
9596
9597 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9598 cx.update_editor(|editor, window, cx| {
9599 editor.change_selections(None, window, cx, |s| {
9600 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9601 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9602 })
9603 });
9604 cx.assert_editor_state(indoc! {"
9605 fn main() {
9606 sample(param1, ˇparam2);
9607 }
9608
9609 fn sample(param1: u8, param2: u8) {}
9610 "});
9611
9612 let mocked_response = lsp::SignatureHelp {
9613 signatures: vec![lsp::SignatureInformation {
9614 label: "fn sample(param1: u8, param2: u8)".to_string(),
9615 documentation: None,
9616 parameters: Some(vec![
9617 lsp::ParameterInformation {
9618 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9619 documentation: None,
9620 },
9621 lsp::ParameterInformation {
9622 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9623 documentation: None,
9624 },
9625 ]),
9626 active_parameter: None,
9627 }],
9628 active_signature: Some(0),
9629 active_parameter: Some(1),
9630 };
9631 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9632 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9633 .await;
9634 cx.update_editor(|editor, _, cx| {
9635 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9636 });
9637 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9638 .await;
9639 cx.update_editor(|editor, window, cx| {
9640 editor.change_selections(None, window, cx, |s| {
9641 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9642 })
9643 });
9644 cx.assert_editor_state(indoc! {"
9645 fn main() {
9646 sample(param1, «ˇparam2»);
9647 }
9648
9649 fn sample(param1: u8, param2: u8) {}
9650 "});
9651 cx.update_editor(|editor, window, cx| {
9652 editor.change_selections(None, window, cx, |s| {
9653 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9654 })
9655 });
9656 cx.assert_editor_state(indoc! {"
9657 fn main() {
9658 sample(param1, ˇparam2);
9659 }
9660
9661 fn sample(param1: u8, param2: u8) {}
9662 "});
9663 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9664 .await;
9665}
9666
9667#[gpui::test]
9668async fn test_completion_mode(cx: &mut TestAppContext) {
9669 init_test(cx, |_| {});
9670 let mut cx = EditorLspTestContext::new_rust(
9671 lsp::ServerCapabilities {
9672 completion_provider: Some(lsp::CompletionOptions {
9673 resolve_provider: Some(true),
9674 ..Default::default()
9675 }),
9676 ..Default::default()
9677 },
9678 cx,
9679 )
9680 .await;
9681
9682 struct Run {
9683 run_description: &'static str,
9684 initial_state: String,
9685 buffer_marked_text: String,
9686 completion_text: &'static str,
9687 expected_with_insert_mode: String,
9688 expected_with_replace_mode: String,
9689 expected_with_replace_subsequence_mode: String,
9690 expected_with_replace_suffix_mode: String,
9691 }
9692
9693 let runs = [
9694 Run {
9695 run_description: "Start of word matches completion text",
9696 initial_state: "before ediˇ after".into(),
9697 buffer_marked_text: "before <edi|> after".into(),
9698 completion_text: "editor",
9699 expected_with_insert_mode: "before editorˇ after".into(),
9700 expected_with_replace_mode: "before editorˇ after".into(),
9701 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9702 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9703 },
9704 Run {
9705 run_description: "Accept same text at the middle of the word",
9706 initial_state: "before ediˇtor after".into(),
9707 buffer_marked_text: "before <edi|tor> after".into(),
9708 completion_text: "editor",
9709 expected_with_insert_mode: "before editorˇtor after".into(),
9710 expected_with_replace_mode: "before editorˇ after".into(),
9711 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9712 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9713 },
9714 Run {
9715 run_description: "End of word matches completion text -- cursor at end",
9716 initial_state: "before torˇ after".into(),
9717 buffer_marked_text: "before <tor|> after".into(),
9718 completion_text: "editor",
9719 expected_with_insert_mode: "before editorˇ after".into(),
9720 expected_with_replace_mode: "before editorˇ after".into(),
9721 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9722 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9723 },
9724 Run {
9725 run_description: "End of word matches completion text -- cursor at start",
9726 initial_state: "before ˇtor after".into(),
9727 buffer_marked_text: "before <|tor> after".into(),
9728 completion_text: "editor",
9729 expected_with_insert_mode: "before editorˇtor after".into(),
9730 expected_with_replace_mode: "before editorˇ after".into(),
9731 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9732 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9733 },
9734 Run {
9735 run_description: "Prepend text containing whitespace",
9736 initial_state: "pˇfield: bool".into(),
9737 buffer_marked_text: "<p|field>: bool".into(),
9738 completion_text: "pub ",
9739 expected_with_insert_mode: "pub ˇfield: bool".into(),
9740 expected_with_replace_mode: "pub ˇ: bool".into(),
9741 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
9742 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
9743 },
9744 Run {
9745 run_description: "Add element to start of list",
9746 initial_state: "[element_ˇelement_2]".into(),
9747 buffer_marked_text: "[<element_|element_2>]".into(),
9748 completion_text: "element_1",
9749 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
9750 expected_with_replace_mode: "[element_1ˇ]".into(),
9751 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
9752 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
9753 },
9754 Run {
9755 run_description: "Add element to start of list -- first and second elements are equal",
9756 initial_state: "[elˇelement]".into(),
9757 buffer_marked_text: "[<el|element>]".into(),
9758 completion_text: "element",
9759 expected_with_insert_mode: "[elementˇelement]".into(),
9760 expected_with_replace_mode: "[elementˇ]".into(),
9761 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
9762 expected_with_replace_suffix_mode: "[elementˇ]".into(),
9763 },
9764 Run {
9765 run_description: "Ends with matching suffix",
9766 initial_state: "SubˇError".into(),
9767 buffer_marked_text: "<Sub|Error>".into(),
9768 completion_text: "SubscriptionError",
9769 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
9770 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9771 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9772 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
9773 },
9774 Run {
9775 run_description: "Suffix is a subsequence -- contiguous",
9776 initial_state: "SubˇErr".into(),
9777 buffer_marked_text: "<Sub|Err>".into(),
9778 completion_text: "SubscriptionError",
9779 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
9780 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9781 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9782 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
9783 },
9784 Run {
9785 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
9786 initial_state: "Suˇscrirr".into(),
9787 buffer_marked_text: "<Su|scrirr>".into(),
9788 completion_text: "SubscriptionError",
9789 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
9790 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9791 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9792 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
9793 },
9794 Run {
9795 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
9796 initial_state: "foo(indˇix)".into(),
9797 buffer_marked_text: "foo(<ind|ix>)".into(),
9798 completion_text: "node_index",
9799 expected_with_insert_mode: "foo(node_indexˇix)".into(),
9800 expected_with_replace_mode: "foo(node_indexˇ)".into(),
9801 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
9802 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
9803 },
9804 ];
9805
9806 for run in runs {
9807 let run_variations = [
9808 (LspInsertMode::Insert, run.expected_with_insert_mode),
9809 (LspInsertMode::Replace, run.expected_with_replace_mode),
9810 (
9811 LspInsertMode::ReplaceSubsequence,
9812 run.expected_with_replace_subsequence_mode,
9813 ),
9814 (
9815 LspInsertMode::ReplaceSuffix,
9816 run.expected_with_replace_suffix_mode,
9817 ),
9818 ];
9819
9820 for (lsp_insert_mode, expected_text) in run_variations {
9821 eprintln!(
9822 "run = {:?}, mode = {lsp_insert_mode:.?}",
9823 run.run_description,
9824 );
9825
9826 update_test_language_settings(&mut cx, |settings| {
9827 settings.defaults.completions = Some(CompletionSettings {
9828 lsp_insert_mode,
9829 words: WordsCompletionMode::Disabled,
9830 lsp: true,
9831 lsp_fetch_timeout_ms: 0,
9832 });
9833 });
9834
9835 cx.set_state(&run.initial_state);
9836 cx.update_editor(|editor, window, cx| {
9837 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9838 });
9839
9840 let counter = Arc::new(AtomicUsize::new(0));
9841 handle_completion_request_with_insert_and_replace(
9842 &mut cx,
9843 &run.buffer_marked_text,
9844 vec![run.completion_text],
9845 counter.clone(),
9846 )
9847 .await;
9848 cx.condition(|editor, _| editor.context_menu_visible())
9849 .await;
9850 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9851
9852 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9853 editor
9854 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9855 .unwrap()
9856 });
9857 cx.assert_editor_state(&expected_text);
9858 handle_resolve_completion_request(&mut cx, None).await;
9859 apply_additional_edits.await.unwrap();
9860 }
9861 }
9862}
9863
9864#[gpui::test]
9865async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
9866 init_test(cx, |_| {});
9867 let mut cx = EditorLspTestContext::new_rust(
9868 lsp::ServerCapabilities {
9869 completion_provider: Some(lsp::CompletionOptions {
9870 resolve_provider: Some(true),
9871 ..Default::default()
9872 }),
9873 ..Default::default()
9874 },
9875 cx,
9876 )
9877 .await;
9878
9879 let initial_state = "SubˇError";
9880 let buffer_marked_text = "<Sub|Error>";
9881 let completion_text = "SubscriptionError";
9882 let expected_with_insert_mode = "SubscriptionErrorˇError";
9883 let expected_with_replace_mode = "SubscriptionErrorˇ";
9884
9885 update_test_language_settings(&mut cx, |settings| {
9886 settings.defaults.completions = Some(CompletionSettings {
9887 words: WordsCompletionMode::Disabled,
9888 // set the opposite here to ensure that the action is overriding the default behavior
9889 lsp_insert_mode: LspInsertMode::Insert,
9890 lsp: true,
9891 lsp_fetch_timeout_ms: 0,
9892 });
9893 });
9894
9895 cx.set_state(initial_state);
9896 cx.update_editor(|editor, window, cx| {
9897 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9898 });
9899
9900 let counter = Arc::new(AtomicUsize::new(0));
9901 handle_completion_request_with_insert_and_replace(
9902 &mut cx,
9903 &buffer_marked_text,
9904 vec![completion_text],
9905 counter.clone(),
9906 )
9907 .await;
9908 cx.condition(|editor, _| editor.context_menu_visible())
9909 .await;
9910 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9911
9912 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9913 editor
9914 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
9915 .unwrap()
9916 });
9917 cx.assert_editor_state(&expected_with_replace_mode);
9918 handle_resolve_completion_request(&mut cx, None).await;
9919 apply_additional_edits.await.unwrap();
9920
9921 update_test_language_settings(&mut cx, |settings| {
9922 settings.defaults.completions = Some(CompletionSettings {
9923 words: WordsCompletionMode::Disabled,
9924 // set the opposite here to ensure that the action is overriding the default behavior
9925 lsp_insert_mode: LspInsertMode::Replace,
9926 lsp: true,
9927 lsp_fetch_timeout_ms: 0,
9928 });
9929 });
9930
9931 cx.set_state(initial_state);
9932 cx.update_editor(|editor, window, cx| {
9933 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9934 });
9935 handle_completion_request_with_insert_and_replace(
9936 &mut cx,
9937 &buffer_marked_text,
9938 vec![completion_text],
9939 counter.clone(),
9940 )
9941 .await;
9942 cx.condition(|editor, _| editor.context_menu_visible())
9943 .await;
9944 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9945
9946 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9947 editor
9948 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
9949 .unwrap()
9950 });
9951 cx.assert_editor_state(&expected_with_insert_mode);
9952 handle_resolve_completion_request(&mut cx, None).await;
9953 apply_additional_edits.await.unwrap();
9954}
9955
9956#[gpui::test]
9957async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
9958 init_test(cx, |_| {});
9959 let mut cx = EditorLspTestContext::new_rust(
9960 lsp::ServerCapabilities {
9961 completion_provider: Some(lsp::CompletionOptions {
9962 resolve_provider: Some(true),
9963 ..Default::default()
9964 }),
9965 ..Default::default()
9966 },
9967 cx,
9968 )
9969 .await;
9970
9971 // scenario: surrounding text matches completion text
9972 let completion_text = "to_offset";
9973 let initial_state = indoc! {"
9974 1. buf.to_offˇsuffix
9975 2. buf.to_offˇsuf
9976 3. buf.to_offˇfix
9977 4. buf.to_offˇ
9978 5. into_offˇensive
9979 6. ˇsuffix
9980 7. let ˇ //
9981 8. aaˇzz
9982 9. buf.to_off«zzzzzˇ»suffix
9983 10. buf.«ˇzzzzz»suffix
9984 11. to_off«ˇzzzzz»
9985
9986 buf.to_offˇsuffix // newest cursor
9987 "};
9988 let completion_marked_buffer = indoc! {"
9989 1. buf.to_offsuffix
9990 2. buf.to_offsuf
9991 3. buf.to_offfix
9992 4. buf.to_off
9993 5. into_offensive
9994 6. suffix
9995 7. let //
9996 8. aazz
9997 9. buf.to_offzzzzzsuffix
9998 10. buf.zzzzzsuffix
9999 11. to_offzzzzz
10000
10001 buf.<to_off|suffix> // newest cursor
10002 "};
10003 let expected = indoc! {"
10004 1. buf.to_offsetˇ
10005 2. buf.to_offsetˇsuf
10006 3. buf.to_offsetˇfix
10007 4. buf.to_offsetˇ
10008 5. into_offsetˇensive
10009 6. to_offsetˇsuffix
10010 7. let to_offsetˇ //
10011 8. aato_offsetˇzz
10012 9. buf.to_offsetˇ
10013 10. buf.to_offsetˇsuffix
10014 11. to_offsetˇ
10015
10016 buf.to_offsetˇ // newest cursor
10017 "};
10018 cx.set_state(initial_state);
10019 cx.update_editor(|editor, window, cx| {
10020 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10021 });
10022 handle_completion_request_with_insert_and_replace(
10023 &mut cx,
10024 completion_marked_buffer,
10025 vec![completion_text],
10026 Arc::new(AtomicUsize::new(0)),
10027 )
10028 .await;
10029 cx.condition(|editor, _| editor.context_menu_visible())
10030 .await;
10031 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10032 editor
10033 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10034 .unwrap()
10035 });
10036 cx.assert_editor_state(expected);
10037 handle_resolve_completion_request(&mut cx, None).await;
10038 apply_additional_edits.await.unwrap();
10039
10040 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10041 let completion_text = "foo_and_bar";
10042 let initial_state = indoc! {"
10043 1. ooanbˇ
10044 2. zooanbˇ
10045 3. ooanbˇz
10046 4. zooanbˇz
10047 5. ooanˇ
10048 6. oanbˇ
10049
10050 ooanbˇ
10051 "};
10052 let completion_marked_buffer = indoc! {"
10053 1. ooanb
10054 2. zooanb
10055 3. ooanbz
10056 4. zooanbz
10057 5. ooan
10058 6. oanb
10059
10060 <ooanb|>
10061 "};
10062 let expected = indoc! {"
10063 1. foo_and_barˇ
10064 2. zfoo_and_barˇ
10065 3. foo_and_barˇz
10066 4. zfoo_and_barˇz
10067 5. ooanfoo_and_barˇ
10068 6. oanbfoo_and_barˇ
10069
10070 foo_and_barˇ
10071 "};
10072 cx.set_state(initial_state);
10073 cx.update_editor(|editor, window, cx| {
10074 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10075 });
10076 handle_completion_request_with_insert_and_replace(
10077 &mut cx,
10078 completion_marked_buffer,
10079 vec![completion_text],
10080 Arc::new(AtomicUsize::new(0)),
10081 )
10082 .await;
10083 cx.condition(|editor, _| editor.context_menu_visible())
10084 .await;
10085 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10086 editor
10087 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10088 .unwrap()
10089 });
10090 cx.assert_editor_state(expected);
10091 handle_resolve_completion_request(&mut cx, None).await;
10092 apply_additional_edits.await.unwrap();
10093
10094 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10095 // (expects the same as if it was inserted at the end)
10096 let completion_text = "foo_and_bar";
10097 let initial_state = indoc! {"
10098 1. ooˇanb
10099 2. zooˇanb
10100 3. ooˇanbz
10101 4. zooˇanbz
10102
10103 ooˇanb
10104 "};
10105 let completion_marked_buffer = indoc! {"
10106 1. ooanb
10107 2. zooanb
10108 3. ooanbz
10109 4. zooanbz
10110
10111 <oo|anb>
10112 "};
10113 let expected = indoc! {"
10114 1. foo_and_barˇ
10115 2. zfoo_and_barˇ
10116 3. foo_and_barˇz
10117 4. zfoo_and_barˇz
10118
10119 foo_and_barˇ
10120 "};
10121 cx.set_state(initial_state);
10122 cx.update_editor(|editor, window, cx| {
10123 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10124 });
10125 handle_completion_request_with_insert_and_replace(
10126 &mut cx,
10127 completion_marked_buffer,
10128 vec![completion_text],
10129 Arc::new(AtomicUsize::new(0)),
10130 )
10131 .await;
10132 cx.condition(|editor, _| editor.context_menu_visible())
10133 .await;
10134 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10135 editor
10136 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10137 .unwrap()
10138 });
10139 cx.assert_editor_state(expected);
10140 handle_resolve_completion_request(&mut cx, None).await;
10141 apply_additional_edits.await.unwrap();
10142}
10143
10144// This used to crash
10145#[gpui::test]
10146async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10147 init_test(cx, |_| {});
10148
10149 let buffer_text = indoc! {"
10150 fn main() {
10151 10.satu;
10152
10153 //
10154 // separate cursors so they open in different excerpts (manually reproducible)
10155 //
10156
10157 10.satu20;
10158 }
10159 "};
10160 let multibuffer_text_with_selections = indoc! {"
10161 fn main() {
10162 10.satuˇ;
10163
10164 //
10165
10166 //
10167
10168 10.satuˇ20;
10169 }
10170 "};
10171 let expected_multibuffer = indoc! {"
10172 fn main() {
10173 10.saturating_sub()ˇ;
10174
10175 //
10176
10177 //
10178
10179 10.saturating_sub()ˇ;
10180 }
10181 "};
10182
10183 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10184 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10185
10186 let fs = FakeFs::new(cx.executor());
10187 fs.insert_tree(
10188 path!("/a"),
10189 json!({
10190 "main.rs": buffer_text,
10191 }),
10192 )
10193 .await;
10194
10195 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10196 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10197 language_registry.add(rust_lang());
10198 let mut fake_servers = language_registry.register_fake_lsp(
10199 "Rust",
10200 FakeLspAdapter {
10201 capabilities: lsp::ServerCapabilities {
10202 completion_provider: Some(lsp::CompletionOptions {
10203 resolve_provider: None,
10204 ..lsp::CompletionOptions::default()
10205 }),
10206 ..lsp::ServerCapabilities::default()
10207 },
10208 ..FakeLspAdapter::default()
10209 },
10210 );
10211 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10212 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10213 let buffer = project
10214 .update(cx, |project, cx| {
10215 project.open_local_buffer(path!("/a/main.rs"), cx)
10216 })
10217 .await
10218 .unwrap();
10219
10220 let multi_buffer = cx.new(|cx| {
10221 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10222 multi_buffer.push_excerpts(
10223 buffer.clone(),
10224 [ExcerptRange::new(0..first_excerpt_end)],
10225 cx,
10226 );
10227 multi_buffer.push_excerpts(
10228 buffer.clone(),
10229 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10230 cx,
10231 );
10232 multi_buffer
10233 });
10234
10235 let editor = workspace
10236 .update(cx, |_, window, cx| {
10237 cx.new(|cx| {
10238 Editor::new(
10239 EditorMode::Full {
10240 scale_ui_elements_with_buffer_font_size: false,
10241 show_active_line_background: false,
10242 },
10243 multi_buffer.clone(),
10244 Some(project.clone()),
10245 window,
10246 cx,
10247 )
10248 })
10249 })
10250 .unwrap();
10251
10252 let pane = workspace
10253 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10254 .unwrap();
10255 pane.update_in(cx, |pane, window, cx| {
10256 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10257 });
10258
10259 let fake_server = fake_servers.next().await.unwrap();
10260
10261 editor.update_in(cx, |editor, window, cx| {
10262 editor.change_selections(None, window, cx, |s| {
10263 s.select_ranges([
10264 Point::new(1, 11)..Point::new(1, 11),
10265 Point::new(7, 11)..Point::new(7, 11),
10266 ])
10267 });
10268
10269 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10270 });
10271
10272 editor.update_in(cx, |editor, window, cx| {
10273 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10274 });
10275
10276 fake_server
10277 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10278 let completion_item = lsp::CompletionItem {
10279 label: "saturating_sub()".into(),
10280 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10281 lsp::InsertReplaceEdit {
10282 new_text: "saturating_sub()".to_owned(),
10283 insert: lsp::Range::new(
10284 lsp::Position::new(7, 7),
10285 lsp::Position::new(7, 11),
10286 ),
10287 replace: lsp::Range::new(
10288 lsp::Position::new(7, 7),
10289 lsp::Position::new(7, 13),
10290 ),
10291 },
10292 )),
10293 ..lsp::CompletionItem::default()
10294 };
10295
10296 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10297 })
10298 .next()
10299 .await
10300 .unwrap();
10301
10302 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10303 .await;
10304
10305 editor
10306 .update_in(cx, |editor, window, cx| {
10307 editor
10308 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10309 .unwrap()
10310 })
10311 .await
10312 .unwrap();
10313
10314 editor.update(cx, |editor, cx| {
10315 assert_text_with_selections(editor, expected_multibuffer, cx);
10316 })
10317}
10318
10319#[gpui::test]
10320async fn test_completion(cx: &mut TestAppContext) {
10321 init_test(cx, |_| {});
10322
10323 let mut cx = EditorLspTestContext::new_rust(
10324 lsp::ServerCapabilities {
10325 completion_provider: Some(lsp::CompletionOptions {
10326 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10327 resolve_provider: Some(true),
10328 ..Default::default()
10329 }),
10330 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10331 ..Default::default()
10332 },
10333 cx,
10334 )
10335 .await;
10336 let counter = Arc::new(AtomicUsize::new(0));
10337
10338 cx.set_state(indoc! {"
10339 oneˇ
10340 two
10341 three
10342 "});
10343 cx.simulate_keystroke(".");
10344 handle_completion_request(
10345 &mut cx,
10346 indoc! {"
10347 one.|<>
10348 two
10349 three
10350 "},
10351 vec!["first_completion", "second_completion"],
10352 counter.clone(),
10353 )
10354 .await;
10355 cx.condition(|editor, _| editor.context_menu_visible())
10356 .await;
10357 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10358
10359 let _handler = handle_signature_help_request(
10360 &mut cx,
10361 lsp::SignatureHelp {
10362 signatures: vec![lsp::SignatureInformation {
10363 label: "test signature".to_string(),
10364 documentation: None,
10365 parameters: Some(vec![lsp::ParameterInformation {
10366 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10367 documentation: None,
10368 }]),
10369 active_parameter: None,
10370 }],
10371 active_signature: None,
10372 active_parameter: None,
10373 },
10374 );
10375 cx.update_editor(|editor, window, cx| {
10376 assert!(
10377 !editor.signature_help_state.is_shown(),
10378 "No signature help was called for"
10379 );
10380 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10381 });
10382 cx.run_until_parked();
10383 cx.update_editor(|editor, _, _| {
10384 assert!(
10385 !editor.signature_help_state.is_shown(),
10386 "No signature help should be shown when completions menu is open"
10387 );
10388 });
10389
10390 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10391 editor.context_menu_next(&Default::default(), window, cx);
10392 editor
10393 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10394 .unwrap()
10395 });
10396 cx.assert_editor_state(indoc! {"
10397 one.second_completionˇ
10398 two
10399 three
10400 "});
10401
10402 handle_resolve_completion_request(
10403 &mut cx,
10404 Some(vec![
10405 (
10406 //This overlaps with the primary completion edit which is
10407 //misbehavior from the LSP spec, test that we filter it out
10408 indoc! {"
10409 one.second_ˇcompletion
10410 two
10411 threeˇ
10412 "},
10413 "overlapping additional edit",
10414 ),
10415 (
10416 indoc! {"
10417 one.second_completion
10418 two
10419 threeˇ
10420 "},
10421 "\nadditional edit",
10422 ),
10423 ]),
10424 )
10425 .await;
10426 apply_additional_edits.await.unwrap();
10427 cx.assert_editor_state(indoc! {"
10428 one.second_completionˇ
10429 two
10430 three
10431 additional edit
10432 "});
10433
10434 cx.set_state(indoc! {"
10435 one.second_completion
10436 twoˇ
10437 threeˇ
10438 additional edit
10439 "});
10440 cx.simulate_keystroke(" ");
10441 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10442 cx.simulate_keystroke("s");
10443 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10444
10445 cx.assert_editor_state(indoc! {"
10446 one.second_completion
10447 two sˇ
10448 three sˇ
10449 additional edit
10450 "});
10451 handle_completion_request(
10452 &mut cx,
10453 indoc! {"
10454 one.second_completion
10455 two s
10456 three <s|>
10457 additional edit
10458 "},
10459 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10460 counter.clone(),
10461 )
10462 .await;
10463 cx.condition(|editor, _| editor.context_menu_visible())
10464 .await;
10465 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10466
10467 cx.simulate_keystroke("i");
10468
10469 handle_completion_request(
10470 &mut cx,
10471 indoc! {"
10472 one.second_completion
10473 two si
10474 three <si|>
10475 additional edit
10476 "},
10477 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10478 counter.clone(),
10479 )
10480 .await;
10481 cx.condition(|editor, _| editor.context_menu_visible())
10482 .await;
10483 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
10484
10485 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10486 editor
10487 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10488 .unwrap()
10489 });
10490 cx.assert_editor_state(indoc! {"
10491 one.second_completion
10492 two sixth_completionˇ
10493 three sixth_completionˇ
10494 additional edit
10495 "});
10496
10497 apply_additional_edits.await.unwrap();
10498
10499 update_test_language_settings(&mut cx, |settings| {
10500 settings.defaults.show_completions_on_input = Some(false);
10501 });
10502 cx.set_state("editorˇ");
10503 cx.simulate_keystroke(".");
10504 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10505 cx.simulate_keystrokes("c l o");
10506 cx.assert_editor_state("editor.cloˇ");
10507 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10508 cx.update_editor(|editor, window, cx| {
10509 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10510 });
10511 handle_completion_request(
10512 &mut cx,
10513 "editor.<clo|>",
10514 vec!["close", "clobber"],
10515 counter.clone(),
10516 )
10517 .await;
10518 cx.condition(|editor, _| editor.context_menu_visible())
10519 .await;
10520 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
10521
10522 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10523 editor
10524 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10525 .unwrap()
10526 });
10527 cx.assert_editor_state("editor.closeˇ");
10528 handle_resolve_completion_request(&mut cx, None).await;
10529 apply_additional_edits.await.unwrap();
10530}
10531
10532#[gpui::test]
10533async fn test_word_completion(cx: &mut TestAppContext) {
10534 let lsp_fetch_timeout_ms = 10;
10535 init_test(cx, |language_settings| {
10536 language_settings.defaults.completions = Some(CompletionSettings {
10537 words: WordsCompletionMode::Fallback,
10538 lsp: true,
10539 lsp_fetch_timeout_ms: 10,
10540 lsp_insert_mode: LspInsertMode::Insert,
10541 });
10542 });
10543
10544 let mut cx = EditorLspTestContext::new_rust(
10545 lsp::ServerCapabilities {
10546 completion_provider: Some(lsp::CompletionOptions {
10547 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10548 ..lsp::CompletionOptions::default()
10549 }),
10550 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10551 ..lsp::ServerCapabilities::default()
10552 },
10553 cx,
10554 )
10555 .await;
10556
10557 let throttle_completions = Arc::new(AtomicBool::new(false));
10558
10559 let lsp_throttle_completions = throttle_completions.clone();
10560 let _completion_requests_handler =
10561 cx.lsp
10562 .server
10563 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
10564 let lsp_throttle_completions = lsp_throttle_completions.clone();
10565 let cx = cx.clone();
10566 async move {
10567 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
10568 cx.background_executor()
10569 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
10570 .await;
10571 }
10572 Ok(Some(lsp::CompletionResponse::Array(vec![
10573 lsp::CompletionItem {
10574 label: "first".into(),
10575 ..lsp::CompletionItem::default()
10576 },
10577 lsp::CompletionItem {
10578 label: "last".into(),
10579 ..lsp::CompletionItem::default()
10580 },
10581 ])))
10582 }
10583 });
10584
10585 cx.set_state(indoc! {"
10586 oneˇ
10587 two
10588 three
10589 "});
10590 cx.simulate_keystroke(".");
10591 cx.executor().run_until_parked();
10592 cx.condition(|editor, _| editor.context_menu_visible())
10593 .await;
10594 cx.update_editor(|editor, window, cx| {
10595 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10596 {
10597 assert_eq!(
10598 completion_menu_entries(&menu),
10599 &["first", "last"],
10600 "When LSP server is fast to reply, no fallback word completions are used"
10601 );
10602 } else {
10603 panic!("expected completion menu to be open");
10604 }
10605 editor.cancel(&Cancel, window, cx);
10606 });
10607 cx.executor().run_until_parked();
10608 cx.condition(|editor, _| !editor.context_menu_visible())
10609 .await;
10610
10611 throttle_completions.store(true, atomic::Ordering::Release);
10612 cx.simulate_keystroke(".");
10613 cx.executor()
10614 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
10615 cx.executor().run_until_parked();
10616 cx.condition(|editor, _| editor.context_menu_visible())
10617 .await;
10618 cx.update_editor(|editor, _, _| {
10619 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10620 {
10621 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
10622 "When LSP server is slow, document words can be shown instead, if configured accordingly");
10623 } else {
10624 panic!("expected completion menu to be open");
10625 }
10626 });
10627}
10628
10629#[gpui::test]
10630async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
10631 init_test(cx, |language_settings| {
10632 language_settings.defaults.completions = Some(CompletionSettings {
10633 words: WordsCompletionMode::Enabled,
10634 lsp: true,
10635 lsp_fetch_timeout_ms: 0,
10636 lsp_insert_mode: LspInsertMode::Insert,
10637 });
10638 });
10639
10640 let mut cx = EditorLspTestContext::new_rust(
10641 lsp::ServerCapabilities {
10642 completion_provider: Some(lsp::CompletionOptions {
10643 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10644 ..lsp::CompletionOptions::default()
10645 }),
10646 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10647 ..lsp::ServerCapabilities::default()
10648 },
10649 cx,
10650 )
10651 .await;
10652
10653 let _completion_requests_handler =
10654 cx.lsp
10655 .server
10656 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10657 Ok(Some(lsp::CompletionResponse::Array(vec![
10658 lsp::CompletionItem {
10659 label: "first".into(),
10660 ..lsp::CompletionItem::default()
10661 },
10662 lsp::CompletionItem {
10663 label: "last".into(),
10664 ..lsp::CompletionItem::default()
10665 },
10666 ])))
10667 });
10668
10669 cx.set_state(indoc! {"ˇ
10670 first
10671 last
10672 second
10673 "});
10674 cx.simulate_keystroke(".");
10675 cx.executor().run_until_parked();
10676 cx.condition(|editor, _| editor.context_menu_visible())
10677 .await;
10678 cx.update_editor(|editor, _, _| {
10679 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10680 {
10681 assert_eq!(
10682 completion_menu_entries(&menu),
10683 &["first", "last", "second"],
10684 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
10685 );
10686 } else {
10687 panic!("expected completion menu to be open");
10688 }
10689 });
10690}
10691
10692#[gpui::test]
10693async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
10694 init_test(cx, |language_settings| {
10695 language_settings.defaults.completions = Some(CompletionSettings {
10696 words: WordsCompletionMode::Disabled,
10697 lsp: true,
10698 lsp_fetch_timeout_ms: 0,
10699 lsp_insert_mode: LspInsertMode::Insert,
10700 });
10701 });
10702
10703 let mut cx = EditorLspTestContext::new_rust(
10704 lsp::ServerCapabilities {
10705 completion_provider: Some(lsp::CompletionOptions {
10706 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10707 ..lsp::CompletionOptions::default()
10708 }),
10709 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10710 ..lsp::ServerCapabilities::default()
10711 },
10712 cx,
10713 )
10714 .await;
10715
10716 let _completion_requests_handler =
10717 cx.lsp
10718 .server
10719 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
10720 panic!("LSP completions should not be queried when dealing with word completions")
10721 });
10722
10723 cx.set_state(indoc! {"ˇ
10724 first
10725 last
10726 second
10727 "});
10728 cx.update_editor(|editor, window, cx| {
10729 editor.show_word_completions(&ShowWordCompletions, window, cx);
10730 });
10731 cx.executor().run_until_parked();
10732 cx.condition(|editor, _| editor.context_menu_visible())
10733 .await;
10734 cx.update_editor(|editor, _, _| {
10735 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10736 {
10737 assert_eq!(
10738 completion_menu_entries(&menu),
10739 &["first", "last", "second"],
10740 "`ShowWordCompletions` action should show word completions"
10741 );
10742 } else {
10743 panic!("expected completion menu to be open");
10744 }
10745 });
10746
10747 cx.simulate_keystroke("l");
10748 cx.executor().run_until_parked();
10749 cx.condition(|editor, _| editor.context_menu_visible())
10750 .await;
10751 cx.update_editor(|editor, _, _| {
10752 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10753 {
10754 assert_eq!(
10755 completion_menu_entries(&menu),
10756 &["last"],
10757 "After showing word completions, further editing should filter them and not query the LSP"
10758 );
10759 } else {
10760 panic!("expected completion menu to be open");
10761 }
10762 });
10763}
10764
10765#[gpui::test]
10766async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
10767 init_test(cx, |language_settings| {
10768 language_settings.defaults.completions = Some(CompletionSettings {
10769 words: WordsCompletionMode::Fallback,
10770 lsp: false,
10771 lsp_fetch_timeout_ms: 0,
10772 lsp_insert_mode: LspInsertMode::Insert,
10773 });
10774 });
10775
10776 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
10777
10778 cx.set_state(indoc! {"ˇ
10779 0_usize
10780 let
10781 33
10782 4.5f32
10783 "});
10784 cx.update_editor(|editor, window, cx| {
10785 editor.show_completions(&ShowCompletions::default(), window, cx);
10786 });
10787 cx.executor().run_until_parked();
10788 cx.condition(|editor, _| editor.context_menu_visible())
10789 .await;
10790 cx.update_editor(|editor, window, cx| {
10791 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10792 {
10793 assert_eq!(
10794 completion_menu_entries(&menu),
10795 &["let"],
10796 "With no digits in the completion query, no digits should be in the word completions"
10797 );
10798 } else {
10799 panic!("expected completion menu to be open");
10800 }
10801 editor.cancel(&Cancel, window, cx);
10802 });
10803
10804 cx.set_state(indoc! {"3ˇ
10805 0_usize
10806 let
10807 3
10808 33.35f32
10809 "});
10810 cx.update_editor(|editor, window, cx| {
10811 editor.show_completions(&ShowCompletions::default(), window, cx);
10812 });
10813 cx.executor().run_until_parked();
10814 cx.condition(|editor, _| editor.context_menu_visible())
10815 .await;
10816 cx.update_editor(|editor, _, _| {
10817 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10818 {
10819 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
10820 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
10821 } else {
10822 panic!("expected completion menu to be open");
10823 }
10824 });
10825}
10826
10827fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
10828 let position = || lsp::Position {
10829 line: params.text_document_position.position.line,
10830 character: params.text_document_position.position.character,
10831 };
10832 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10833 range: lsp::Range {
10834 start: position(),
10835 end: position(),
10836 },
10837 new_text: text.to_string(),
10838 }))
10839}
10840
10841#[gpui::test]
10842async fn test_multiline_completion(cx: &mut TestAppContext) {
10843 init_test(cx, |_| {});
10844
10845 let fs = FakeFs::new(cx.executor());
10846 fs.insert_tree(
10847 path!("/a"),
10848 json!({
10849 "main.ts": "a",
10850 }),
10851 )
10852 .await;
10853
10854 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10855 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10856 let typescript_language = Arc::new(Language::new(
10857 LanguageConfig {
10858 name: "TypeScript".into(),
10859 matcher: LanguageMatcher {
10860 path_suffixes: vec!["ts".to_string()],
10861 ..LanguageMatcher::default()
10862 },
10863 line_comments: vec!["// ".into()],
10864 ..LanguageConfig::default()
10865 },
10866 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10867 ));
10868 language_registry.add(typescript_language.clone());
10869 let mut fake_servers = language_registry.register_fake_lsp(
10870 "TypeScript",
10871 FakeLspAdapter {
10872 capabilities: lsp::ServerCapabilities {
10873 completion_provider: Some(lsp::CompletionOptions {
10874 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10875 ..lsp::CompletionOptions::default()
10876 }),
10877 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10878 ..lsp::ServerCapabilities::default()
10879 },
10880 // Emulate vtsls label generation
10881 label_for_completion: Some(Box::new(|item, _| {
10882 let text = if let Some(description) = item
10883 .label_details
10884 .as_ref()
10885 .and_then(|label_details| label_details.description.as_ref())
10886 {
10887 format!("{} {}", item.label, description)
10888 } else if let Some(detail) = &item.detail {
10889 format!("{} {}", item.label, detail)
10890 } else {
10891 item.label.clone()
10892 };
10893 let len = text.len();
10894 Some(language::CodeLabel {
10895 text,
10896 runs: Vec::new(),
10897 filter_range: 0..len,
10898 })
10899 })),
10900 ..FakeLspAdapter::default()
10901 },
10902 );
10903 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10904 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10905 let worktree_id = workspace
10906 .update(cx, |workspace, _window, cx| {
10907 workspace.project().update(cx, |project, cx| {
10908 project.worktrees(cx).next().unwrap().read(cx).id()
10909 })
10910 })
10911 .unwrap();
10912 let _buffer = project
10913 .update(cx, |project, cx| {
10914 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
10915 })
10916 .await
10917 .unwrap();
10918 let editor = workspace
10919 .update(cx, |workspace, window, cx| {
10920 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
10921 })
10922 .unwrap()
10923 .await
10924 .unwrap()
10925 .downcast::<Editor>()
10926 .unwrap();
10927 let fake_server = fake_servers.next().await.unwrap();
10928
10929 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
10930 let multiline_label_2 = "a\nb\nc\n";
10931 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
10932 let multiline_description = "d\ne\nf\n";
10933 let multiline_detail_2 = "g\nh\ni\n";
10934
10935 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
10936 move |params, _| async move {
10937 Ok(Some(lsp::CompletionResponse::Array(vec![
10938 lsp::CompletionItem {
10939 label: multiline_label.to_string(),
10940 text_edit: gen_text_edit(¶ms, "new_text_1"),
10941 ..lsp::CompletionItem::default()
10942 },
10943 lsp::CompletionItem {
10944 label: "single line label 1".to_string(),
10945 detail: Some(multiline_detail.to_string()),
10946 text_edit: gen_text_edit(¶ms, "new_text_2"),
10947 ..lsp::CompletionItem::default()
10948 },
10949 lsp::CompletionItem {
10950 label: "single line label 2".to_string(),
10951 label_details: Some(lsp::CompletionItemLabelDetails {
10952 description: Some(multiline_description.to_string()),
10953 detail: None,
10954 }),
10955 text_edit: gen_text_edit(¶ms, "new_text_2"),
10956 ..lsp::CompletionItem::default()
10957 },
10958 lsp::CompletionItem {
10959 label: multiline_label_2.to_string(),
10960 detail: Some(multiline_detail_2.to_string()),
10961 text_edit: gen_text_edit(¶ms, "new_text_3"),
10962 ..lsp::CompletionItem::default()
10963 },
10964 lsp::CompletionItem {
10965 label: "Label with many spaces and \t but without newlines".to_string(),
10966 detail: Some(
10967 "Details with many spaces and \t but without newlines".to_string(),
10968 ),
10969 text_edit: gen_text_edit(¶ms, "new_text_4"),
10970 ..lsp::CompletionItem::default()
10971 },
10972 ])))
10973 },
10974 );
10975
10976 editor.update_in(cx, |editor, window, cx| {
10977 cx.focus_self(window);
10978 editor.move_to_end(&MoveToEnd, window, cx);
10979 editor.handle_input(".", window, cx);
10980 });
10981 cx.run_until_parked();
10982 completion_handle.next().await.unwrap();
10983
10984 editor.update(cx, |editor, _| {
10985 assert!(editor.context_menu_visible());
10986 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10987 {
10988 let completion_labels = menu
10989 .completions
10990 .borrow()
10991 .iter()
10992 .map(|c| c.label.text.clone())
10993 .collect::<Vec<_>>();
10994 assert_eq!(
10995 completion_labels,
10996 &[
10997 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
10998 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
10999 "single line label 2 d e f ",
11000 "a b c g h i ",
11001 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11002 ],
11003 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11004 );
11005
11006 for completion in menu
11007 .completions
11008 .borrow()
11009 .iter() {
11010 assert_eq!(
11011 completion.label.filter_range,
11012 0..completion.label.text.len(),
11013 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11014 );
11015 }
11016 } else {
11017 panic!("expected completion menu to be open");
11018 }
11019 });
11020}
11021
11022#[gpui::test]
11023async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11024 init_test(cx, |_| {});
11025 let mut cx = EditorLspTestContext::new_rust(
11026 lsp::ServerCapabilities {
11027 completion_provider: Some(lsp::CompletionOptions {
11028 trigger_characters: Some(vec![".".to_string()]),
11029 ..Default::default()
11030 }),
11031 ..Default::default()
11032 },
11033 cx,
11034 )
11035 .await;
11036 cx.lsp
11037 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11038 Ok(Some(lsp::CompletionResponse::Array(vec![
11039 lsp::CompletionItem {
11040 label: "first".into(),
11041 ..Default::default()
11042 },
11043 lsp::CompletionItem {
11044 label: "last".into(),
11045 ..Default::default()
11046 },
11047 ])))
11048 });
11049 cx.set_state("variableˇ");
11050 cx.simulate_keystroke(".");
11051 cx.executor().run_until_parked();
11052
11053 cx.update_editor(|editor, _, _| {
11054 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11055 {
11056 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11057 } else {
11058 panic!("expected completion menu to be open");
11059 }
11060 });
11061
11062 cx.update_editor(|editor, window, cx| {
11063 editor.move_page_down(&MovePageDown::default(), window, cx);
11064 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11065 {
11066 assert!(
11067 menu.selected_item == 1,
11068 "expected PageDown to select the last item from the context menu"
11069 );
11070 } else {
11071 panic!("expected completion menu to stay open after PageDown");
11072 }
11073 });
11074
11075 cx.update_editor(|editor, window, cx| {
11076 editor.move_page_up(&MovePageUp::default(), window, cx);
11077 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11078 {
11079 assert!(
11080 menu.selected_item == 0,
11081 "expected PageUp to select the first item from the context menu"
11082 );
11083 } else {
11084 panic!("expected completion menu to stay open after PageUp");
11085 }
11086 });
11087}
11088
11089#[gpui::test]
11090async fn test_completion_sort(cx: &mut TestAppContext) {
11091 init_test(cx, |_| {});
11092 let mut cx = EditorLspTestContext::new_rust(
11093 lsp::ServerCapabilities {
11094 completion_provider: Some(lsp::CompletionOptions {
11095 trigger_characters: Some(vec![".".to_string()]),
11096 ..Default::default()
11097 }),
11098 ..Default::default()
11099 },
11100 cx,
11101 )
11102 .await;
11103 cx.lsp
11104 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11105 Ok(Some(lsp::CompletionResponse::Array(vec![
11106 lsp::CompletionItem {
11107 label: "Range".into(),
11108 sort_text: Some("a".into()),
11109 ..Default::default()
11110 },
11111 lsp::CompletionItem {
11112 label: "r".into(),
11113 sort_text: Some("b".into()),
11114 ..Default::default()
11115 },
11116 lsp::CompletionItem {
11117 label: "ret".into(),
11118 sort_text: Some("c".into()),
11119 ..Default::default()
11120 },
11121 lsp::CompletionItem {
11122 label: "return".into(),
11123 sort_text: Some("d".into()),
11124 ..Default::default()
11125 },
11126 lsp::CompletionItem {
11127 label: "slice".into(),
11128 sort_text: Some("d".into()),
11129 ..Default::default()
11130 },
11131 ])))
11132 });
11133 cx.set_state("rˇ");
11134 cx.executor().run_until_parked();
11135 cx.update_editor(|editor, window, cx| {
11136 editor.show_completions(
11137 &ShowCompletions {
11138 trigger: Some("r".into()),
11139 },
11140 window,
11141 cx,
11142 );
11143 });
11144 cx.executor().run_until_parked();
11145
11146 cx.update_editor(|editor, _, _| {
11147 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11148 {
11149 assert_eq!(
11150 completion_menu_entries(&menu),
11151 &["r", "ret", "Range", "return"]
11152 );
11153 } else {
11154 panic!("expected completion menu to be open");
11155 }
11156 });
11157}
11158
11159#[gpui::test]
11160async fn test_as_is_completions(cx: &mut TestAppContext) {
11161 init_test(cx, |_| {});
11162 let mut cx = EditorLspTestContext::new_rust(
11163 lsp::ServerCapabilities {
11164 completion_provider: Some(lsp::CompletionOptions {
11165 ..Default::default()
11166 }),
11167 ..Default::default()
11168 },
11169 cx,
11170 )
11171 .await;
11172 cx.lsp
11173 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11174 Ok(Some(lsp::CompletionResponse::Array(vec![
11175 lsp::CompletionItem {
11176 label: "unsafe".into(),
11177 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11178 range: lsp::Range {
11179 start: lsp::Position {
11180 line: 1,
11181 character: 2,
11182 },
11183 end: lsp::Position {
11184 line: 1,
11185 character: 3,
11186 },
11187 },
11188 new_text: "unsafe".to_string(),
11189 })),
11190 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11191 ..Default::default()
11192 },
11193 ])))
11194 });
11195 cx.set_state("fn a() {}\n nˇ");
11196 cx.executor().run_until_parked();
11197 cx.update_editor(|editor, window, cx| {
11198 editor.show_completions(
11199 &ShowCompletions {
11200 trigger: Some("\n".into()),
11201 },
11202 window,
11203 cx,
11204 );
11205 });
11206 cx.executor().run_until_parked();
11207
11208 cx.update_editor(|editor, window, cx| {
11209 editor.confirm_completion(&Default::default(), window, cx)
11210 });
11211 cx.executor().run_until_parked();
11212 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11213}
11214
11215#[gpui::test]
11216async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11217 init_test(cx, |_| {});
11218
11219 let mut cx = EditorLspTestContext::new_rust(
11220 lsp::ServerCapabilities {
11221 completion_provider: Some(lsp::CompletionOptions {
11222 trigger_characters: Some(vec![".".to_string()]),
11223 resolve_provider: Some(true),
11224 ..Default::default()
11225 }),
11226 ..Default::default()
11227 },
11228 cx,
11229 )
11230 .await;
11231
11232 cx.set_state("fn main() { let a = 2ˇ; }");
11233 cx.simulate_keystroke(".");
11234 let completion_item = lsp::CompletionItem {
11235 label: "Some".into(),
11236 kind: Some(lsp::CompletionItemKind::SNIPPET),
11237 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11238 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11239 kind: lsp::MarkupKind::Markdown,
11240 value: "```rust\nSome(2)\n```".to_string(),
11241 })),
11242 deprecated: Some(false),
11243 sort_text: Some("Some".to_string()),
11244 filter_text: Some("Some".to_string()),
11245 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11246 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11247 range: lsp::Range {
11248 start: lsp::Position {
11249 line: 0,
11250 character: 22,
11251 },
11252 end: lsp::Position {
11253 line: 0,
11254 character: 22,
11255 },
11256 },
11257 new_text: "Some(2)".to_string(),
11258 })),
11259 additional_text_edits: Some(vec![lsp::TextEdit {
11260 range: lsp::Range {
11261 start: lsp::Position {
11262 line: 0,
11263 character: 20,
11264 },
11265 end: lsp::Position {
11266 line: 0,
11267 character: 22,
11268 },
11269 },
11270 new_text: "".to_string(),
11271 }]),
11272 ..Default::default()
11273 };
11274
11275 let closure_completion_item = completion_item.clone();
11276 let counter = Arc::new(AtomicUsize::new(0));
11277 let counter_clone = counter.clone();
11278 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11279 let task_completion_item = closure_completion_item.clone();
11280 counter_clone.fetch_add(1, atomic::Ordering::Release);
11281 async move {
11282 Ok(Some(lsp::CompletionResponse::Array(vec![
11283 task_completion_item,
11284 ])))
11285 }
11286 });
11287
11288 cx.condition(|editor, _| editor.context_menu_visible())
11289 .await;
11290 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11291 assert!(request.next().await.is_some());
11292 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11293
11294 cx.simulate_keystrokes("S o m");
11295 cx.condition(|editor, _| editor.context_menu_visible())
11296 .await;
11297 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11298 assert!(request.next().await.is_some());
11299 assert!(request.next().await.is_some());
11300 assert!(request.next().await.is_some());
11301 request.close();
11302 assert!(request.next().await.is_none());
11303 assert_eq!(
11304 counter.load(atomic::Ordering::Acquire),
11305 4,
11306 "With the completions menu open, only one LSP request should happen per input"
11307 );
11308}
11309
11310#[gpui::test]
11311async fn test_toggle_comment(cx: &mut TestAppContext) {
11312 init_test(cx, |_| {});
11313 let mut cx = EditorTestContext::new(cx).await;
11314 let language = Arc::new(Language::new(
11315 LanguageConfig {
11316 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11317 ..Default::default()
11318 },
11319 Some(tree_sitter_rust::LANGUAGE.into()),
11320 ));
11321 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11322
11323 // If multiple selections intersect a line, the line is only toggled once.
11324 cx.set_state(indoc! {"
11325 fn a() {
11326 «//b();
11327 ˇ»// «c();
11328 //ˇ» d();
11329 }
11330 "});
11331
11332 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11333
11334 cx.assert_editor_state(indoc! {"
11335 fn a() {
11336 «b();
11337 c();
11338 ˇ» d();
11339 }
11340 "});
11341
11342 // The comment prefix is inserted at the same column for every line in a
11343 // selection.
11344 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11345
11346 cx.assert_editor_state(indoc! {"
11347 fn a() {
11348 // «b();
11349 // c();
11350 ˇ»// d();
11351 }
11352 "});
11353
11354 // If a selection ends at the beginning of a line, that line is not toggled.
11355 cx.set_selections_state(indoc! {"
11356 fn a() {
11357 // b();
11358 «// c();
11359 ˇ» // d();
11360 }
11361 "});
11362
11363 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11364
11365 cx.assert_editor_state(indoc! {"
11366 fn a() {
11367 // b();
11368 «c();
11369 ˇ» // d();
11370 }
11371 "});
11372
11373 // If a selection span a single line and is empty, the line is toggled.
11374 cx.set_state(indoc! {"
11375 fn a() {
11376 a();
11377 b();
11378 ˇ
11379 }
11380 "});
11381
11382 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11383
11384 cx.assert_editor_state(indoc! {"
11385 fn a() {
11386 a();
11387 b();
11388 //•ˇ
11389 }
11390 "});
11391
11392 // If a selection span multiple lines, empty lines are not toggled.
11393 cx.set_state(indoc! {"
11394 fn a() {
11395 «a();
11396
11397 c();ˇ»
11398 }
11399 "});
11400
11401 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11402
11403 cx.assert_editor_state(indoc! {"
11404 fn a() {
11405 // «a();
11406
11407 // c();ˇ»
11408 }
11409 "});
11410
11411 // If a selection includes multiple comment prefixes, all lines are uncommented.
11412 cx.set_state(indoc! {"
11413 fn a() {
11414 «// a();
11415 /// b();
11416 //! c();ˇ»
11417 }
11418 "});
11419
11420 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11421
11422 cx.assert_editor_state(indoc! {"
11423 fn a() {
11424 «a();
11425 b();
11426 c();ˇ»
11427 }
11428 "});
11429}
11430
11431#[gpui::test]
11432async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11433 init_test(cx, |_| {});
11434 let mut cx = EditorTestContext::new(cx).await;
11435 let language = Arc::new(Language::new(
11436 LanguageConfig {
11437 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11438 ..Default::default()
11439 },
11440 Some(tree_sitter_rust::LANGUAGE.into()),
11441 ));
11442 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11443
11444 let toggle_comments = &ToggleComments {
11445 advance_downwards: false,
11446 ignore_indent: true,
11447 };
11448
11449 // If multiple selections intersect a line, the line is only toggled once.
11450 cx.set_state(indoc! {"
11451 fn a() {
11452 // «b();
11453 // c();
11454 // ˇ» d();
11455 }
11456 "});
11457
11458 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11459
11460 cx.assert_editor_state(indoc! {"
11461 fn a() {
11462 «b();
11463 c();
11464 ˇ» d();
11465 }
11466 "});
11467
11468 // The comment prefix is inserted at the beginning of each line
11469 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11470
11471 cx.assert_editor_state(indoc! {"
11472 fn a() {
11473 // «b();
11474 // c();
11475 // ˇ» d();
11476 }
11477 "});
11478
11479 // If a selection ends at the beginning of a line, that line is not toggled.
11480 cx.set_selections_state(indoc! {"
11481 fn a() {
11482 // b();
11483 // «c();
11484 ˇ»// d();
11485 }
11486 "});
11487
11488 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11489
11490 cx.assert_editor_state(indoc! {"
11491 fn a() {
11492 // b();
11493 «c();
11494 ˇ»// d();
11495 }
11496 "});
11497
11498 // If a selection span a single line and is empty, the line is toggled.
11499 cx.set_state(indoc! {"
11500 fn a() {
11501 a();
11502 b();
11503 ˇ
11504 }
11505 "});
11506
11507 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11508
11509 cx.assert_editor_state(indoc! {"
11510 fn a() {
11511 a();
11512 b();
11513 //ˇ
11514 }
11515 "});
11516
11517 // If a selection span multiple lines, empty lines are not toggled.
11518 cx.set_state(indoc! {"
11519 fn a() {
11520 «a();
11521
11522 c();ˇ»
11523 }
11524 "});
11525
11526 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11527
11528 cx.assert_editor_state(indoc! {"
11529 fn a() {
11530 // «a();
11531
11532 // c();ˇ»
11533 }
11534 "});
11535
11536 // If a selection includes multiple comment prefixes, all lines are uncommented.
11537 cx.set_state(indoc! {"
11538 fn a() {
11539 // «a();
11540 /// b();
11541 //! c();ˇ»
11542 }
11543 "});
11544
11545 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11546
11547 cx.assert_editor_state(indoc! {"
11548 fn a() {
11549 «a();
11550 b();
11551 c();ˇ»
11552 }
11553 "});
11554}
11555
11556#[gpui::test]
11557async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
11558 init_test(cx, |_| {});
11559
11560 let language = Arc::new(Language::new(
11561 LanguageConfig {
11562 line_comments: vec!["// ".into()],
11563 ..Default::default()
11564 },
11565 Some(tree_sitter_rust::LANGUAGE.into()),
11566 ));
11567
11568 let mut cx = EditorTestContext::new(cx).await;
11569
11570 cx.language_registry().add(language.clone());
11571 cx.update_buffer(|buffer, cx| {
11572 buffer.set_language(Some(language), cx);
11573 });
11574
11575 let toggle_comments = &ToggleComments {
11576 advance_downwards: true,
11577 ignore_indent: false,
11578 };
11579
11580 // Single cursor on one line -> advance
11581 // Cursor moves horizontally 3 characters as well on non-blank line
11582 cx.set_state(indoc!(
11583 "fn a() {
11584 ˇdog();
11585 cat();
11586 }"
11587 ));
11588 cx.update_editor(|editor, window, cx| {
11589 editor.toggle_comments(toggle_comments, window, cx);
11590 });
11591 cx.assert_editor_state(indoc!(
11592 "fn a() {
11593 // dog();
11594 catˇ();
11595 }"
11596 ));
11597
11598 // Single selection on one line -> don't advance
11599 cx.set_state(indoc!(
11600 "fn a() {
11601 «dog()ˇ»;
11602 cat();
11603 }"
11604 ));
11605 cx.update_editor(|editor, window, cx| {
11606 editor.toggle_comments(toggle_comments, window, cx);
11607 });
11608 cx.assert_editor_state(indoc!(
11609 "fn a() {
11610 // «dog()ˇ»;
11611 cat();
11612 }"
11613 ));
11614
11615 // Multiple cursors on one line -> advance
11616 cx.set_state(indoc!(
11617 "fn a() {
11618 ˇdˇog();
11619 cat();
11620 }"
11621 ));
11622 cx.update_editor(|editor, window, cx| {
11623 editor.toggle_comments(toggle_comments, window, cx);
11624 });
11625 cx.assert_editor_state(indoc!(
11626 "fn a() {
11627 // dog();
11628 catˇ(ˇ);
11629 }"
11630 ));
11631
11632 // Multiple cursors on one line, with selection -> don't advance
11633 cx.set_state(indoc!(
11634 "fn a() {
11635 ˇdˇog«()ˇ»;
11636 cat();
11637 }"
11638 ));
11639 cx.update_editor(|editor, window, cx| {
11640 editor.toggle_comments(toggle_comments, window, cx);
11641 });
11642 cx.assert_editor_state(indoc!(
11643 "fn a() {
11644 // ˇdˇog«()ˇ»;
11645 cat();
11646 }"
11647 ));
11648
11649 // Single cursor on one line -> advance
11650 // Cursor moves to column 0 on blank line
11651 cx.set_state(indoc!(
11652 "fn a() {
11653 ˇdog();
11654
11655 cat();
11656 }"
11657 ));
11658 cx.update_editor(|editor, window, cx| {
11659 editor.toggle_comments(toggle_comments, window, cx);
11660 });
11661 cx.assert_editor_state(indoc!(
11662 "fn a() {
11663 // dog();
11664 ˇ
11665 cat();
11666 }"
11667 ));
11668
11669 // Single cursor on one line -> advance
11670 // Cursor starts and ends at column 0
11671 cx.set_state(indoc!(
11672 "fn a() {
11673 ˇ dog();
11674 cat();
11675 }"
11676 ));
11677 cx.update_editor(|editor, window, cx| {
11678 editor.toggle_comments(toggle_comments, window, cx);
11679 });
11680 cx.assert_editor_state(indoc!(
11681 "fn a() {
11682 // dog();
11683 ˇ cat();
11684 }"
11685 ));
11686}
11687
11688#[gpui::test]
11689async fn test_toggle_block_comment(cx: &mut TestAppContext) {
11690 init_test(cx, |_| {});
11691
11692 let mut cx = EditorTestContext::new(cx).await;
11693
11694 let html_language = Arc::new(
11695 Language::new(
11696 LanguageConfig {
11697 name: "HTML".into(),
11698 block_comment: Some(("<!-- ".into(), " -->".into())),
11699 ..Default::default()
11700 },
11701 Some(tree_sitter_html::LANGUAGE.into()),
11702 )
11703 .with_injection_query(
11704 r#"
11705 (script_element
11706 (raw_text) @injection.content
11707 (#set! injection.language "javascript"))
11708 "#,
11709 )
11710 .unwrap(),
11711 );
11712
11713 let javascript_language = Arc::new(Language::new(
11714 LanguageConfig {
11715 name: "JavaScript".into(),
11716 line_comments: vec!["// ".into()],
11717 ..Default::default()
11718 },
11719 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
11720 ));
11721
11722 cx.language_registry().add(html_language.clone());
11723 cx.language_registry().add(javascript_language.clone());
11724 cx.update_buffer(|buffer, cx| {
11725 buffer.set_language(Some(html_language), cx);
11726 });
11727
11728 // Toggle comments for empty selections
11729 cx.set_state(
11730 &r#"
11731 <p>A</p>ˇ
11732 <p>B</p>ˇ
11733 <p>C</p>ˇ
11734 "#
11735 .unindent(),
11736 );
11737 cx.update_editor(|editor, window, cx| {
11738 editor.toggle_comments(&ToggleComments::default(), window, cx)
11739 });
11740 cx.assert_editor_state(
11741 &r#"
11742 <!-- <p>A</p>ˇ -->
11743 <!-- <p>B</p>ˇ -->
11744 <!-- <p>C</p>ˇ -->
11745 "#
11746 .unindent(),
11747 );
11748 cx.update_editor(|editor, window, cx| {
11749 editor.toggle_comments(&ToggleComments::default(), window, cx)
11750 });
11751 cx.assert_editor_state(
11752 &r#"
11753 <p>A</p>ˇ
11754 <p>B</p>ˇ
11755 <p>C</p>ˇ
11756 "#
11757 .unindent(),
11758 );
11759
11760 // Toggle comments for mixture of empty and non-empty selections, where
11761 // multiple selections occupy a given line.
11762 cx.set_state(
11763 &r#"
11764 <p>A«</p>
11765 <p>ˇ»B</p>ˇ
11766 <p>C«</p>
11767 <p>ˇ»D</p>ˇ
11768 "#
11769 .unindent(),
11770 );
11771
11772 cx.update_editor(|editor, window, cx| {
11773 editor.toggle_comments(&ToggleComments::default(), window, cx)
11774 });
11775 cx.assert_editor_state(
11776 &r#"
11777 <!-- <p>A«</p>
11778 <p>ˇ»B</p>ˇ -->
11779 <!-- <p>C«</p>
11780 <p>ˇ»D</p>ˇ -->
11781 "#
11782 .unindent(),
11783 );
11784 cx.update_editor(|editor, window, cx| {
11785 editor.toggle_comments(&ToggleComments::default(), window, cx)
11786 });
11787 cx.assert_editor_state(
11788 &r#"
11789 <p>A«</p>
11790 <p>ˇ»B</p>ˇ
11791 <p>C«</p>
11792 <p>ˇ»D</p>ˇ
11793 "#
11794 .unindent(),
11795 );
11796
11797 // Toggle comments when different languages are active for different
11798 // selections.
11799 cx.set_state(
11800 &r#"
11801 ˇ<script>
11802 ˇvar x = new Y();
11803 ˇ</script>
11804 "#
11805 .unindent(),
11806 );
11807 cx.executor().run_until_parked();
11808 cx.update_editor(|editor, window, cx| {
11809 editor.toggle_comments(&ToggleComments::default(), window, cx)
11810 });
11811 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
11812 // Uncommenting and commenting from this position brings in even more wrong artifacts.
11813 cx.assert_editor_state(
11814 &r#"
11815 <!-- ˇ<script> -->
11816 // ˇvar x = new Y();
11817 <!-- ˇ</script> -->
11818 "#
11819 .unindent(),
11820 );
11821}
11822
11823#[gpui::test]
11824fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
11825 init_test(cx, |_| {});
11826
11827 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11828 let multibuffer = cx.new(|cx| {
11829 let mut multibuffer = MultiBuffer::new(ReadWrite);
11830 multibuffer.push_excerpts(
11831 buffer.clone(),
11832 [
11833 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
11834 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
11835 ],
11836 cx,
11837 );
11838 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
11839 multibuffer
11840 });
11841
11842 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11843 editor.update_in(cx, |editor, window, cx| {
11844 assert_eq!(editor.text(cx), "aaaa\nbbbb");
11845 editor.change_selections(None, window, cx, |s| {
11846 s.select_ranges([
11847 Point::new(0, 0)..Point::new(0, 0),
11848 Point::new(1, 0)..Point::new(1, 0),
11849 ])
11850 });
11851
11852 editor.handle_input("X", window, cx);
11853 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
11854 assert_eq!(
11855 editor.selections.ranges(cx),
11856 [
11857 Point::new(0, 1)..Point::new(0, 1),
11858 Point::new(1, 1)..Point::new(1, 1),
11859 ]
11860 );
11861
11862 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
11863 editor.change_selections(None, window, cx, |s| {
11864 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
11865 });
11866 editor.backspace(&Default::default(), window, cx);
11867 assert_eq!(editor.text(cx), "Xa\nbbb");
11868 assert_eq!(
11869 editor.selections.ranges(cx),
11870 [Point::new(1, 0)..Point::new(1, 0)]
11871 );
11872
11873 editor.change_selections(None, window, cx, |s| {
11874 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
11875 });
11876 editor.backspace(&Default::default(), window, cx);
11877 assert_eq!(editor.text(cx), "X\nbb");
11878 assert_eq!(
11879 editor.selections.ranges(cx),
11880 [Point::new(0, 1)..Point::new(0, 1)]
11881 );
11882 });
11883}
11884
11885#[gpui::test]
11886fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
11887 init_test(cx, |_| {});
11888
11889 let markers = vec![('[', ']').into(), ('(', ')').into()];
11890 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
11891 indoc! {"
11892 [aaaa
11893 (bbbb]
11894 cccc)",
11895 },
11896 markers.clone(),
11897 );
11898 let excerpt_ranges = markers.into_iter().map(|marker| {
11899 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
11900 ExcerptRange::new(context.clone())
11901 });
11902 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
11903 let multibuffer = cx.new(|cx| {
11904 let mut multibuffer = MultiBuffer::new(ReadWrite);
11905 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
11906 multibuffer
11907 });
11908
11909 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11910 editor.update_in(cx, |editor, window, cx| {
11911 let (expected_text, selection_ranges) = marked_text_ranges(
11912 indoc! {"
11913 aaaa
11914 bˇbbb
11915 bˇbbˇb
11916 cccc"
11917 },
11918 true,
11919 );
11920 assert_eq!(editor.text(cx), expected_text);
11921 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
11922
11923 editor.handle_input("X", window, cx);
11924
11925 let (expected_text, expected_selections) = marked_text_ranges(
11926 indoc! {"
11927 aaaa
11928 bXˇbbXb
11929 bXˇbbXˇb
11930 cccc"
11931 },
11932 false,
11933 );
11934 assert_eq!(editor.text(cx), expected_text);
11935 assert_eq!(editor.selections.ranges(cx), expected_selections);
11936
11937 editor.newline(&Newline, window, cx);
11938 let (expected_text, expected_selections) = marked_text_ranges(
11939 indoc! {"
11940 aaaa
11941 bX
11942 ˇbbX
11943 b
11944 bX
11945 ˇbbX
11946 ˇb
11947 cccc"
11948 },
11949 false,
11950 );
11951 assert_eq!(editor.text(cx), expected_text);
11952 assert_eq!(editor.selections.ranges(cx), expected_selections);
11953 });
11954}
11955
11956#[gpui::test]
11957fn test_refresh_selections(cx: &mut TestAppContext) {
11958 init_test(cx, |_| {});
11959
11960 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11961 let mut excerpt1_id = None;
11962 let multibuffer = cx.new(|cx| {
11963 let mut multibuffer = MultiBuffer::new(ReadWrite);
11964 excerpt1_id = multibuffer
11965 .push_excerpts(
11966 buffer.clone(),
11967 [
11968 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11969 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11970 ],
11971 cx,
11972 )
11973 .into_iter()
11974 .next();
11975 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11976 multibuffer
11977 });
11978
11979 let editor = cx.add_window(|window, cx| {
11980 let mut editor = build_editor(multibuffer.clone(), window, cx);
11981 let snapshot = editor.snapshot(window, cx);
11982 editor.change_selections(None, window, cx, |s| {
11983 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
11984 });
11985 editor.begin_selection(
11986 Point::new(2, 1).to_display_point(&snapshot),
11987 true,
11988 1,
11989 window,
11990 cx,
11991 );
11992 assert_eq!(
11993 editor.selections.ranges(cx),
11994 [
11995 Point::new(1, 3)..Point::new(1, 3),
11996 Point::new(2, 1)..Point::new(2, 1),
11997 ]
11998 );
11999 editor
12000 });
12001
12002 // Refreshing selections is a no-op when excerpts haven't changed.
12003 _ = editor.update(cx, |editor, window, cx| {
12004 editor.change_selections(None, window, cx, |s| s.refresh());
12005 assert_eq!(
12006 editor.selections.ranges(cx),
12007 [
12008 Point::new(1, 3)..Point::new(1, 3),
12009 Point::new(2, 1)..Point::new(2, 1),
12010 ]
12011 );
12012 });
12013
12014 multibuffer.update(cx, |multibuffer, cx| {
12015 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12016 });
12017 _ = editor.update(cx, |editor, window, cx| {
12018 // Removing an excerpt causes the first selection to become degenerate.
12019 assert_eq!(
12020 editor.selections.ranges(cx),
12021 [
12022 Point::new(0, 0)..Point::new(0, 0),
12023 Point::new(0, 1)..Point::new(0, 1)
12024 ]
12025 );
12026
12027 // Refreshing selections will relocate the first selection to the original buffer
12028 // location.
12029 editor.change_selections(None, window, cx, |s| s.refresh());
12030 assert_eq!(
12031 editor.selections.ranges(cx),
12032 [
12033 Point::new(0, 1)..Point::new(0, 1),
12034 Point::new(0, 3)..Point::new(0, 3)
12035 ]
12036 );
12037 assert!(editor.selections.pending_anchor().is_some());
12038 });
12039}
12040
12041#[gpui::test]
12042fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12043 init_test(cx, |_| {});
12044
12045 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12046 let mut excerpt1_id = None;
12047 let multibuffer = cx.new(|cx| {
12048 let mut multibuffer = MultiBuffer::new(ReadWrite);
12049 excerpt1_id = multibuffer
12050 .push_excerpts(
12051 buffer.clone(),
12052 [
12053 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12054 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12055 ],
12056 cx,
12057 )
12058 .into_iter()
12059 .next();
12060 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12061 multibuffer
12062 });
12063
12064 let editor = cx.add_window(|window, cx| {
12065 let mut editor = build_editor(multibuffer.clone(), window, cx);
12066 let snapshot = editor.snapshot(window, cx);
12067 editor.begin_selection(
12068 Point::new(1, 3).to_display_point(&snapshot),
12069 false,
12070 1,
12071 window,
12072 cx,
12073 );
12074 assert_eq!(
12075 editor.selections.ranges(cx),
12076 [Point::new(1, 3)..Point::new(1, 3)]
12077 );
12078 editor
12079 });
12080
12081 multibuffer.update(cx, |multibuffer, cx| {
12082 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12083 });
12084 _ = editor.update(cx, |editor, window, cx| {
12085 assert_eq!(
12086 editor.selections.ranges(cx),
12087 [Point::new(0, 0)..Point::new(0, 0)]
12088 );
12089
12090 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12091 editor.change_selections(None, window, cx, |s| s.refresh());
12092 assert_eq!(
12093 editor.selections.ranges(cx),
12094 [Point::new(0, 3)..Point::new(0, 3)]
12095 );
12096 assert!(editor.selections.pending_anchor().is_some());
12097 });
12098}
12099
12100#[gpui::test]
12101async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12102 init_test(cx, |_| {});
12103
12104 let language = Arc::new(
12105 Language::new(
12106 LanguageConfig {
12107 brackets: BracketPairConfig {
12108 pairs: vec![
12109 BracketPair {
12110 start: "{".to_string(),
12111 end: "}".to_string(),
12112 close: true,
12113 surround: true,
12114 newline: true,
12115 },
12116 BracketPair {
12117 start: "/* ".to_string(),
12118 end: " */".to_string(),
12119 close: true,
12120 surround: true,
12121 newline: true,
12122 },
12123 ],
12124 ..Default::default()
12125 },
12126 ..Default::default()
12127 },
12128 Some(tree_sitter_rust::LANGUAGE.into()),
12129 )
12130 .with_indents_query("")
12131 .unwrap(),
12132 );
12133
12134 let text = concat!(
12135 "{ }\n", //
12136 " x\n", //
12137 " /* */\n", //
12138 "x\n", //
12139 "{{} }\n", //
12140 );
12141
12142 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12143 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12144 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12145 editor
12146 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12147 .await;
12148
12149 editor.update_in(cx, |editor, window, cx| {
12150 editor.change_selections(None, window, cx, |s| {
12151 s.select_display_ranges([
12152 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12153 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12154 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12155 ])
12156 });
12157 editor.newline(&Newline, window, cx);
12158
12159 assert_eq!(
12160 editor.buffer().read(cx).read(cx).text(),
12161 concat!(
12162 "{ \n", // Suppress rustfmt
12163 "\n", //
12164 "}\n", //
12165 " x\n", //
12166 " /* \n", //
12167 " \n", //
12168 " */\n", //
12169 "x\n", //
12170 "{{} \n", //
12171 "}\n", //
12172 )
12173 );
12174 });
12175}
12176
12177#[gpui::test]
12178fn test_highlighted_ranges(cx: &mut TestAppContext) {
12179 init_test(cx, |_| {});
12180
12181 let editor = cx.add_window(|window, cx| {
12182 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12183 build_editor(buffer.clone(), window, cx)
12184 });
12185
12186 _ = editor.update(cx, |editor, window, cx| {
12187 struct Type1;
12188 struct Type2;
12189
12190 let buffer = editor.buffer.read(cx).snapshot(cx);
12191
12192 let anchor_range =
12193 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12194
12195 editor.highlight_background::<Type1>(
12196 &[
12197 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12198 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12199 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12200 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12201 ],
12202 |_| Hsla::red(),
12203 cx,
12204 );
12205 editor.highlight_background::<Type2>(
12206 &[
12207 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12208 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12209 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12210 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12211 ],
12212 |_| Hsla::green(),
12213 cx,
12214 );
12215
12216 let snapshot = editor.snapshot(window, cx);
12217 let mut highlighted_ranges = editor.background_highlights_in_range(
12218 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12219 &snapshot,
12220 cx.theme().colors(),
12221 );
12222 // Enforce a consistent ordering based on color without relying on the ordering of the
12223 // highlight's `TypeId` which is non-executor.
12224 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12225 assert_eq!(
12226 highlighted_ranges,
12227 &[
12228 (
12229 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12230 Hsla::red(),
12231 ),
12232 (
12233 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12234 Hsla::red(),
12235 ),
12236 (
12237 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12238 Hsla::green(),
12239 ),
12240 (
12241 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12242 Hsla::green(),
12243 ),
12244 ]
12245 );
12246 assert_eq!(
12247 editor.background_highlights_in_range(
12248 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12249 &snapshot,
12250 cx.theme().colors(),
12251 ),
12252 &[(
12253 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12254 Hsla::red(),
12255 )]
12256 );
12257 });
12258}
12259
12260#[gpui::test]
12261async fn test_following(cx: &mut TestAppContext) {
12262 init_test(cx, |_| {});
12263
12264 let fs = FakeFs::new(cx.executor());
12265 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12266
12267 let buffer = project.update(cx, |project, cx| {
12268 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12269 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12270 });
12271 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12272 let follower = cx.update(|cx| {
12273 cx.open_window(
12274 WindowOptions {
12275 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12276 gpui::Point::new(px(0.), px(0.)),
12277 gpui::Point::new(px(10.), px(80.)),
12278 ))),
12279 ..Default::default()
12280 },
12281 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12282 )
12283 .unwrap()
12284 });
12285
12286 let is_still_following = Rc::new(RefCell::new(true));
12287 let follower_edit_event_count = Rc::new(RefCell::new(0));
12288 let pending_update = Rc::new(RefCell::new(None));
12289 let leader_entity = leader.root(cx).unwrap();
12290 let follower_entity = follower.root(cx).unwrap();
12291 _ = follower.update(cx, {
12292 let update = pending_update.clone();
12293 let is_still_following = is_still_following.clone();
12294 let follower_edit_event_count = follower_edit_event_count.clone();
12295 |_, window, cx| {
12296 cx.subscribe_in(
12297 &leader_entity,
12298 window,
12299 move |_, leader, event, window, cx| {
12300 leader.read(cx).add_event_to_update_proto(
12301 event,
12302 &mut update.borrow_mut(),
12303 window,
12304 cx,
12305 );
12306 },
12307 )
12308 .detach();
12309
12310 cx.subscribe_in(
12311 &follower_entity,
12312 window,
12313 move |_, _, event: &EditorEvent, _window, _cx| {
12314 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12315 *is_still_following.borrow_mut() = false;
12316 }
12317
12318 if let EditorEvent::BufferEdited = event {
12319 *follower_edit_event_count.borrow_mut() += 1;
12320 }
12321 },
12322 )
12323 .detach();
12324 }
12325 });
12326
12327 // Update the selections only
12328 _ = leader.update(cx, |leader, window, cx| {
12329 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12330 });
12331 follower
12332 .update(cx, |follower, window, cx| {
12333 follower.apply_update_proto(
12334 &project,
12335 pending_update.borrow_mut().take().unwrap(),
12336 window,
12337 cx,
12338 )
12339 })
12340 .unwrap()
12341 .await
12342 .unwrap();
12343 _ = follower.update(cx, |follower, _, cx| {
12344 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12345 });
12346 assert!(*is_still_following.borrow());
12347 assert_eq!(*follower_edit_event_count.borrow(), 0);
12348
12349 // Update the scroll position only
12350 _ = leader.update(cx, |leader, window, cx| {
12351 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12352 });
12353 follower
12354 .update(cx, |follower, window, cx| {
12355 follower.apply_update_proto(
12356 &project,
12357 pending_update.borrow_mut().take().unwrap(),
12358 window,
12359 cx,
12360 )
12361 })
12362 .unwrap()
12363 .await
12364 .unwrap();
12365 assert_eq!(
12366 follower
12367 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12368 .unwrap(),
12369 gpui::Point::new(1.5, 3.5)
12370 );
12371 assert!(*is_still_following.borrow());
12372 assert_eq!(*follower_edit_event_count.borrow(), 0);
12373
12374 // Update the selections and scroll position. The follower's scroll position is updated
12375 // via autoscroll, not via the leader's exact scroll position.
12376 _ = leader.update(cx, |leader, window, cx| {
12377 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12378 leader.request_autoscroll(Autoscroll::newest(), cx);
12379 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12380 });
12381 follower
12382 .update(cx, |follower, window, cx| {
12383 follower.apply_update_proto(
12384 &project,
12385 pending_update.borrow_mut().take().unwrap(),
12386 window,
12387 cx,
12388 )
12389 })
12390 .unwrap()
12391 .await
12392 .unwrap();
12393 _ = follower.update(cx, |follower, _, cx| {
12394 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12395 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12396 });
12397 assert!(*is_still_following.borrow());
12398
12399 // Creating a pending selection that precedes another selection
12400 _ = leader.update(cx, |leader, window, cx| {
12401 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12402 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12403 });
12404 follower
12405 .update(cx, |follower, window, cx| {
12406 follower.apply_update_proto(
12407 &project,
12408 pending_update.borrow_mut().take().unwrap(),
12409 window,
12410 cx,
12411 )
12412 })
12413 .unwrap()
12414 .await
12415 .unwrap();
12416 _ = follower.update(cx, |follower, _, cx| {
12417 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12418 });
12419 assert!(*is_still_following.borrow());
12420
12421 // Extend the pending selection so that it surrounds another selection
12422 _ = leader.update(cx, |leader, window, cx| {
12423 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12424 });
12425 follower
12426 .update(cx, |follower, window, cx| {
12427 follower.apply_update_proto(
12428 &project,
12429 pending_update.borrow_mut().take().unwrap(),
12430 window,
12431 cx,
12432 )
12433 })
12434 .unwrap()
12435 .await
12436 .unwrap();
12437 _ = follower.update(cx, |follower, _, cx| {
12438 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12439 });
12440
12441 // Scrolling locally breaks the follow
12442 _ = follower.update(cx, |follower, window, cx| {
12443 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12444 follower.set_scroll_anchor(
12445 ScrollAnchor {
12446 anchor: top_anchor,
12447 offset: gpui::Point::new(0.0, 0.5),
12448 },
12449 window,
12450 cx,
12451 );
12452 });
12453 assert!(!(*is_still_following.borrow()));
12454}
12455
12456#[gpui::test]
12457async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12458 init_test(cx, |_| {});
12459
12460 let fs = FakeFs::new(cx.executor());
12461 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12462 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12463 let pane = workspace
12464 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12465 .unwrap();
12466
12467 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12468
12469 let leader = pane.update_in(cx, |_, window, cx| {
12470 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
12471 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
12472 });
12473
12474 // Start following the editor when it has no excerpts.
12475 let mut state_message =
12476 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12477 let workspace_entity = workspace.root(cx).unwrap();
12478 let follower_1 = cx
12479 .update_window(*workspace.deref(), |_, window, cx| {
12480 Editor::from_state_proto(
12481 workspace_entity,
12482 ViewId {
12483 creator: Default::default(),
12484 id: 0,
12485 },
12486 &mut state_message,
12487 window,
12488 cx,
12489 )
12490 })
12491 .unwrap()
12492 .unwrap()
12493 .await
12494 .unwrap();
12495
12496 let update_message = Rc::new(RefCell::new(None));
12497 follower_1.update_in(cx, {
12498 let update = update_message.clone();
12499 |_, window, cx| {
12500 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
12501 leader.read(cx).add_event_to_update_proto(
12502 event,
12503 &mut update.borrow_mut(),
12504 window,
12505 cx,
12506 );
12507 })
12508 .detach();
12509 }
12510 });
12511
12512 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
12513 (
12514 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
12515 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
12516 )
12517 });
12518
12519 // Insert some excerpts.
12520 leader.update(cx, |leader, cx| {
12521 leader.buffer.update(cx, |multibuffer, cx| {
12522 let excerpt_ids = multibuffer.push_excerpts(
12523 buffer_1.clone(),
12524 [
12525 ExcerptRange::new(1..6),
12526 ExcerptRange::new(12..15),
12527 ExcerptRange::new(0..3),
12528 ],
12529 cx,
12530 );
12531 multibuffer.insert_excerpts_after(
12532 excerpt_ids[0],
12533 buffer_2.clone(),
12534 [ExcerptRange::new(8..12), ExcerptRange::new(0..6)],
12535 cx,
12536 );
12537 });
12538 });
12539
12540 // Apply the update of adding the excerpts.
12541 follower_1
12542 .update_in(cx, |follower, window, cx| {
12543 follower.apply_update_proto(
12544 &project,
12545 update_message.borrow().clone().unwrap(),
12546 window,
12547 cx,
12548 )
12549 })
12550 .await
12551 .unwrap();
12552 assert_eq!(
12553 follower_1.update(cx, |editor, cx| editor.text(cx)),
12554 leader.update(cx, |editor, cx| editor.text(cx))
12555 );
12556 update_message.borrow_mut().take();
12557
12558 // Start following separately after it already has excerpts.
12559 let mut state_message =
12560 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12561 let workspace_entity = workspace.root(cx).unwrap();
12562 let follower_2 = cx
12563 .update_window(*workspace.deref(), |_, window, cx| {
12564 Editor::from_state_proto(
12565 workspace_entity,
12566 ViewId {
12567 creator: Default::default(),
12568 id: 0,
12569 },
12570 &mut state_message,
12571 window,
12572 cx,
12573 )
12574 })
12575 .unwrap()
12576 .unwrap()
12577 .await
12578 .unwrap();
12579 assert_eq!(
12580 follower_2.update(cx, |editor, cx| editor.text(cx)),
12581 leader.update(cx, |editor, cx| editor.text(cx))
12582 );
12583
12584 // Remove some excerpts.
12585 leader.update(cx, |leader, cx| {
12586 leader.buffer.update(cx, |multibuffer, cx| {
12587 let excerpt_ids = multibuffer.excerpt_ids();
12588 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
12589 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
12590 });
12591 });
12592
12593 // Apply the update of removing the excerpts.
12594 follower_1
12595 .update_in(cx, |follower, window, cx| {
12596 follower.apply_update_proto(
12597 &project,
12598 update_message.borrow().clone().unwrap(),
12599 window,
12600 cx,
12601 )
12602 })
12603 .await
12604 .unwrap();
12605 follower_2
12606 .update_in(cx, |follower, window, cx| {
12607 follower.apply_update_proto(
12608 &project,
12609 update_message.borrow().clone().unwrap(),
12610 window,
12611 cx,
12612 )
12613 })
12614 .await
12615 .unwrap();
12616 update_message.borrow_mut().take();
12617 assert_eq!(
12618 follower_1.update(cx, |editor, cx| editor.text(cx)),
12619 leader.update(cx, |editor, cx| editor.text(cx))
12620 );
12621}
12622
12623#[gpui::test]
12624async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12625 init_test(cx, |_| {});
12626
12627 let mut cx = EditorTestContext::new(cx).await;
12628 let lsp_store =
12629 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12630
12631 cx.set_state(indoc! {"
12632 ˇfn func(abc def: i32) -> u32 {
12633 }
12634 "});
12635
12636 cx.update(|_, cx| {
12637 lsp_store.update(cx, |lsp_store, cx| {
12638 lsp_store
12639 .update_diagnostics(
12640 LanguageServerId(0),
12641 lsp::PublishDiagnosticsParams {
12642 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12643 version: None,
12644 diagnostics: vec![
12645 lsp::Diagnostic {
12646 range: lsp::Range::new(
12647 lsp::Position::new(0, 11),
12648 lsp::Position::new(0, 12),
12649 ),
12650 severity: Some(lsp::DiagnosticSeverity::ERROR),
12651 ..Default::default()
12652 },
12653 lsp::Diagnostic {
12654 range: lsp::Range::new(
12655 lsp::Position::new(0, 12),
12656 lsp::Position::new(0, 15),
12657 ),
12658 severity: Some(lsp::DiagnosticSeverity::ERROR),
12659 ..Default::default()
12660 },
12661 lsp::Diagnostic {
12662 range: lsp::Range::new(
12663 lsp::Position::new(0, 25),
12664 lsp::Position::new(0, 28),
12665 ),
12666 severity: Some(lsp::DiagnosticSeverity::ERROR),
12667 ..Default::default()
12668 },
12669 ],
12670 },
12671 &[],
12672 cx,
12673 )
12674 .unwrap()
12675 });
12676 });
12677
12678 executor.run_until_parked();
12679
12680 cx.update_editor(|editor, window, cx| {
12681 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12682 });
12683
12684 cx.assert_editor_state(indoc! {"
12685 fn func(abc def: i32) -> ˇu32 {
12686 }
12687 "});
12688
12689 cx.update_editor(|editor, window, cx| {
12690 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12691 });
12692
12693 cx.assert_editor_state(indoc! {"
12694 fn func(abc ˇdef: i32) -> u32 {
12695 }
12696 "});
12697
12698 cx.update_editor(|editor, window, cx| {
12699 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12700 });
12701
12702 cx.assert_editor_state(indoc! {"
12703 fn func(abcˇ def: i32) -> u32 {
12704 }
12705 "});
12706
12707 cx.update_editor(|editor, window, cx| {
12708 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12709 });
12710
12711 cx.assert_editor_state(indoc! {"
12712 fn func(abc def: i32) -> ˇu32 {
12713 }
12714 "});
12715}
12716
12717#[gpui::test]
12718async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
12719 init_test(cx, |_| {});
12720
12721 let mut cx = EditorTestContext::new(cx).await;
12722
12723 cx.set_state(indoc! {"
12724 fn func(abˇc def: i32) -> u32 {
12725 }
12726 "});
12727 let lsp_store =
12728 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12729
12730 cx.update(|_, cx| {
12731 lsp_store.update(cx, |lsp_store, cx| {
12732 lsp_store.update_diagnostics(
12733 LanguageServerId(0),
12734 lsp::PublishDiagnosticsParams {
12735 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12736 version: None,
12737 diagnostics: vec![lsp::Diagnostic {
12738 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
12739 severity: Some(lsp::DiagnosticSeverity::ERROR),
12740 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
12741 ..Default::default()
12742 }],
12743 },
12744 &[],
12745 cx,
12746 )
12747 })
12748 }).unwrap();
12749 cx.run_until_parked();
12750 cx.update_editor(|editor, window, cx| {
12751 hover_popover::hover(editor, &Default::default(), window, cx)
12752 });
12753 cx.run_until_parked();
12754 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
12755}
12756
12757#[gpui::test]
12758async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12759 init_test(cx, |_| {});
12760
12761 let mut cx = EditorTestContext::new(cx).await;
12762
12763 let diff_base = r#"
12764 use some::mod;
12765
12766 const A: u32 = 42;
12767
12768 fn main() {
12769 println!("hello");
12770
12771 println!("world");
12772 }
12773 "#
12774 .unindent();
12775
12776 // Edits are modified, removed, modified, added
12777 cx.set_state(
12778 &r#"
12779 use some::modified;
12780
12781 ˇ
12782 fn main() {
12783 println!("hello there");
12784
12785 println!("around the");
12786 println!("world");
12787 }
12788 "#
12789 .unindent(),
12790 );
12791
12792 cx.set_head_text(&diff_base);
12793 executor.run_until_parked();
12794
12795 cx.update_editor(|editor, window, cx| {
12796 //Wrap around the bottom of the buffer
12797 for _ in 0..3 {
12798 editor.go_to_next_hunk(&GoToHunk, window, cx);
12799 }
12800 });
12801
12802 cx.assert_editor_state(
12803 &r#"
12804 ˇuse some::modified;
12805
12806
12807 fn main() {
12808 println!("hello there");
12809
12810 println!("around the");
12811 println!("world");
12812 }
12813 "#
12814 .unindent(),
12815 );
12816
12817 cx.update_editor(|editor, window, cx| {
12818 //Wrap around the top of the buffer
12819 for _ in 0..2 {
12820 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12821 }
12822 });
12823
12824 cx.assert_editor_state(
12825 &r#"
12826 use some::modified;
12827
12828
12829 fn main() {
12830 ˇ println!("hello there");
12831
12832 println!("around the");
12833 println!("world");
12834 }
12835 "#
12836 .unindent(),
12837 );
12838
12839 cx.update_editor(|editor, window, cx| {
12840 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12841 });
12842
12843 cx.assert_editor_state(
12844 &r#"
12845 use some::modified;
12846
12847 ˇ
12848 fn main() {
12849 println!("hello there");
12850
12851 println!("around the");
12852 println!("world");
12853 }
12854 "#
12855 .unindent(),
12856 );
12857
12858 cx.update_editor(|editor, window, cx| {
12859 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12860 });
12861
12862 cx.assert_editor_state(
12863 &r#"
12864 ˇuse some::modified;
12865
12866
12867 fn main() {
12868 println!("hello there");
12869
12870 println!("around the");
12871 println!("world");
12872 }
12873 "#
12874 .unindent(),
12875 );
12876
12877 cx.update_editor(|editor, window, cx| {
12878 for _ in 0..2 {
12879 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12880 }
12881 });
12882
12883 cx.assert_editor_state(
12884 &r#"
12885 use some::modified;
12886
12887
12888 fn main() {
12889 ˇ println!("hello there");
12890
12891 println!("around the");
12892 println!("world");
12893 }
12894 "#
12895 .unindent(),
12896 );
12897
12898 cx.update_editor(|editor, window, cx| {
12899 editor.fold(&Fold, window, cx);
12900 });
12901
12902 cx.update_editor(|editor, window, cx| {
12903 editor.go_to_next_hunk(&GoToHunk, window, cx);
12904 });
12905
12906 cx.assert_editor_state(
12907 &r#"
12908 ˇuse some::modified;
12909
12910
12911 fn main() {
12912 println!("hello there");
12913
12914 println!("around the");
12915 println!("world");
12916 }
12917 "#
12918 .unindent(),
12919 );
12920}
12921
12922#[test]
12923fn test_split_words() {
12924 fn split(text: &str) -> Vec<&str> {
12925 split_words(text).collect()
12926 }
12927
12928 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12929 assert_eq!(split("hello_world"), &["hello_", "world"]);
12930 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12931 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12932 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12933 assert_eq!(split("helloworld"), &["helloworld"]);
12934
12935 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12936}
12937
12938#[gpui::test]
12939async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12940 init_test(cx, |_| {});
12941
12942 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12943 let mut assert = |before, after| {
12944 let _state_context = cx.set_state(before);
12945 cx.run_until_parked();
12946 cx.update_editor(|editor, window, cx| {
12947 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12948 });
12949 cx.run_until_parked();
12950 cx.assert_editor_state(after);
12951 };
12952
12953 // Outside bracket jumps to outside of matching bracket
12954 assert("console.logˇ(var);", "console.log(var)ˇ;");
12955 assert("console.log(var)ˇ;", "console.logˇ(var);");
12956
12957 // Inside bracket jumps to inside of matching bracket
12958 assert("console.log(ˇvar);", "console.log(varˇ);");
12959 assert("console.log(varˇ);", "console.log(ˇvar);");
12960
12961 // When outside a bracket and inside, favor jumping to the inside bracket
12962 assert(
12963 "console.log('foo', [1, 2, 3]ˇ);",
12964 "console.log(ˇ'foo', [1, 2, 3]);",
12965 );
12966 assert(
12967 "console.log(ˇ'foo', [1, 2, 3]);",
12968 "console.log('foo', [1, 2, 3]ˇ);",
12969 );
12970
12971 // Bias forward if two options are equally likely
12972 assert(
12973 "let result = curried_fun()ˇ();",
12974 "let result = curried_fun()()ˇ;",
12975 );
12976
12977 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12978 assert(
12979 indoc! {"
12980 function test() {
12981 console.log('test')ˇ
12982 }"},
12983 indoc! {"
12984 function test() {
12985 console.logˇ('test')
12986 }"},
12987 );
12988}
12989
12990#[gpui::test]
12991async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12992 init_test(cx, |_| {});
12993
12994 let fs = FakeFs::new(cx.executor());
12995 fs.insert_tree(
12996 path!("/a"),
12997 json!({
12998 "main.rs": "fn main() { let a = 5; }",
12999 "other.rs": "// Test file",
13000 }),
13001 )
13002 .await;
13003 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13004
13005 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13006 language_registry.add(Arc::new(Language::new(
13007 LanguageConfig {
13008 name: "Rust".into(),
13009 matcher: LanguageMatcher {
13010 path_suffixes: vec!["rs".to_string()],
13011 ..Default::default()
13012 },
13013 brackets: BracketPairConfig {
13014 pairs: vec![BracketPair {
13015 start: "{".to_string(),
13016 end: "}".to_string(),
13017 close: true,
13018 surround: true,
13019 newline: true,
13020 }],
13021 disabled_scopes_by_bracket_ix: Vec::new(),
13022 },
13023 ..Default::default()
13024 },
13025 Some(tree_sitter_rust::LANGUAGE.into()),
13026 )));
13027 let mut fake_servers = language_registry.register_fake_lsp(
13028 "Rust",
13029 FakeLspAdapter {
13030 capabilities: lsp::ServerCapabilities {
13031 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13032 first_trigger_character: "{".to_string(),
13033 more_trigger_character: None,
13034 }),
13035 ..Default::default()
13036 },
13037 ..Default::default()
13038 },
13039 );
13040
13041 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13042
13043 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13044
13045 let worktree_id = workspace
13046 .update(cx, |workspace, _, cx| {
13047 workspace.project().update(cx, |project, cx| {
13048 project.worktrees(cx).next().unwrap().read(cx).id()
13049 })
13050 })
13051 .unwrap();
13052
13053 let buffer = project
13054 .update(cx, |project, cx| {
13055 project.open_local_buffer(path!("/a/main.rs"), cx)
13056 })
13057 .await
13058 .unwrap();
13059 let editor_handle = workspace
13060 .update(cx, |workspace, window, cx| {
13061 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13062 })
13063 .unwrap()
13064 .await
13065 .unwrap()
13066 .downcast::<Editor>()
13067 .unwrap();
13068
13069 cx.executor().start_waiting();
13070 let fake_server = fake_servers.next().await.unwrap();
13071
13072 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13073 |params, _| async move {
13074 assert_eq!(
13075 params.text_document_position.text_document.uri,
13076 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13077 );
13078 assert_eq!(
13079 params.text_document_position.position,
13080 lsp::Position::new(0, 21),
13081 );
13082
13083 Ok(Some(vec![lsp::TextEdit {
13084 new_text: "]".to_string(),
13085 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13086 }]))
13087 },
13088 );
13089
13090 editor_handle.update_in(cx, |editor, window, cx| {
13091 window.focus(&editor.focus_handle(cx));
13092 editor.change_selections(None, window, cx, |s| {
13093 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13094 });
13095 editor.handle_input("{", window, cx);
13096 });
13097
13098 cx.executor().run_until_parked();
13099
13100 buffer.update(cx, |buffer, _| {
13101 assert_eq!(
13102 buffer.text(),
13103 "fn main() { let a = {5}; }",
13104 "No extra braces from on type formatting should appear in the buffer"
13105 )
13106 });
13107}
13108
13109#[gpui::test]
13110async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13111 init_test(cx, |_| {});
13112
13113 let fs = FakeFs::new(cx.executor());
13114 fs.insert_tree(
13115 path!("/a"),
13116 json!({
13117 "main.rs": "fn main() { let a = 5; }",
13118 "other.rs": "// Test file",
13119 }),
13120 )
13121 .await;
13122
13123 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13124
13125 let server_restarts = Arc::new(AtomicUsize::new(0));
13126 let closure_restarts = Arc::clone(&server_restarts);
13127 let language_server_name = "test language server";
13128 let language_name: LanguageName = "Rust".into();
13129
13130 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13131 language_registry.add(Arc::new(Language::new(
13132 LanguageConfig {
13133 name: language_name.clone(),
13134 matcher: LanguageMatcher {
13135 path_suffixes: vec!["rs".to_string()],
13136 ..Default::default()
13137 },
13138 ..Default::default()
13139 },
13140 Some(tree_sitter_rust::LANGUAGE.into()),
13141 )));
13142 let mut fake_servers = language_registry.register_fake_lsp(
13143 "Rust",
13144 FakeLspAdapter {
13145 name: language_server_name,
13146 initialization_options: Some(json!({
13147 "testOptionValue": true
13148 })),
13149 initializer: Some(Box::new(move |fake_server| {
13150 let task_restarts = Arc::clone(&closure_restarts);
13151 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13152 task_restarts.fetch_add(1, atomic::Ordering::Release);
13153 futures::future::ready(Ok(()))
13154 });
13155 })),
13156 ..Default::default()
13157 },
13158 );
13159
13160 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13161 let _buffer = project
13162 .update(cx, |project, cx| {
13163 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13164 })
13165 .await
13166 .unwrap();
13167 let _fake_server = fake_servers.next().await.unwrap();
13168 update_test_language_settings(cx, |language_settings| {
13169 language_settings.languages.insert(
13170 language_name.clone(),
13171 LanguageSettingsContent {
13172 tab_size: NonZeroU32::new(8),
13173 ..Default::default()
13174 },
13175 );
13176 });
13177 cx.executor().run_until_parked();
13178 assert_eq!(
13179 server_restarts.load(atomic::Ordering::Acquire),
13180 0,
13181 "Should not restart LSP server on an unrelated change"
13182 );
13183
13184 update_test_project_settings(cx, |project_settings| {
13185 project_settings.lsp.insert(
13186 "Some other server name".into(),
13187 LspSettings {
13188 binary: None,
13189 settings: None,
13190 initialization_options: Some(json!({
13191 "some other init value": false
13192 })),
13193 enable_lsp_tasks: false,
13194 },
13195 );
13196 });
13197 cx.executor().run_until_parked();
13198 assert_eq!(
13199 server_restarts.load(atomic::Ordering::Acquire),
13200 0,
13201 "Should not restart LSP server on an unrelated LSP settings change"
13202 );
13203
13204 update_test_project_settings(cx, |project_settings| {
13205 project_settings.lsp.insert(
13206 language_server_name.into(),
13207 LspSettings {
13208 binary: None,
13209 settings: None,
13210 initialization_options: Some(json!({
13211 "anotherInitValue": false
13212 })),
13213 enable_lsp_tasks: false,
13214 },
13215 );
13216 });
13217 cx.executor().run_until_parked();
13218 assert_eq!(
13219 server_restarts.load(atomic::Ordering::Acquire),
13220 1,
13221 "Should restart LSP server on a related LSP settings change"
13222 );
13223
13224 update_test_project_settings(cx, |project_settings| {
13225 project_settings.lsp.insert(
13226 language_server_name.into(),
13227 LspSettings {
13228 binary: None,
13229 settings: None,
13230 initialization_options: Some(json!({
13231 "anotherInitValue": false
13232 })),
13233 enable_lsp_tasks: false,
13234 },
13235 );
13236 });
13237 cx.executor().run_until_parked();
13238 assert_eq!(
13239 server_restarts.load(atomic::Ordering::Acquire),
13240 1,
13241 "Should not restart LSP server on a related LSP settings change that is the same"
13242 );
13243
13244 update_test_project_settings(cx, |project_settings| {
13245 project_settings.lsp.insert(
13246 language_server_name.into(),
13247 LspSettings {
13248 binary: None,
13249 settings: None,
13250 initialization_options: None,
13251 enable_lsp_tasks: false,
13252 },
13253 );
13254 });
13255 cx.executor().run_until_parked();
13256 assert_eq!(
13257 server_restarts.load(atomic::Ordering::Acquire),
13258 2,
13259 "Should restart LSP server on another related LSP settings change"
13260 );
13261}
13262
13263#[gpui::test]
13264async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13265 init_test(cx, |_| {});
13266
13267 let mut cx = EditorLspTestContext::new_rust(
13268 lsp::ServerCapabilities {
13269 completion_provider: Some(lsp::CompletionOptions {
13270 trigger_characters: Some(vec![".".to_string()]),
13271 resolve_provider: Some(true),
13272 ..Default::default()
13273 }),
13274 ..Default::default()
13275 },
13276 cx,
13277 )
13278 .await;
13279
13280 cx.set_state("fn main() { let a = 2ˇ; }");
13281 cx.simulate_keystroke(".");
13282 let completion_item = lsp::CompletionItem {
13283 label: "some".into(),
13284 kind: Some(lsp::CompletionItemKind::SNIPPET),
13285 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13286 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13287 kind: lsp::MarkupKind::Markdown,
13288 value: "```rust\nSome(2)\n```".to_string(),
13289 })),
13290 deprecated: Some(false),
13291 sort_text: Some("fffffff2".to_string()),
13292 filter_text: Some("some".to_string()),
13293 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13294 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13295 range: lsp::Range {
13296 start: lsp::Position {
13297 line: 0,
13298 character: 22,
13299 },
13300 end: lsp::Position {
13301 line: 0,
13302 character: 22,
13303 },
13304 },
13305 new_text: "Some(2)".to_string(),
13306 })),
13307 additional_text_edits: Some(vec![lsp::TextEdit {
13308 range: lsp::Range {
13309 start: lsp::Position {
13310 line: 0,
13311 character: 20,
13312 },
13313 end: lsp::Position {
13314 line: 0,
13315 character: 22,
13316 },
13317 },
13318 new_text: "".to_string(),
13319 }]),
13320 ..Default::default()
13321 };
13322
13323 let closure_completion_item = completion_item.clone();
13324 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13325 let task_completion_item = closure_completion_item.clone();
13326 async move {
13327 Ok(Some(lsp::CompletionResponse::Array(vec![
13328 task_completion_item,
13329 ])))
13330 }
13331 });
13332
13333 request.next().await;
13334
13335 cx.condition(|editor, _| editor.context_menu_visible())
13336 .await;
13337 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13338 editor
13339 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13340 .unwrap()
13341 });
13342 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13343
13344 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13345 let task_completion_item = completion_item.clone();
13346 async move { Ok(task_completion_item) }
13347 })
13348 .next()
13349 .await
13350 .unwrap();
13351 apply_additional_edits.await.unwrap();
13352 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13353}
13354
13355#[gpui::test]
13356async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13357 init_test(cx, |_| {});
13358
13359 let mut cx = EditorLspTestContext::new_rust(
13360 lsp::ServerCapabilities {
13361 completion_provider: Some(lsp::CompletionOptions {
13362 trigger_characters: Some(vec![".".to_string()]),
13363 resolve_provider: Some(true),
13364 ..Default::default()
13365 }),
13366 ..Default::default()
13367 },
13368 cx,
13369 )
13370 .await;
13371
13372 cx.set_state("fn main() { let a = 2ˇ; }");
13373 cx.simulate_keystroke(".");
13374
13375 let item1 = lsp::CompletionItem {
13376 label: "method id()".to_string(),
13377 filter_text: Some("id".to_string()),
13378 detail: None,
13379 documentation: None,
13380 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13381 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13382 new_text: ".id".to_string(),
13383 })),
13384 ..lsp::CompletionItem::default()
13385 };
13386
13387 let item2 = lsp::CompletionItem {
13388 label: "other".to_string(),
13389 filter_text: Some("other".to_string()),
13390 detail: None,
13391 documentation: None,
13392 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13393 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13394 new_text: ".other".to_string(),
13395 })),
13396 ..lsp::CompletionItem::default()
13397 };
13398
13399 let item1 = item1.clone();
13400 cx.set_request_handler::<lsp::request::Completion, _, _>({
13401 let item1 = item1.clone();
13402 move |_, _, _| {
13403 let item1 = item1.clone();
13404 let item2 = item2.clone();
13405 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13406 }
13407 })
13408 .next()
13409 .await;
13410
13411 cx.condition(|editor, _| editor.context_menu_visible())
13412 .await;
13413 cx.update_editor(|editor, _, _| {
13414 let context_menu = editor.context_menu.borrow_mut();
13415 let context_menu = context_menu
13416 .as_ref()
13417 .expect("Should have the context menu deployed");
13418 match context_menu {
13419 CodeContextMenu::Completions(completions_menu) => {
13420 let completions = completions_menu.completions.borrow_mut();
13421 assert_eq!(
13422 completions
13423 .iter()
13424 .map(|completion| &completion.label.text)
13425 .collect::<Vec<_>>(),
13426 vec!["method id()", "other"]
13427 )
13428 }
13429 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13430 }
13431 });
13432
13433 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13434 let item1 = item1.clone();
13435 move |_, item_to_resolve, _| {
13436 let item1 = item1.clone();
13437 async move {
13438 if item1 == item_to_resolve {
13439 Ok(lsp::CompletionItem {
13440 label: "method id()".to_string(),
13441 filter_text: Some("id".to_string()),
13442 detail: Some("Now resolved!".to_string()),
13443 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13444 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13445 range: lsp::Range::new(
13446 lsp::Position::new(0, 22),
13447 lsp::Position::new(0, 22),
13448 ),
13449 new_text: ".id".to_string(),
13450 })),
13451 ..lsp::CompletionItem::default()
13452 })
13453 } else {
13454 Ok(item_to_resolve)
13455 }
13456 }
13457 }
13458 })
13459 .next()
13460 .await
13461 .unwrap();
13462 cx.run_until_parked();
13463
13464 cx.update_editor(|editor, window, cx| {
13465 editor.context_menu_next(&Default::default(), window, cx);
13466 });
13467
13468 cx.update_editor(|editor, _, _| {
13469 let context_menu = editor.context_menu.borrow_mut();
13470 let context_menu = context_menu
13471 .as_ref()
13472 .expect("Should have the context menu deployed");
13473 match context_menu {
13474 CodeContextMenu::Completions(completions_menu) => {
13475 let completions = completions_menu.completions.borrow_mut();
13476 assert_eq!(
13477 completions
13478 .iter()
13479 .map(|completion| &completion.label.text)
13480 .collect::<Vec<_>>(),
13481 vec!["method id() Now resolved!", "other"],
13482 "Should update first completion label, but not second as the filter text did not match."
13483 );
13484 }
13485 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13486 }
13487 });
13488}
13489
13490#[gpui::test]
13491async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13492 init_test(cx, |_| {});
13493
13494 let mut cx = EditorLspTestContext::new_rust(
13495 lsp::ServerCapabilities {
13496 completion_provider: Some(lsp::CompletionOptions {
13497 trigger_characters: Some(vec![".".to_string()]),
13498 resolve_provider: Some(true),
13499 ..Default::default()
13500 }),
13501 ..Default::default()
13502 },
13503 cx,
13504 )
13505 .await;
13506
13507 cx.set_state("fn main() { let a = 2ˇ; }");
13508 cx.simulate_keystroke(".");
13509
13510 let unresolved_item_1 = lsp::CompletionItem {
13511 label: "id".to_string(),
13512 filter_text: Some("id".to_string()),
13513 detail: None,
13514 documentation: None,
13515 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13516 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13517 new_text: ".id".to_string(),
13518 })),
13519 ..lsp::CompletionItem::default()
13520 };
13521 let resolved_item_1 = lsp::CompletionItem {
13522 additional_text_edits: Some(vec![lsp::TextEdit {
13523 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13524 new_text: "!!".to_string(),
13525 }]),
13526 ..unresolved_item_1.clone()
13527 };
13528 let unresolved_item_2 = lsp::CompletionItem {
13529 label: "other".to_string(),
13530 filter_text: Some("other".to_string()),
13531 detail: None,
13532 documentation: None,
13533 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13534 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13535 new_text: ".other".to_string(),
13536 })),
13537 ..lsp::CompletionItem::default()
13538 };
13539 let resolved_item_2 = lsp::CompletionItem {
13540 additional_text_edits: Some(vec![lsp::TextEdit {
13541 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13542 new_text: "??".to_string(),
13543 }]),
13544 ..unresolved_item_2.clone()
13545 };
13546
13547 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13548 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13549 cx.lsp
13550 .server
13551 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13552 let unresolved_item_1 = unresolved_item_1.clone();
13553 let resolved_item_1 = resolved_item_1.clone();
13554 let unresolved_item_2 = unresolved_item_2.clone();
13555 let resolved_item_2 = resolved_item_2.clone();
13556 let resolve_requests_1 = resolve_requests_1.clone();
13557 let resolve_requests_2 = resolve_requests_2.clone();
13558 move |unresolved_request, _| {
13559 let unresolved_item_1 = unresolved_item_1.clone();
13560 let resolved_item_1 = resolved_item_1.clone();
13561 let unresolved_item_2 = unresolved_item_2.clone();
13562 let resolved_item_2 = resolved_item_2.clone();
13563 let resolve_requests_1 = resolve_requests_1.clone();
13564 let resolve_requests_2 = resolve_requests_2.clone();
13565 async move {
13566 if unresolved_request == unresolved_item_1 {
13567 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13568 Ok(resolved_item_1.clone())
13569 } else if unresolved_request == unresolved_item_2 {
13570 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13571 Ok(resolved_item_2.clone())
13572 } else {
13573 panic!("Unexpected completion item {unresolved_request:?}")
13574 }
13575 }
13576 }
13577 })
13578 .detach();
13579
13580 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13581 let unresolved_item_1 = unresolved_item_1.clone();
13582 let unresolved_item_2 = unresolved_item_2.clone();
13583 async move {
13584 Ok(Some(lsp::CompletionResponse::Array(vec![
13585 unresolved_item_1,
13586 unresolved_item_2,
13587 ])))
13588 }
13589 })
13590 .next()
13591 .await;
13592
13593 cx.condition(|editor, _| editor.context_menu_visible())
13594 .await;
13595 cx.update_editor(|editor, _, _| {
13596 let context_menu = editor.context_menu.borrow_mut();
13597 let context_menu = context_menu
13598 .as_ref()
13599 .expect("Should have the context menu deployed");
13600 match context_menu {
13601 CodeContextMenu::Completions(completions_menu) => {
13602 let completions = completions_menu.completions.borrow_mut();
13603 assert_eq!(
13604 completions
13605 .iter()
13606 .map(|completion| &completion.label.text)
13607 .collect::<Vec<_>>(),
13608 vec!["id", "other"]
13609 )
13610 }
13611 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13612 }
13613 });
13614 cx.run_until_parked();
13615
13616 cx.update_editor(|editor, window, cx| {
13617 editor.context_menu_next(&ContextMenuNext, window, cx);
13618 });
13619 cx.run_until_parked();
13620 cx.update_editor(|editor, window, cx| {
13621 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13622 });
13623 cx.run_until_parked();
13624 cx.update_editor(|editor, window, cx| {
13625 editor.context_menu_next(&ContextMenuNext, window, cx);
13626 });
13627 cx.run_until_parked();
13628 cx.update_editor(|editor, window, cx| {
13629 editor
13630 .compose_completion(&ComposeCompletion::default(), window, cx)
13631 .expect("No task returned")
13632 })
13633 .await
13634 .expect("Completion failed");
13635 cx.run_until_parked();
13636
13637 cx.update_editor(|editor, _, cx| {
13638 assert_eq!(
13639 resolve_requests_1.load(atomic::Ordering::Acquire),
13640 1,
13641 "Should always resolve once despite multiple selections"
13642 );
13643 assert_eq!(
13644 resolve_requests_2.load(atomic::Ordering::Acquire),
13645 1,
13646 "Should always resolve once after multiple selections and applying the completion"
13647 );
13648 assert_eq!(
13649 editor.text(cx),
13650 "fn main() { let a = ??.other; }",
13651 "Should use resolved data when applying the completion"
13652 );
13653 });
13654}
13655
13656#[gpui::test]
13657async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13658 init_test(cx, |_| {});
13659
13660 let item_0 = lsp::CompletionItem {
13661 label: "abs".into(),
13662 insert_text: Some("abs".into()),
13663 data: Some(json!({ "very": "special"})),
13664 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13665 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13666 lsp::InsertReplaceEdit {
13667 new_text: "abs".to_string(),
13668 insert: lsp::Range::default(),
13669 replace: lsp::Range::default(),
13670 },
13671 )),
13672 ..lsp::CompletionItem::default()
13673 };
13674 let items = iter::once(item_0.clone())
13675 .chain((11..51).map(|i| lsp::CompletionItem {
13676 label: format!("item_{}", i),
13677 insert_text: Some(format!("item_{}", i)),
13678 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13679 ..lsp::CompletionItem::default()
13680 }))
13681 .collect::<Vec<_>>();
13682
13683 let default_commit_characters = vec!["?".to_string()];
13684 let default_data = json!({ "default": "data"});
13685 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13686 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13687 let default_edit_range = lsp::Range {
13688 start: lsp::Position {
13689 line: 0,
13690 character: 5,
13691 },
13692 end: lsp::Position {
13693 line: 0,
13694 character: 5,
13695 },
13696 };
13697
13698 let mut cx = EditorLspTestContext::new_rust(
13699 lsp::ServerCapabilities {
13700 completion_provider: Some(lsp::CompletionOptions {
13701 trigger_characters: Some(vec![".".to_string()]),
13702 resolve_provider: Some(true),
13703 ..Default::default()
13704 }),
13705 ..Default::default()
13706 },
13707 cx,
13708 )
13709 .await;
13710
13711 cx.set_state("fn main() { let a = 2ˇ; }");
13712 cx.simulate_keystroke(".");
13713
13714 let completion_data = default_data.clone();
13715 let completion_characters = default_commit_characters.clone();
13716 let completion_items = items.clone();
13717 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13718 let default_data = completion_data.clone();
13719 let default_commit_characters = completion_characters.clone();
13720 let items = completion_items.clone();
13721 async move {
13722 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13723 items,
13724 item_defaults: Some(lsp::CompletionListItemDefaults {
13725 data: Some(default_data.clone()),
13726 commit_characters: Some(default_commit_characters.clone()),
13727 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13728 default_edit_range,
13729 )),
13730 insert_text_format: Some(default_insert_text_format),
13731 insert_text_mode: Some(default_insert_text_mode),
13732 }),
13733 ..lsp::CompletionList::default()
13734 })))
13735 }
13736 })
13737 .next()
13738 .await;
13739
13740 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13741 cx.lsp
13742 .server
13743 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13744 let closure_resolved_items = resolved_items.clone();
13745 move |item_to_resolve, _| {
13746 let closure_resolved_items = closure_resolved_items.clone();
13747 async move {
13748 closure_resolved_items.lock().push(item_to_resolve.clone());
13749 Ok(item_to_resolve)
13750 }
13751 }
13752 })
13753 .detach();
13754
13755 cx.condition(|editor, _| editor.context_menu_visible())
13756 .await;
13757 cx.run_until_parked();
13758 cx.update_editor(|editor, _, _| {
13759 let menu = editor.context_menu.borrow_mut();
13760 match menu.as_ref().expect("should have the completions menu") {
13761 CodeContextMenu::Completions(completions_menu) => {
13762 assert_eq!(
13763 completions_menu
13764 .entries
13765 .borrow()
13766 .iter()
13767 .map(|mat| mat.string.clone())
13768 .collect::<Vec<String>>(),
13769 items
13770 .iter()
13771 .map(|completion| completion.label.clone())
13772 .collect::<Vec<String>>()
13773 );
13774 }
13775 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13776 }
13777 });
13778 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13779 // with 4 from the end.
13780 assert_eq!(
13781 *resolved_items.lock(),
13782 [&items[0..16], &items[items.len() - 4..items.len()]]
13783 .concat()
13784 .iter()
13785 .cloned()
13786 .map(|mut item| {
13787 if item.data.is_none() {
13788 item.data = Some(default_data.clone());
13789 }
13790 item
13791 })
13792 .collect::<Vec<lsp::CompletionItem>>(),
13793 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13794 );
13795 resolved_items.lock().clear();
13796
13797 cx.update_editor(|editor, window, cx| {
13798 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13799 });
13800 cx.run_until_parked();
13801 // Completions that have already been resolved are skipped.
13802 assert_eq!(
13803 *resolved_items.lock(),
13804 items[items.len() - 16..items.len() - 4]
13805 .iter()
13806 .cloned()
13807 .map(|mut item| {
13808 if item.data.is_none() {
13809 item.data = Some(default_data.clone());
13810 }
13811 item
13812 })
13813 .collect::<Vec<lsp::CompletionItem>>()
13814 );
13815 resolved_items.lock().clear();
13816}
13817
13818#[gpui::test]
13819async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13820 init_test(cx, |_| {});
13821
13822 let mut cx = EditorLspTestContext::new(
13823 Language::new(
13824 LanguageConfig {
13825 matcher: LanguageMatcher {
13826 path_suffixes: vec!["jsx".into()],
13827 ..Default::default()
13828 },
13829 overrides: [(
13830 "element".into(),
13831 LanguageConfigOverride {
13832 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13833 ..Default::default()
13834 },
13835 )]
13836 .into_iter()
13837 .collect(),
13838 ..Default::default()
13839 },
13840 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13841 )
13842 .with_override_query("(jsx_self_closing_element) @element")
13843 .unwrap(),
13844 lsp::ServerCapabilities {
13845 completion_provider: Some(lsp::CompletionOptions {
13846 trigger_characters: Some(vec![":".to_string()]),
13847 ..Default::default()
13848 }),
13849 ..Default::default()
13850 },
13851 cx,
13852 )
13853 .await;
13854
13855 cx.lsp
13856 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13857 Ok(Some(lsp::CompletionResponse::Array(vec![
13858 lsp::CompletionItem {
13859 label: "bg-blue".into(),
13860 ..Default::default()
13861 },
13862 lsp::CompletionItem {
13863 label: "bg-red".into(),
13864 ..Default::default()
13865 },
13866 lsp::CompletionItem {
13867 label: "bg-yellow".into(),
13868 ..Default::default()
13869 },
13870 ])))
13871 });
13872
13873 cx.set_state(r#"<p class="bgˇ" />"#);
13874
13875 // Trigger completion when typing a dash, because the dash is an extra
13876 // word character in the 'element' scope, which contains the cursor.
13877 cx.simulate_keystroke("-");
13878 cx.executor().run_until_parked();
13879 cx.update_editor(|editor, _, _| {
13880 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13881 {
13882 assert_eq!(
13883 completion_menu_entries(&menu),
13884 &["bg-red", "bg-blue", "bg-yellow"]
13885 );
13886 } else {
13887 panic!("expected completion menu to be open");
13888 }
13889 });
13890
13891 cx.simulate_keystroke("l");
13892 cx.executor().run_until_parked();
13893 cx.update_editor(|editor, _, _| {
13894 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13895 {
13896 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13897 } else {
13898 panic!("expected completion menu to be open");
13899 }
13900 });
13901
13902 // When filtering completions, consider the character after the '-' to
13903 // be the start of a subword.
13904 cx.set_state(r#"<p class="yelˇ" />"#);
13905 cx.simulate_keystroke("l");
13906 cx.executor().run_until_parked();
13907 cx.update_editor(|editor, _, _| {
13908 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13909 {
13910 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13911 } else {
13912 panic!("expected completion menu to be open");
13913 }
13914 });
13915}
13916
13917fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13918 let entries = menu.entries.borrow();
13919 entries.iter().map(|mat| mat.string.clone()).collect()
13920}
13921
13922#[gpui::test]
13923async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13924 init_test(cx, |settings| {
13925 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13926 FormatterList(vec![Formatter::Prettier].into()),
13927 ))
13928 });
13929
13930 let fs = FakeFs::new(cx.executor());
13931 fs.insert_file(path!("/file.ts"), Default::default()).await;
13932
13933 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13934 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13935
13936 language_registry.add(Arc::new(Language::new(
13937 LanguageConfig {
13938 name: "TypeScript".into(),
13939 matcher: LanguageMatcher {
13940 path_suffixes: vec!["ts".to_string()],
13941 ..Default::default()
13942 },
13943 ..Default::default()
13944 },
13945 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13946 )));
13947 update_test_language_settings(cx, |settings| {
13948 settings.defaults.prettier = Some(PrettierSettings {
13949 allowed: true,
13950 ..PrettierSettings::default()
13951 });
13952 });
13953
13954 let test_plugin = "test_plugin";
13955 let _ = language_registry.register_fake_lsp(
13956 "TypeScript",
13957 FakeLspAdapter {
13958 prettier_plugins: vec![test_plugin],
13959 ..Default::default()
13960 },
13961 );
13962
13963 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13964 let buffer = project
13965 .update(cx, |project, cx| {
13966 project.open_local_buffer(path!("/file.ts"), cx)
13967 })
13968 .await
13969 .unwrap();
13970
13971 let buffer_text = "one\ntwo\nthree\n";
13972 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13973 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13974 editor.update_in(cx, |editor, window, cx| {
13975 editor.set_text(buffer_text, window, cx)
13976 });
13977
13978 editor
13979 .update_in(cx, |editor, window, cx| {
13980 editor.perform_format(
13981 project.clone(),
13982 FormatTrigger::Manual,
13983 FormatTarget::Buffers,
13984 window,
13985 cx,
13986 )
13987 })
13988 .unwrap()
13989 .await;
13990 assert_eq!(
13991 editor.update(cx, |editor, cx| editor.text(cx)),
13992 buffer_text.to_string() + prettier_format_suffix,
13993 "Test prettier formatting was not applied to the original buffer text",
13994 );
13995
13996 update_test_language_settings(cx, |settings| {
13997 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13998 });
13999 let format = editor.update_in(cx, |editor, window, cx| {
14000 editor.perform_format(
14001 project.clone(),
14002 FormatTrigger::Manual,
14003 FormatTarget::Buffers,
14004 window,
14005 cx,
14006 )
14007 });
14008 format.await.unwrap();
14009 assert_eq!(
14010 editor.update(cx, |editor, cx| editor.text(cx)),
14011 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14012 "Autoformatting (via test prettier) was not applied to the original buffer text",
14013 );
14014}
14015
14016#[gpui::test]
14017async fn test_addition_reverts(cx: &mut TestAppContext) {
14018 init_test(cx, |_| {});
14019 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14020 let base_text = indoc! {r#"
14021 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
14033 // When addition hunks are not adjacent to carets, no hunk revert is performed
14034 assert_hunk_revert(
14035 indoc! {r#"struct Row;
14036 struct Row1;
14037 struct Row1.1;
14038 struct Row1.2;
14039 struct Row2;ˇ
14040
14041 struct Row4;
14042 struct Row5;
14043 struct Row6;
14044
14045 struct Row8;
14046 ˇstruct Row9;
14047 struct Row9.1;
14048 struct Row9.2;
14049 struct Row9.3;
14050 struct Row10;"#},
14051 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14052 indoc! {r#"struct Row;
14053 struct Row1;
14054 struct Row1.1;
14055 struct Row1.2;
14056 struct Row2;ˇ
14057
14058 struct Row4;
14059 struct Row5;
14060 struct Row6;
14061
14062 struct Row8;
14063 ˇstruct Row9;
14064 struct Row9.1;
14065 struct Row9.2;
14066 struct Row9.3;
14067 struct Row10;"#},
14068 base_text,
14069 &mut cx,
14070 );
14071 // Same for selections
14072 assert_hunk_revert(
14073 indoc! {r#"struct Row;
14074 struct Row1;
14075 struct Row2;
14076 struct Row2.1;
14077 struct Row2.2;
14078 «ˇ
14079 struct Row4;
14080 struct» Row5;
14081 «struct Row6;
14082 ˇ»
14083 struct Row9.1;
14084 struct Row9.2;
14085 struct Row9.3;
14086 struct Row8;
14087 struct Row9;
14088 struct Row10;"#},
14089 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14090 indoc! {r#"struct Row;
14091 struct Row1;
14092 struct Row2;
14093 struct Row2.1;
14094 struct Row2.2;
14095 «ˇ
14096 struct Row4;
14097 struct» Row5;
14098 «struct Row6;
14099 ˇ»
14100 struct Row9.1;
14101 struct Row9.2;
14102 struct Row9.3;
14103 struct Row8;
14104 struct Row9;
14105 struct Row10;"#},
14106 base_text,
14107 &mut cx,
14108 );
14109
14110 // When carets and selections intersect the addition hunks, those are reverted.
14111 // Adjacent carets got merged.
14112 assert_hunk_revert(
14113 indoc! {r#"struct Row;
14114 ˇ// something on the top
14115 struct Row1;
14116 struct Row2;
14117 struct Roˇw3.1;
14118 struct Row2.2;
14119 struct Row2.3;ˇ
14120
14121 struct Row4;
14122 struct ˇRow5.1;
14123 struct Row5.2;
14124 struct «Rowˇ»5.3;
14125 struct Row5;
14126 struct Row6;
14127 ˇ
14128 struct Row9.1;
14129 struct «Rowˇ»9.2;
14130 struct «ˇRow»9.3;
14131 struct Row8;
14132 struct Row9;
14133 «ˇ// something on bottom»
14134 struct Row10;"#},
14135 vec![
14136 DiffHunkStatusKind::Added,
14137 DiffHunkStatusKind::Added,
14138 DiffHunkStatusKind::Added,
14139 DiffHunkStatusKind::Added,
14140 DiffHunkStatusKind::Added,
14141 ],
14142 indoc! {r#"struct Row;
14143 ˇstruct Row1;
14144 struct Row2;
14145 ˇ
14146 struct Row4;
14147 ˇstruct Row5;
14148 struct Row6;
14149 ˇ
14150 ˇstruct Row8;
14151 struct Row9;
14152 ˇstruct Row10;"#},
14153 base_text,
14154 &mut cx,
14155 );
14156}
14157
14158#[gpui::test]
14159async fn test_modification_reverts(cx: &mut TestAppContext) {
14160 init_test(cx, |_| {});
14161 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14162 let base_text = indoc! {r#"
14163 struct Row;
14164 struct Row1;
14165 struct Row2;
14166
14167 struct Row4;
14168 struct Row5;
14169 struct Row6;
14170
14171 struct Row8;
14172 struct Row9;
14173 struct Row10;"#};
14174
14175 // Modification hunks behave the same as the addition ones.
14176 assert_hunk_revert(
14177 indoc! {r#"struct Row;
14178 struct Row1;
14179 struct Row33;
14180 ˇ
14181 struct Row4;
14182 struct Row5;
14183 struct Row6;
14184 ˇ
14185 struct Row99;
14186 struct Row9;
14187 struct Row10;"#},
14188 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14189 indoc! {r#"struct Row;
14190 struct Row1;
14191 struct Row33;
14192 ˇ
14193 struct Row4;
14194 struct Row5;
14195 struct Row6;
14196 ˇ
14197 struct Row99;
14198 struct Row9;
14199 struct Row10;"#},
14200 base_text,
14201 &mut cx,
14202 );
14203 assert_hunk_revert(
14204 indoc! {r#"struct Row;
14205 struct Row1;
14206 struct Row33;
14207 «ˇ
14208 struct Row4;
14209 struct» Row5;
14210 «struct Row6;
14211 ˇ»
14212 struct Row99;
14213 struct Row9;
14214 struct Row10;"#},
14215 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14216 indoc! {r#"struct Row;
14217 struct Row1;
14218 struct Row33;
14219 «ˇ
14220 struct Row4;
14221 struct» Row5;
14222 «struct Row6;
14223 ˇ»
14224 struct Row99;
14225 struct Row9;
14226 struct Row10;"#},
14227 base_text,
14228 &mut cx,
14229 );
14230
14231 assert_hunk_revert(
14232 indoc! {r#"ˇstruct Row1.1;
14233 struct Row1;
14234 «ˇstr»uct Row22;
14235
14236 struct ˇRow44;
14237 struct Row5;
14238 struct «Rˇ»ow66;ˇ
14239
14240 «struˇ»ct Row88;
14241 struct Row9;
14242 struct Row1011;ˇ"#},
14243 vec![
14244 DiffHunkStatusKind::Modified,
14245 DiffHunkStatusKind::Modified,
14246 DiffHunkStatusKind::Modified,
14247 DiffHunkStatusKind::Modified,
14248 DiffHunkStatusKind::Modified,
14249 DiffHunkStatusKind::Modified,
14250 ],
14251 indoc! {r#"struct Row;
14252 ˇstruct Row1;
14253 struct Row2;
14254 ˇ
14255 struct Row4;
14256 ˇstruct Row5;
14257 struct Row6;
14258 ˇ
14259 struct Row8;
14260 ˇstruct Row9;
14261 struct Row10;ˇ"#},
14262 base_text,
14263 &mut cx,
14264 );
14265}
14266
14267#[gpui::test]
14268async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14269 init_test(cx, |_| {});
14270 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14271 let base_text = indoc! {r#"
14272 one
14273
14274 two
14275 three
14276 "#};
14277
14278 cx.set_head_text(base_text);
14279 cx.set_state("\nˇ\n");
14280 cx.executor().run_until_parked();
14281 cx.update_editor(|editor, _window, cx| {
14282 editor.expand_selected_diff_hunks(cx);
14283 });
14284 cx.executor().run_until_parked();
14285 cx.update_editor(|editor, window, cx| {
14286 editor.backspace(&Default::default(), window, cx);
14287 });
14288 cx.run_until_parked();
14289 cx.assert_state_with_diff(
14290 indoc! {r#"
14291
14292 - two
14293 - threeˇ
14294 +
14295 "#}
14296 .to_string(),
14297 );
14298}
14299
14300#[gpui::test]
14301async fn test_deletion_reverts(cx: &mut TestAppContext) {
14302 init_test(cx, |_| {});
14303 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14304 let base_text = indoc! {r#"struct Row;
14305struct Row1;
14306struct Row2;
14307
14308struct Row4;
14309struct Row5;
14310struct Row6;
14311
14312struct Row8;
14313struct Row9;
14314struct Row10;"#};
14315
14316 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14317 assert_hunk_revert(
14318 indoc! {r#"struct Row;
14319 struct Row2;
14320
14321 ˇstruct Row4;
14322 struct Row5;
14323 struct Row6;
14324 ˇ
14325 struct Row8;
14326 struct Row10;"#},
14327 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14328 indoc! {r#"struct Row;
14329 struct Row2;
14330
14331 ˇstruct Row4;
14332 struct Row5;
14333 struct Row6;
14334 ˇ
14335 struct Row8;
14336 struct Row10;"#},
14337 base_text,
14338 &mut cx,
14339 );
14340 assert_hunk_revert(
14341 indoc! {r#"struct Row;
14342 struct Row2;
14343
14344 «ˇstruct Row4;
14345 struct» Row5;
14346 «struct Row6;
14347 ˇ»
14348 struct Row8;
14349 struct Row10;"#},
14350 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14351 indoc! {r#"struct Row;
14352 struct Row2;
14353
14354 «ˇstruct Row4;
14355 struct» Row5;
14356 «struct Row6;
14357 ˇ»
14358 struct Row8;
14359 struct Row10;"#},
14360 base_text,
14361 &mut cx,
14362 );
14363
14364 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14365 assert_hunk_revert(
14366 indoc! {r#"struct Row;
14367 ˇstruct Row2;
14368
14369 struct Row4;
14370 struct Row5;
14371 struct Row6;
14372
14373 struct Row8;ˇ
14374 struct Row10;"#},
14375 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14376 indoc! {r#"struct Row;
14377 struct Row1;
14378 ˇstruct Row2;
14379
14380 struct Row4;
14381 struct Row5;
14382 struct Row6;
14383
14384 struct Row8;ˇ
14385 struct Row9;
14386 struct Row10;"#},
14387 base_text,
14388 &mut cx,
14389 );
14390 assert_hunk_revert(
14391 indoc! {r#"struct Row;
14392 struct Row2«ˇ;
14393 struct Row4;
14394 struct» Row5;
14395 «struct Row6;
14396
14397 struct Row8;ˇ»
14398 struct Row10;"#},
14399 vec![
14400 DiffHunkStatusKind::Deleted,
14401 DiffHunkStatusKind::Deleted,
14402 DiffHunkStatusKind::Deleted,
14403 ],
14404 indoc! {r#"struct Row;
14405 struct Row1;
14406 struct Row2«ˇ;
14407
14408 struct Row4;
14409 struct» Row5;
14410 «struct Row6;
14411
14412 struct Row8;ˇ»
14413 struct Row9;
14414 struct Row10;"#},
14415 base_text,
14416 &mut cx,
14417 );
14418}
14419
14420#[gpui::test]
14421async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14422 init_test(cx, |_| {});
14423
14424 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14425 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14426 let base_text_3 =
14427 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14428
14429 let text_1 = edit_first_char_of_every_line(base_text_1);
14430 let text_2 = edit_first_char_of_every_line(base_text_2);
14431 let text_3 = edit_first_char_of_every_line(base_text_3);
14432
14433 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14434 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14435 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14436
14437 let multibuffer = cx.new(|cx| {
14438 let mut multibuffer = MultiBuffer::new(ReadWrite);
14439 multibuffer.push_excerpts(
14440 buffer_1.clone(),
14441 [
14442 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14443 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14444 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14445 ],
14446 cx,
14447 );
14448 multibuffer.push_excerpts(
14449 buffer_2.clone(),
14450 [
14451 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14452 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14453 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14454 ],
14455 cx,
14456 );
14457 multibuffer.push_excerpts(
14458 buffer_3.clone(),
14459 [
14460 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14461 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14462 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14463 ],
14464 cx,
14465 );
14466 multibuffer
14467 });
14468
14469 let fs = FakeFs::new(cx.executor());
14470 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14471 let (editor, cx) = cx
14472 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14473 editor.update_in(cx, |editor, _window, cx| {
14474 for (buffer, diff_base) in [
14475 (buffer_1.clone(), base_text_1),
14476 (buffer_2.clone(), base_text_2),
14477 (buffer_3.clone(), base_text_3),
14478 ] {
14479 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14480 editor
14481 .buffer
14482 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14483 }
14484 });
14485 cx.executor().run_until_parked();
14486
14487 editor.update_in(cx, |editor, window, cx| {
14488 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}");
14489 editor.select_all(&SelectAll, window, cx);
14490 editor.git_restore(&Default::default(), window, cx);
14491 });
14492 cx.executor().run_until_parked();
14493
14494 // When all ranges are selected, all buffer hunks are reverted.
14495 editor.update(cx, |editor, cx| {
14496 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");
14497 });
14498 buffer_1.update(cx, |buffer, _| {
14499 assert_eq!(buffer.text(), base_text_1);
14500 });
14501 buffer_2.update(cx, |buffer, _| {
14502 assert_eq!(buffer.text(), base_text_2);
14503 });
14504 buffer_3.update(cx, |buffer, _| {
14505 assert_eq!(buffer.text(), base_text_3);
14506 });
14507
14508 editor.update_in(cx, |editor, window, cx| {
14509 editor.undo(&Default::default(), window, cx);
14510 });
14511
14512 editor.update_in(cx, |editor, window, cx| {
14513 editor.change_selections(None, window, cx, |s| {
14514 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14515 });
14516 editor.git_restore(&Default::default(), window, cx);
14517 });
14518
14519 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14520 // but not affect buffer_2 and its related excerpts.
14521 editor.update(cx, |editor, cx| {
14522 assert_eq!(
14523 editor.text(cx),
14524 "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}"
14525 );
14526 });
14527 buffer_1.update(cx, |buffer, _| {
14528 assert_eq!(buffer.text(), base_text_1);
14529 });
14530 buffer_2.update(cx, |buffer, _| {
14531 assert_eq!(
14532 buffer.text(),
14533 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14534 );
14535 });
14536 buffer_3.update(cx, |buffer, _| {
14537 assert_eq!(
14538 buffer.text(),
14539 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14540 );
14541 });
14542
14543 fn edit_first_char_of_every_line(text: &str) -> String {
14544 text.split('\n')
14545 .map(|line| format!("X{}", &line[1..]))
14546 .collect::<Vec<_>>()
14547 .join("\n")
14548 }
14549}
14550
14551#[gpui::test]
14552async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14553 init_test(cx, |_| {});
14554
14555 let cols = 4;
14556 let rows = 10;
14557 let sample_text_1 = sample_text(rows, cols, 'a');
14558 assert_eq!(
14559 sample_text_1,
14560 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14561 );
14562 let sample_text_2 = sample_text(rows, cols, 'l');
14563 assert_eq!(
14564 sample_text_2,
14565 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14566 );
14567 let sample_text_3 = sample_text(rows, cols, 'v');
14568 assert_eq!(
14569 sample_text_3,
14570 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14571 );
14572
14573 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14574 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14575 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14576
14577 let multi_buffer = cx.new(|cx| {
14578 let mut multibuffer = MultiBuffer::new(ReadWrite);
14579 multibuffer.push_excerpts(
14580 buffer_1.clone(),
14581 [
14582 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14583 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14584 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14585 ],
14586 cx,
14587 );
14588 multibuffer.push_excerpts(
14589 buffer_2.clone(),
14590 [
14591 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14592 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14593 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14594 ],
14595 cx,
14596 );
14597 multibuffer.push_excerpts(
14598 buffer_3.clone(),
14599 [
14600 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14601 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14602 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14603 ],
14604 cx,
14605 );
14606 multibuffer
14607 });
14608
14609 let fs = FakeFs::new(cx.executor());
14610 fs.insert_tree(
14611 "/a",
14612 json!({
14613 "main.rs": sample_text_1,
14614 "other.rs": sample_text_2,
14615 "lib.rs": sample_text_3,
14616 }),
14617 )
14618 .await;
14619 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14620 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14621 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14622 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14623 Editor::new(
14624 EditorMode::full(),
14625 multi_buffer,
14626 Some(project.clone()),
14627 window,
14628 cx,
14629 )
14630 });
14631 let multibuffer_item_id = workspace
14632 .update(cx, |workspace, window, cx| {
14633 assert!(
14634 workspace.active_item(cx).is_none(),
14635 "active item should be None before the first item is added"
14636 );
14637 workspace.add_item_to_active_pane(
14638 Box::new(multi_buffer_editor.clone()),
14639 None,
14640 true,
14641 window,
14642 cx,
14643 );
14644 let active_item = workspace
14645 .active_item(cx)
14646 .expect("should have an active item after adding the multi buffer");
14647 assert!(
14648 !active_item.is_singleton(cx),
14649 "A multi buffer was expected to active after adding"
14650 );
14651 active_item.item_id()
14652 })
14653 .unwrap();
14654 cx.executor().run_until_parked();
14655
14656 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14657 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14658 s.select_ranges(Some(1..2))
14659 });
14660 editor.open_excerpts(&OpenExcerpts, window, cx);
14661 });
14662 cx.executor().run_until_parked();
14663 let first_item_id = workspace
14664 .update(cx, |workspace, window, cx| {
14665 let active_item = workspace
14666 .active_item(cx)
14667 .expect("should have an active item after navigating into the 1st buffer");
14668 let first_item_id = active_item.item_id();
14669 assert_ne!(
14670 first_item_id, multibuffer_item_id,
14671 "Should navigate into the 1st buffer and activate it"
14672 );
14673 assert!(
14674 active_item.is_singleton(cx),
14675 "New active item should be a singleton buffer"
14676 );
14677 assert_eq!(
14678 active_item
14679 .act_as::<Editor>(cx)
14680 .expect("should have navigated into an editor for the 1st buffer")
14681 .read(cx)
14682 .text(cx),
14683 sample_text_1
14684 );
14685
14686 workspace
14687 .go_back(workspace.active_pane().downgrade(), window, cx)
14688 .detach_and_log_err(cx);
14689
14690 first_item_id
14691 })
14692 .unwrap();
14693 cx.executor().run_until_parked();
14694 workspace
14695 .update(cx, |workspace, _, cx| {
14696 let active_item = workspace
14697 .active_item(cx)
14698 .expect("should have an active item after navigating back");
14699 assert_eq!(
14700 active_item.item_id(),
14701 multibuffer_item_id,
14702 "Should navigate back to the multi buffer"
14703 );
14704 assert!(!active_item.is_singleton(cx));
14705 })
14706 .unwrap();
14707
14708 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14709 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14710 s.select_ranges(Some(39..40))
14711 });
14712 editor.open_excerpts(&OpenExcerpts, window, cx);
14713 });
14714 cx.executor().run_until_parked();
14715 let second_item_id = workspace
14716 .update(cx, |workspace, window, cx| {
14717 let active_item = workspace
14718 .active_item(cx)
14719 .expect("should have an active item after navigating into the 2nd buffer");
14720 let second_item_id = active_item.item_id();
14721 assert_ne!(
14722 second_item_id, multibuffer_item_id,
14723 "Should navigate away from the multibuffer"
14724 );
14725 assert_ne!(
14726 second_item_id, first_item_id,
14727 "Should navigate into the 2nd buffer and activate it"
14728 );
14729 assert!(
14730 active_item.is_singleton(cx),
14731 "New active item should be a singleton buffer"
14732 );
14733 assert_eq!(
14734 active_item
14735 .act_as::<Editor>(cx)
14736 .expect("should have navigated into an editor")
14737 .read(cx)
14738 .text(cx),
14739 sample_text_2
14740 );
14741
14742 workspace
14743 .go_back(workspace.active_pane().downgrade(), window, cx)
14744 .detach_and_log_err(cx);
14745
14746 second_item_id
14747 })
14748 .unwrap();
14749 cx.executor().run_until_parked();
14750 workspace
14751 .update(cx, |workspace, _, cx| {
14752 let active_item = workspace
14753 .active_item(cx)
14754 .expect("should have an active item after navigating back from the 2nd buffer");
14755 assert_eq!(
14756 active_item.item_id(),
14757 multibuffer_item_id,
14758 "Should navigate back from the 2nd buffer to the multi buffer"
14759 );
14760 assert!(!active_item.is_singleton(cx));
14761 })
14762 .unwrap();
14763
14764 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14765 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14766 s.select_ranges(Some(70..70))
14767 });
14768 editor.open_excerpts(&OpenExcerpts, window, cx);
14769 });
14770 cx.executor().run_until_parked();
14771 workspace
14772 .update(cx, |workspace, window, cx| {
14773 let active_item = workspace
14774 .active_item(cx)
14775 .expect("should have an active item after navigating into the 3rd buffer");
14776 let third_item_id = active_item.item_id();
14777 assert_ne!(
14778 third_item_id, multibuffer_item_id,
14779 "Should navigate into the 3rd buffer and activate it"
14780 );
14781 assert_ne!(third_item_id, first_item_id);
14782 assert_ne!(third_item_id, second_item_id);
14783 assert!(
14784 active_item.is_singleton(cx),
14785 "New active item should be a singleton buffer"
14786 );
14787 assert_eq!(
14788 active_item
14789 .act_as::<Editor>(cx)
14790 .expect("should have navigated into an editor")
14791 .read(cx)
14792 .text(cx),
14793 sample_text_3
14794 );
14795
14796 workspace
14797 .go_back(workspace.active_pane().downgrade(), window, cx)
14798 .detach_and_log_err(cx);
14799 })
14800 .unwrap();
14801 cx.executor().run_until_parked();
14802 workspace
14803 .update(cx, |workspace, _, cx| {
14804 let active_item = workspace
14805 .active_item(cx)
14806 .expect("should have an active item after navigating back from the 3rd buffer");
14807 assert_eq!(
14808 active_item.item_id(),
14809 multibuffer_item_id,
14810 "Should navigate back from the 3rd buffer to the multi buffer"
14811 );
14812 assert!(!active_item.is_singleton(cx));
14813 })
14814 .unwrap();
14815}
14816
14817#[gpui::test]
14818async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14819 init_test(cx, |_| {});
14820
14821 let mut cx = EditorTestContext::new(cx).await;
14822
14823 let diff_base = r#"
14824 use some::mod;
14825
14826 const A: u32 = 42;
14827
14828 fn main() {
14829 println!("hello");
14830
14831 println!("world");
14832 }
14833 "#
14834 .unindent();
14835
14836 cx.set_state(
14837 &r#"
14838 use some::modified;
14839
14840 ˇ
14841 fn main() {
14842 println!("hello there");
14843
14844 println!("around the");
14845 println!("world");
14846 }
14847 "#
14848 .unindent(),
14849 );
14850
14851 cx.set_head_text(&diff_base);
14852 executor.run_until_parked();
14853
14854 cx.update_editor(|editor, window, cx| {
14855 editor.go_to_next_hunk(&GoToHunk, window, cx);
14856 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14857 });
14858 executor.run_until_parked();
14859 cx.assert_state_with_diff(
14860 r#"
14861 use some::modified;
14862
14863
14864 fn main() {
14865 - println!("hello");
14866 + ˇ println!("hello there");
14867
14868 println!("around the");
14869 println!("world");
14870 }
14871 "#
14872 .unindent(),
14873 );
14874
14875 cx.update_editor(|editor, window, cx| {
14876 for _ in 0..2 {
14877 editor.go_to_next_hunk(&GoToHunk, window, cx);
14878 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14879 }
14880 });
14881 executor.run_until_parked();
14882 cx.assert_state_with_diff(
14883 r#"
14884 - use some::mod;
14885 + ˇuse some::modified;
14886
14887
14888 fn main() {
14889 - println!("hello");
14890 + println!("hello there");
14891
14892 + println!("around the");
14893 println!("world");
14894 }
14895 "#
14896 .unindent(),
14897 );
14898
14899 cx.update_editor(|editor, window, cx| {
14900 editor.go_to_next_hunk(&GoToHunk, window, cx);
14901 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14902 });
14903 executor.run_until_parked();
14904 cx.assert_state_with_diff(
14905 r#"
14906 - use some::mod;
14907 + use some::modified;
14908
14909 - const A: u32 = 42;
14910 ˇ
14911 fn main() {
14912 - println!("hello");
14913 + println!("hello there");
14914
14915 + println!("around the");
14916 println!("world");
14917 }
14918 "#
14919 .unindent(),
14920 );
14921
14922 cx.update_editor(|editor, window, cx| {
14923 editor.cancel(&Cancel, window, cx);
14924 });
14925
14926 cx.assert_state_with_diff(
14927 r#"
14928 use some::modified;
14929
14930 ˇ
14931 fn main() {
14932 println!("hello there");
14933
14934 println!("around the");
14935 println!("world");
14936 }
14937 "#
14938 .unindent(),
14939 );
14940}
14941
14942#[gpui::test]
14943async fn test_diff_base_change_with_expanded_diff_hunks(
14944 executor: BackgroundExecutor,
14945 cx: &mut TestAppContext,
14946) {
14947 init_test(cx, |_| {});
14948
14949 let mut cx = EditorTestContext::new(cx).await;
14950
14951 let diff_base = r#"
14952 use some::mod1;
14953 use some::mod2;
14954
14955 const A: u32 = 42;
14956 const B: u32 = 42;
14957 const C: u32 = 42;
14958
14959 fn main() {
14960 println!("hello");
14961
14962 println!("world");
14963 }
14964 "#
14965 .unindent();
14966
14967 cx.set_state(
14968 &r#"
14969 use some::mod2;
14970
14971 const A: u32 = 42;
14972 const C: u32 = 42;
14973
14974 fn main(ˇ) {
14975 //println!("hello");
14976
14977 println!("world");
14978 //
14979 //
14980 }
14981 "#
14982 .unindent(),
14983 );
14984
14985 cx.set_head_text(&diff_base);
14986 executor.run_until_parked();
14987
14988 cx.update_editor(|editor, window, cx| {
14989 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14990 });
14991 executor.run_until_parked();
14992 cx.assert_state_with_diff(
14993 r#"
14994 - use some::mod1;
14995 use some::mod2;
14996
14997 const A: u32 = 42;
14998 - const B: u32 = 42;
14999 const C: u32 = 42;
15000
15001 fn main(ˇ) {
15002 - println!("hello");
15003 + //println!("hello");
15004
15005 println!("world");
15006 + //
15007 + //
15008 }
15009 "#
15010 .unindent(),
15011 );
15012
15013 cx.set_head_text("new diff base!");
15014 executor.run_until_parked();
15015 cx.assert_state_with_diff(
15016 r#"
15017 - new diff base!
15018 + use some::mod2;
15019 +
15020 + const A: u32 = 42;
15021 + const C: u32 = 42;
15022 +
15023 + fn main(ˇ) {
15024 + //println!("hello");
15025 +
15026 + println!("world");
15027 + //
15028 + //
15029 + }
15030 "#
15031 .unindent(),
15032 );
15033}
15034
15035#[gpui::test]
15036async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15037 init_test(cx, |_| {});
15038
15039 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15040 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15041 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15042 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15043 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15044 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15045
15046 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15047 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15048 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15049
15050 let multi_buffer = cx.new(|cx| {
15051 let mut multibuffer = MultiBuffer::new(ReadWrite);
15052 multibuffer.push_excerpts(
15053 buffer_1.clone(),
15054 [
15055 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15056 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15057 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15058 ],
15059 cx,
15060 );
15061 multibuffer.push_excerpts(
15062 buffer_2.clone(),
15063 [
15064 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15065 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15066 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15067 ],
15068 cx,
15069 );
15070 multibuffer.push_excerpts(
15071 buffer_3.clone(),
15072 [
15073 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15074 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15075 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15076 ],
15077 cx,
15078 );
15079 multibuffer
15080 });
15081
15082 let editor =
15083 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15084 editor
15085 .update(cx, |editor, _window, cx| {
15086 for (buffer, diff_base) in [
15087 (buffer_1.clone(), file_1_old),
15088 (buffer_2.clone(), file_2_old),
15089 (buffer_3.clone(), file_3_old),
15090 ] {
15091 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15092 editor
15093 .buffer
15094 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15095 }
15096 })
15097 .unwrap();
15098
15099 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15100 cx.run_until_parked();
15101
15102 cx.assert_editor_state(
15103 &"
15104 ˇaaa
15105 ccc
15106 ddd
15107
15108 ggg
15109 hhh
15110
15111
15112 lll
15113 mmm
15114 NNN
15115
15116 qqq
15117 rrr
15118
15119 uuu
15120 111
15121 222
15122 333
15123
15124 666
15125 777
15126
15127 000
15128 !!!"
15129 .unindent(),
15130 );
15131
15132 cx.update_editor(|editor, window, cx| {
15133 editor.select_all(&SelectAll, window, cx);
15134 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15135 });
15136 cx.executor().run_until_parked();
15137
15138 cx.assert_state_with_diff(
15139 "
15140 «aaa
15141 - bbb
15142 ccc
15143 ddd
15144
15145 ggg
15146 hhh
15147
15148
15149 lll
15150 mmm
15151 - nnn
15152 + NNN
15153
15154 qqq
15155 rrr
15156
15157 uuu
15158 111
15159 222
15160 333
15161
15162 + 666
15163 777
15164
15165 000
15166 !!!ˇ»"
15167 .unindent(),
15168 );
15169}
15170
15171#[gpui::test]
15172async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15173 init_test(cx, |_| {});
15174
15175 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15176 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15177
15178 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15179 let multi_buffer = cx.new(|cx| {
15180 let mut multibuffer = MultiBuffer::new(ReadWrite);
15181 multibuffer.push_excerpts(
15182 buffer.clone(),
15183 [
15184 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15185 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15186 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15187 ],
15188 cx,
15189 );
15190 multibuffer
15191 });
15192
15193 let editor =
15194 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15195 editor
15196 .update(cx, |editor, _window, cx| {
15197 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15198 editor
15199 .buffer
15200 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15201 })
15202 .unwrap();
15203
15204 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15205 cx.run_until_parked();
15206
15207 cx.update_editor(|editor, window, cx| {
15208 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15209 });
15210 cx.executor().run_until_parked();
15211
15212 // When the start of a hunk coincides with the start of its excerpt,
15213 // the hunk is expanded. When the start of a a hunk is earlier than
15214 // the start of its excerpt, the hunk is not expanded.
15215 cx.assert_state_with_diff(
15216 "
15217 ˇaaa
15218 - bbb
15219 + BBB
15220
15221 - ddd
15222 - eee
15223 + DDD
15224 + EEE
15225 fff
15226
15227 iii
15228 "
15229 .unindent(),
15230 );
15231}
15232
15233#[gpui::test]
15234async fn test_edits_around_expanded_insertion_hunks(
15235 executor: BackgroundExecutor,
15236 cx: &mut TestAppContext,
15237) {
15238 init_test(cx, |_| {});
15239
15240 let mut cx = EditorTestContext::new(cx).await;
15241
15242 let diff_base = r#"
15243 use some::mod1;
15244 use some::mod2;
15245
15246 const A: u32 = 42;
15247
15248 fn main() {
15249 println!("hello");
15250
15251 println!("world");
15252 }
15253 "#
15254 .unindent();
15255 executor.run_until_parked();
15256 cx.set_state(
15257 &r#"
15258 use some::mod1;
15259 use some::mod2;
15260
15261 const A: u32 = 42;
15262 const B: u32 = 42;
15263 const C: u32 = 42;
15264 ˇ
15265
15266 fn main() {
15267 println!("hello");
15268
15269 println!("world");
15270 }
15271 "#
15272 .unindent(),
15273 );
15274
15275 cx.set_head_text(&diff_base);
15276 executor.run_until_parked();
15277
15278 cx.update_editor(|editor, window, cx| {
15279 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15280 });
15281 executor.run_until_parked();
15282
15283 cx.assert_state_with_diff(
15284 r#"
15285 use some::mod1;
15286 use some::mod2;
15287
15288 const A: u32 = 42;
15289 + const B: u32 = 42;
15290 + const C: u32 = 42;
15291 + ˇ
15292
15293 fn main() {
15294 println!("hello");
15295
15296 println!("world");
15297 }
15298 "#
15299 .unindent(),
15300 );
15301
15302 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15303 executor.run_until_parked();
15304
15305 cx.assert_state_with_diff(
15306 r#"
15307 use some::mod1;
15308 use some::mod2;
15309
15310 const A: u32 = 42;
15311 + const B: u32 = 42;
15312 + const C: u32 = 42;
15313 + const D: u32 = 42;
15314 + ˇ
15315
15316 fn main() {
15317 println!("hello");
15318
15319 println!("world");
15320 }
15321 "#
15322 .unindent(),
15323 );
15324
15325 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15326 executor.run_until_parked();
15327
15328 cx.assert_state_with_diff(
15329 r#"
15330 use some::mod1;
15331 use some::mod2;
15332
15333 const A: u32 = 42;
15334 + const B: u32 = 42;
15335 + const C: u32 = 42;
15336 + const D: u32 = 42;
15337 + const E: u32 = 42;
15338 + ˇ
15339
15340 fn main() {
15341 println!("hello");
15342
15343 println!("world");
15344 }
15345 "#
15346 .unindent(),
15347 );
15348
15349 cx.update_editor(|editor, window, cx| {
15350 editor.delete_line(&DeleteLine, window, cx);
15351 });
15352 executor.run_until_parked();
15353
15354 cx.assert_state_with_diff(
15355 r#"
15356 use some::mod1;
15357 use some::mod2;
15358
15359 const A: u32 = 42;
15360 + const B: u32 = 42;
15361 + const C: u32 = 42;
15362 + const D: u32 = 42;
15363 + const E: u32 = 42;
15364 ˇ
15365 fn main() {
15366 println!("hello");
15367
15368 println!("world");
15369 }
15370 "#
15371 .unindent(),
15372 );
15373
15374 cx.update_editor(|editor, window, cx| {
15375 editor.move_up(&MoveUp, window, cx);
15376 editor.delete_line(&DeleteLine, window, cx);
15377 editor.move_up(&MoveUp, window, cx);
15378 editor.delete_line(&DeleteLine, window, cx);
15379 editor.move_up(&MoveUp, window, cx);
15380 editor.delete_line(&DeleteLine, window, cx);
15381 });
15382 executor.run_until_parked();
15383 cx.assert_state_with_diff(
15384 r#"
15385 use some::mod1;
15386 use some::mod2;
15387
15388 const A: u32 = 42;
15389 + const B: u32 = 42;
15390 ˇ
15391 fn main() {
15392 println!("hello");
15393
15394 println!("world");
15395 }
15396 "#
15397 .unindent(),
15398 );
15399
15400 cx.update_editor(|editor, window, cx| {
15401 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15402 editor.delete_line(&DeleteLine, window, cx);
15403 });
15404 executor.run_until_parked();
15405 cx.assert_state_with_diff(
15406 r#"
15407 ˇ
15408 fn main() {
15409 println!("hello");
15410
15411 println!("world");
15412 }
15413 "#
15414 .unindent(),
15415 );
15416}
15417
15418#[gpui::test]
15419async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15420 init_test(cx, |_| {});
15421
15422 let mut cx = EditorTestContext::new(cx).await;
15423 cx.set_head_text(indoc! { "
15424 one
15425 two
15426 three
15427 four
15428 five
15429 "
15430 });
15431 cx.set_state(indoc! { "
15432 one
15433 ˇthree
15434 five
15435 "});
15436 cx.run_until_parked();
15437 cx.update_editor(|editor, window, cx| {
15438 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15439 });
15440 cx.assert_state_with_diff(
15441 indoc! { "
15442 one
15443 - two
15444 ˇthree
15445 - four
15446 five
15447 "}
15448 .to_string(),
15449 );
15450 cx.update_editor(|editor, window, cx| {
15451 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15452 });
15453
15454 cx.assert_state_with_diff(
15455 indoc! { "
15456 one
15457 ˇthree
15458 five
15459 "}
15460 .to_string(),
15461 );
15462
15463 cx.set_state(indoc! { "
15464 one
15465 ˇTWO
15466 three
15467 four
15468 five
15469 "});
15470 cx.run_until_parked();
15471 cx.update_editor(|editor, window, cx| {
15472 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15473 });
15474
15475 cx.assert_state_with_diff(
15476 indoc! { "
15477 one
15478 - two
15479 + ˇTWO
15480 three
15481 four
15482 five
15483 "}
15484 .to_string(),
15485 );
15486 cx.update_editor(|editor, window, cx| {
15487 editor.move_up(&Default::default(), window, cx);
15488 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15489 });
15490 cx.assert_state_with_diff(
15491 indoc! { "
15492 one
15493 ˇTWO
15494 three
15495 four
15496 five
15497 "}
15498 .to_string(),
15499 );
15500}
15501
15502#[gpui::test]
15503async fn test_edits_around_expanded_deletion_hunks(
15504 executor: BackgroundExecutor,
15505 cx: &mut TestAppContext,
15506) {
15507 init_test(cx, |_| {});
15508
15509 let mut cx = EditorTestContext::new(cx).await;
15510
15511 let diff_base = r#"
15512 use some::mod1;
15513 use some::mod2;
15514
15515 const A: u32 = 42;
15516 const B: u32 = 42;
15517 const C: u32 = 42;
15518
15519
15520 fn main() {
15521 println!("hello");
15522
15523 println!("world");
15524 }
15525 "#
15526 .unindent();
15527 executor.run_until_parked();
15528 cx.set_state(
15529 &r#"
15530 use some::mod1;
15531 use some::mod2;
15532
15533 ˇconst B: u32 = 42;
15534 const C: u32 = 42;
15535
15536
15537 fn main() {
15538 println!("hello");
15539
15540 println!("world");
15541 }
15542 "#
15543 .unindent(),
15544 );
15545
15546 cx.set_head_text(&diff_base);
15547 executor.run_until_parked();
15548
15549 cx.update_editor(|editor, window, cx| {
15550 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15551 });
15552 executor.run_until_parked();
15553
15554 cx.assert_state_with_diff(
15555 r#"
15556 use some::mod1;
15557 use some::mod2;
15558
15559 - const A: u32 = 42;
15560 ˇconst B: u32 = 42;
15561 const C: u32 = 42;
15562
15563
15564 fn main() {
15565 println!("hello");
15566
15567 println!("world");
15568 }
15569 "#
15570 .unindent(),
15571 );
15572
15573 cx.update_editor(|editor, window, cx| {
15574 editor.delete_line(&DeleteLine, window, cx);
15575 });
15576 executor.run_until_parked();
15577 cx.assert_state_with_diff(
15578 r#"
15579 use some::mod1;
15580 use some::mod2;
15581
15582 - const A: u32 = 42;
15583 - const B: u32 = 42;
15584 ˇconst C: u32 = 42;
15585
15586
15587 fn main() {
15588 println!("hello");
15589
15590 println!("world");
15591 }
15592 "#
15593 .unindent(),
15594 );
15595
15596 cx.update_editor(|editor, window, cx| {
15597 editor.delete_line(&DeleteLine, window, cx);
15598 });
15599 executor.run_until_parked();
15600 cx.assert_state_with_diff(
15601 r#"
15602 use some::mod1;
15603 use some::mod2;
15604
15605 - const A: u32 = 42;
15606 - const B: u32 = 42;
15607 - const C: u32 = 42;
15608 ˇ
15609
15610 fn main() {
15611 println!("hello");
15612
15613 println!("world");
15614 }
15615 "#
15616 .unindent(),
15617 );
15618
15619 cx.update_editor(|editor, window, cx| {
15620 editor.handle_input("replacement", window, cx);
15621 });
15622 executor.run_until_parked();
15623 cx.assert_state_with_diff(
15624 r#"
15625 use some::mod1;
15626 use some::mod2;
15627
15628 - const A: u32 = 42;
15629 - const B: u32 = 42;
15630 - const C: u32 = 42;
15631 -
15632 + replacementˇ
15633
15634 fn main() {
15635 println!("hello");
15636
15637 println!("world");
15638 }
15639 "#
15640 .unindent(),
15641 );
15642}
15643
15644#[gpui::test]
15645async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15646 init_test(cx, |_| {});
15647
15648 let mut cx = EditorTestContext::new(cx).await;
15649
15650 let base_text = r#"
15651 one
15652 two
15653 three
15654 four
15655 five
15656 "#
15657 .unindent();
15658 executor.run_until_parked();
15659 cx.set_state(
15660 &r#"
15661 one
15662 two
15663 fˇour
15664 five
15665 "#
15666 .unindent(),
15667 );
15668
15669 cx.set_head_text(&base_text);
15670 executor.run_until_parked();
15671
15672 cx.update_editor(|editor, window, cx| {
15673 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15674 });
15675 executor.run_until_parked();
15676
15677 cx.assert_state_with_diff(
15678 r#"
15679 one
15680 two
15681 - three
15682 fˇour
15683 five
15684 "#
15685 .unindent(),
15686 );
15687
15688 cx.update_editor(|editor, window, cx| {
15689 editor.backspace(&Backspace, window, cx);
15690 editor.backspace(&Backspace, window, cx);
15691 });
15692 executor.run_until_parked();
15693 cx.assert_state_with_diff(
15694 r#"
15695 one
15696 two
15697 - threeˇ
15698 - four
15699 + our
15700 five
15701 "#
15702 .unindent(),
15703 );
15704}
15705
15706#[gpui::test]
15707async fn test_edit_after_expanded_modification_hunk(
15708 executor: BackgroundExecutor,
15709 cx: &mut TestAppContext,
15710) {
15711 init_test(cx, |_| {});
15712
15713 let mut cx = EditorTestContext::new(cx).await;
15714
15715 let diff_base = r#"
15716 use some::mod1;
15717 use some::mod2;
15718
15719 const A: u32 = 42;
15720 const B: u32 = 42;
15721 const C: u32 = 42;
15722 const D: u32 = 42;
15723
15724
15725 fn main() {
15726 println!("hello");
15727
15728 println!("world");
15729 }"#
15730 .unindent();
15731
15732 cx.set_state(
15733 &r#"
15734 use some::mod1;
15735 use some::mod2;
15736
15737 const A: u32 = 42;
15738 const B: u32 = 42;
15739 const C: u32 = 43ˇ
15740 const D: u32 = 42;
15741
15742
15743 fn main() {
15744 println!("hello");
15745
15746 println!("world");
15747 }"#
15748 .unindent(),
15749 );
15750
15751 cx.set_head_text(&diff_base);
15752 executor.run_until_parked();
15753 cx.update_editor(|editor, window, cx| {
15754 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15755 });
15756 executor.run_until_parked();
15757
15758 cx.assert_state_with_diff(
15759 r#"
15760 use some::mod1;
15761 use some::mod2;
15762
15763 const A: u32 = 42;
15764 const B: u32 = 42;
15765 - const C: u32 = 42;
15766 + const C: u32 = 43ˇ
15767 const D: u32 = 42;
15768
15769
15770 fn main() {
15771 println!("hello");
15772
15773 println!("world");
15774 }"#
15775 .unindent(),
15776 );
15777
15778 cx.update_editor(|editor, window, cx| {
15779 editor.handle_input("\nnew_line\n", window, cx);
15780 });
15781 executor.run_until_parked();
15782
15783 cx.assert_state_with_diff(
15784 r#"
15785 use some::mod1;
15786 use some::mod2;
15787
15788 const A: u32 = 42;
15789 const B: u32 = 42;
15790 - const C: u32 = 42;
15791 + const C: u32 = 43
15792 + new_line
15793 + ˇ
15794 const D: u32 = 42;
15795
15796
15797 fn main() {
15798 println!("hello");
15799
15800 println!("world");
15801 }"#
15802 .unindent(),
15803 );
15804}
15805
15806#[gpui::test]
15807async fn test_stage_and_unstage_added_file_hunk(
15808 executor: BackgroundExecutor,
15809 cx: &mut TestAppContext,
15810) {
15811 init_test(cx, |_| {});
15812
15813 let mut cx = EditorTestContext::new(cx).await;
15814 cx.update_editor(|editor, _, cx| {
15815 editor.set_expand_all_diff_hunks(cx);
15816 });
15817
15818 let working_copy = r#"
15819 ˇfn main() {
15820 println!("hello, world!");
15821 }
15822 "#
15823 .unindent();
15824
15825 cx.set_state(&working_copy);
15826 executor.run_until_parked();
15827
15828 cx.assert_state_with_diff(
15829 r#"
15830 + ˇfn main() {
15831 + println!("hello, world!");
15832 + }
15833 "#
15834 .unindent(),
15835 );
15836 cx.assert_index_text(None);
15837
15838 cx.update_editor(|editor, window, cx| {
15839 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15840 });
15841 executor.run_until_parked();
15842 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15843 cx.assert_state_with_diff(
15844 r#"
15845 + ˇfn main() {
15846 + println!("hello, world!");
15847 + }
15848 "#
15849 .unindent(),
15850 );
15851
15852 cx.update_editor(|editor, window, cx| {
15853 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15854 });
15855 executor.run_until_parked();
15856 cx.assert_index_text(None);
15857}
15858
15859async fn setup_indent_guides_editor(
15860 text: &str,
15861 cx: &mut TestAppContext,
15862) -> (BufferId, EditorTestContext) {
15863 init_test(cx, |_| {});
15864
15865 let mut cx = EditorTestContext::new(cx).await;
15866
15867 let buffer_id = cx.update_editor(|editor, window, cx| {
15868 editor.set_text(text, window, cx);
15869 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15870
15871 buffer_ids[0]
15872 });
15873
15874 (buffer_id, cx)
15875}
15876
15877fn assert_indent_guides(
15878 range: Range<u32>,
15879 expected: Vec<IndentGuide>,
15880 active_indices: Option<Vec<usize>>,
15881 cx: &mut EditorTestContext,
15882) {
15883 let indent_guides = cx.update_editor(|editor, window, cx| {
15884 let snapshot = editor.snapshot(window, cx).display_snapshot;
15885 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15886 editor,
15887 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15888 true,
15889 &snapshot,
15890 cx,
15891 );
15892
15893 indent_guides.sort_by(|a, b| {
15894 a.depth.cmp(&b.depth).then(
15895 a.start_row
15896 .cmp(&b.start_row)
15897 .then(a.end_row.cmp(&b.end_row)),
15898 )
15899 });
15900 indent_guides
15901 });
15902
15903 if let Some(expected) = active_indices {
15904 let active_indices = cx.update_editor(|editor, window, cx| {
15905 let snapshot = editor.snapshot(window, cx).display_snapshot;
15906 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15907 });
15908
15909 assert_eq!(
15910 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15911 expected,
15912 "Active indent guide indices do not match"
15913 );
15914 }
15915
15916 assert_eq!(indent_guides, expected, "Indent guides do not match");
15917}
15918
15919fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15920 IndentGuide {
15921 buffer_id,
15922 start_row: MultiBufferRow(start_row),
15923 end_row: MultiBufferRow(end_row),
15924 depth,
15925 tab_size: 4,
15926 settings: IndentGuideSettings {
15927 enabled: true,
15928 line_width: 1,
15929 active_line_width: 1,
15930 ..Default::default()
15931 },
15932 }
15933}
15934
15935#[gpui::test]
15936async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15937 let (buffer_id, mut cx) = setup_indent_guides_editor(
15938 &"
15939 fn main() {
15940 let a = 1;
15941 }"
15942 .unindent(),
15943 cx,
15944 )
15945 .await;
15946
15947 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15948}
15949
15950#[gpui::test]
15951async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15952 let (buffer_id, mut cx) = setup_indent_guides_editor(
15953 &"
15954 fn main() {
15955 let a = 1;
15956 let b = 2;
15957 }"
15958 .unindent(),
15959 cx,
15960 )
15961 .await;
15962
15963 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15964}
15965
15966#[gpui::test]
15967async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15968 let (buffer_id, mut cx) = setup_indent_guides_editor(
15969 &"
15970 fn main() {
15971 let a = 1;
15972 if a == 3 {
15973 let b = 2;
15974 } else {
15975 let c = 3;
15976 }
15977 }"
15978 .unindent(),
15979 cx,
15980 )
15981 .await;
15982
15983 assert_indent_guides(
15984 0..8,
15985 vec![
15986 indent_guide(buffer_id, 1, 6, 0),
15987 indent_guide(buffer_id, 3, 3, 1),
15988 indent_guide(buffer_id, 5, 5, 1),
15989 ],
15990 None,
15991 &mut cx,
15992 );
15993}
15994
15995#[gpui::test]
15996async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15997 let (buffer_id, mut cx) = setup_indent_guides_editor(
15998 &"
15999 fn main() {
16000 let a = 1;
16001 let b = 2;
16002 let c = 3;
16003 }"
16004 .unindent(),
16005 cx,
16006 )
16007 .await;
16008
16009 assert_indent_guides(
16010 0..5,
16011 vec![
16012 indent_guide(buffer_id, 1, 3, 0),
16013 indent_guide(buffer_id, 2, 2, 1),
16014 ],
16015 None,
16016 &mut cx,
16017 );
16018}
16019
16020#[gpui::test]
16021async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16022 let (buffer_id, mut cx) = setup_indent_guides_editor(
16023 &"
16024 fn main() {
16025 let a = 1;
16026
16027 let c = 3;
16028 }"
16029 .unindent(),
16030 cx,
16031 )
16032 .await;
16033
16034 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16035}
16036
16037#[gpui::test]
16038async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16039 let (buffer_id, mut cx) = setup_indent_guides_editor(
16040 &"
16041 fn main() {
16042 let a = 1;
16043
16044 let c = 3;
16045
16046 if a == 3 {
16047 let b = 2;
16048 } else {
16049 let c = 3;
16050 }
16051 }"
16052 .unindent(),
16053 cx,
16054 )
16055 .await;
16056
16057 assert_indent_guides(
16058 0..11,
16059 vec![
16060 indent_guide(buffer_id, 1, 9, 0),
16061 indent_guide(buffer_id, 6, 6, 1),
16062 indent_guide(buffer_id, 8, 8, 1),
16063 ],
16064 None,
16065 &mut cx,
16066 );
16067}
16068
16069#[gpui::test]
16070async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16071 let (buffer_id, mut cx) = setup_indent_guides_editor(
16072 &"
16073 fn main() {
16074 let a = 1;
16075
16076 let c = 3;
16077
16078 if a == 3 {
16079 let b = 2;
16080 } else {
16081 let c = 3;
16082 }
16083 }"
16084 .unindent(),
16085 cx,
16086 )
16087 .await;
16088
16089 assert_indent_guides(
16090 1..11,
16091 vec![
16092 indent_guide(buffer_id, 1, 9, 0),
16093 indent_guide(buffer_id, 6, 6, 1),
16094 indent_guide(buffer_id, 8, 8, 1),
16095 ],
16096 None,
16097 &mut cx,
16098 );
16099}
16100
16101#[gpui::test]
16102async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16103 let (buffer_id, mut cx) = setup_indent_guides_editor(
16104 &"
16105 fn main() {
16106 let a = 1;
16107
16108 let c = 3;
16109
16110 if a == 3 {
16111 let b = 2;
16112 } else {
16113 let c = 3;
16114 }
16115 }"
16116 .unindent(),
16117 cx,
16118 )
16119 .await;
16120
16121 assert_indent_guides(
16122 1..10,
16123 vec![
16124 indent_guide(buffer_id, 1, 9, 0),
16125 indent_guide(buffer_id, 6, 6, 1),
16126 indent_guide(buffer_id, 8, 8, 1),
16127 ],
16128 None,
16129 &mut cx,
16130 );
16131}
16132
16133#[gpui::test]
16134async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16135 let (buffer_id, mut cx) = setup_indent_guides_editor(
16136 &"
16137 block1
16138 block2
16139 block3
16140 block4
16141 block2
16142 block1
16143 block1"
16144 .unindent(),
16145 cx,
16146 )
16147 .await;
16148
16149 assert_indent_guides(
16150 1..10,
16151 vec![
16152 indent_guide(buffer_id, 1, 4, 0),
16153 indent_guide(buffer_id, 2, 3, 1),
16154 indent_guide(buffer_id, 3, 3, 2),
16155 ],
16156 None,
16157 &mut cx,
16158 );
16159}
16160
16161#[gpui::test]
16162async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16163 let (buffer_id, mut cx) = setup_indent_guides_editor(
16164 &"
16165 block1
16166 block2
16167 block3
16168
16169 block1
16170 block1"
16171 .unindent(),
16172 cx,
16173 )
16174 .await;
16175
16176 assert_indent_guides(
16177 0..6,
16178 vec![
16179 indent_guide(buffer_id, 1, 2, 0),
16180 indent_guide(buffer_id, 2, 2, 1),
16181 ],
16182 None,
16183 &mut cx,
16184 );
16185}
16186
16187#[gpui::test]
16188async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16189 let (buffer_id, mut cx) = setup_indent_guides_editor(
16190 &"
16191 block1
16192
16193
16194
16195 block2
16196 "
16197 .unindent(),
16198 cx,
16199 )
16200 .await;
16201
16202 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16203}
16204
16205#[gpui::test]
16206async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16207 let (buffer_id, mut cx) = setup_indent_guides_editor(
16208 &"
16209 def a:
16210 \tb = 3
16211 \tif True:
16212 \t\tc = 4
16213 \t\td = 5
16214 \tprint(b)
16215 "
16216 .unindent(),
16217 cx,
16218 )
16219 .await;
16220
16221 assert_indent_guides(
16222 0..6,
16223 vec![
16224 indent_guide(buffer_id, 1, 6, 0),
16225 indent_guide(buffer_id, 3, 4, 1),
16226 ],
16227 None,
16228 &mut cx,
16229 );
16230}
16231
16232#[gpui::test]
16233async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16234 let (buffer_id, mut cx) = setup_indent_guides_editor(
16235 &"
16236 fn main() {
16237 let a = 1;
16238 }"
16239 .unindent(),
16240 cx,
16241 )
16242 .await;
16243
16244 cx.update_editor(|editor, window, cx| {
16245 editor.change_selections(None, window, cx, |s| {
16246 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16247 });
16248 });
16249
16250 assert_indent_guides(
16251 0..3,
16252 vec![indent_guide(buffer_id, 1, 1, 0)],
16253 Some(vec![0]),
16254 &mut cx,
16255 );
16256}
16257
16258#[gpui::test]
16259async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16260 let (buffer_id, mut cx) = setup_indent_guides_editor(
16261 &"
16262 fn main() {
16263 if 1 == 2 {
16264 let a = 1;
16265 }
16266 }"
16267 .unindent(),
16268 cx,
16269 )
16270 .await;
16271
16272 cx.update_editor(|editor, window, cx| {
16273 editor.change_selections(None, window, cx, |s| {
16274 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16275 });
16276 });
16277
16278 assert_indent_guides(
16279 0..4,
16280 vec![
16281 indent_guide(buffer_id, 1, 3, 0),
16282 indent_guide(buffer_id, 2, 2, 1),
16283 ],
16284 Some(vec![1]),
16285 &mut cx,
16286 );
16287
16288 cx.update_editor(|editor, window, cx| {
16289 editor.change_selections(None, window, cx, |s| {
16290 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16291 });
16292 });
16293
16294 assert_indent_guides(
16295 0..4,
16296 vec![
16297 indent_guide(buffer_id, 1, 3, 0),
16298 indent_guide(buffer_id, 2, 2, 1),
16299 ],
16300 Some(vec![1]),
16301 &mut cx,
16302 );
16303
16304 cx.update_editor(|editor, window, cx| {
16305 editor.change_selections(None, window, cx, |s| {
16306 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16307 });
16308 });
16309
16310 assert_indent_guides(
16311 0..4,
16312 vec![
16313 indent_guide(buffer_id, 1, 3, 0),
16314 indent_guide(buffer_id, 2, 2, 1),
16315 ],
16316 Some(vec![0]),
16317 &mut cx,
16318 );
16319}
16320
16321#[gpui::test]
16322async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16323 let (buffer_id, mut cx) = setup_indent_guides_editor(
16324 &"
16325 fn main() {
16326 let a = 1;
16327
16328 let b = 2;
16329 }"
16330 .unindent(),
16331 cx,
16332 )
16333 .await;
16334
16335 cx.update_editor(|editor, window, cx| {
16336 editor.change_selections(None, window, cx, |s| {
16337 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16338 });
16339 });
16340
16341 assert_indent_guides(
16342 0..5,
16343 vec![indent_guide(buffer_id, 1, 3, 0)],
16344 Some(vec![0]),
16345 &mut cx,
16346 );
16347}
16348
16349#[gpui::test]
16350async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16351 let (buffer_id, mut cx) = setup_indent_guides_editor(
16352 &"
16353 def m:
16354 a = 1
16355 pass"
16356 .unindent(),
16357 cx,
16358 )
16359 .await;
16360
16361 cx.update_editor(|editor, window, cx| {
16362 editor.change_selections(None, window, cx, |s| {
16363 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16364 });
16365 });
16366
16367 assert_indent_guides(
16368 0..3,
16369 vec![indent_guide(buffer_id, 1, 2, 0)],
16370 Some(vec![0]),
16371 &mut cx,
16372 );
16373}
16374
16375#[gpui::test]
16376async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16377 init_test(cx, |_| {});
16378 let mut cx = EditorTestContext::new(cx).await;
16379 let text = indoc! {
16380 "
16381 impl A {
16382 fn b() {
16383 0;
16384 3;
16385 5;
16386 6;
16387 7;
16388 }
16389 }
16390 "
16391 };
16392 let base_text = indoc! {
16393 "
16394 impl A {
16395 fn b() {
16396 0;
16397 1;
16398 2;
16399 3;
16400 4;
16401 }
16402 fn c() {
16403 5;
16404 6;
16405 7;
16406 }
16407 }
16408 "
16409 };
16410
16411 cx.update_editor(|editor, window, cx| {
16412 editor.set_text(text, window, cx);
16413
16414 editor.buffer().update(cx, |multibuffer, cx| {
16415 let buffer = multibuffer.as_singleton().unwrap();
16416 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16417
16418 multibuffer.set_all_diff_hunks_expanded(cx);
16419 multibuffer.add_diff(diff, cx);
16420
16421 buffer.read(cx).remote_id()
16422 })
16423 });
16424 cx.run_until_parked();
16425
16426 cx.assert_state_with_diff(
16427 indoc! { "
16428 impl A {
16429 fn b() {
16430 0;
16431 - 1;
16432 - 2;
16433 3;
16434 - 4;
16435 - }
16436 - fn c() {
16437 5;
16438 6;
16439 7;
16440 }
16441 }
16442 ˇ"
16443 }
16444 .to_string(),
16445 );
16446
16447 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16448 editor
16449 .snapshot(window, cx)
16450 .buffer_snapshot
16451 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16452 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16453 .collect::<Vec<_>>()
16454 });
16455 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16456 assert_eq!(
16457 actual_guides,
16458 vec![
16459 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16460 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16461 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16462 ]
16463 );
16464}
16465
16466#[gpui::test]
16467async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16468 init_test(cx, |_| {});
16469 let mut cx = EditorTestContext::new(cx).await;
16470
16471 let diff_base = r#"
16472 a
16473 b
16474 c
16475 "#
16476 .unindent();
16477
16478 cx.set_state(
16479 &r#"
16480 ˇA
16481 b
16482 C
16483 "#
16484 .unindent(),
16485 );
16486 cx.set_head_text(&diff_base);
16487 cx.update_editor(|editor, window, cx| {
16488 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16489 });
16490 executor.run_until_parked();
16491
16492 let both_hunks_expanded = r#"
16493 - a
16494 + ˇA
16495 b
16496 - c
16497 + C
16498 "#
16499 .unindent();
16500
16501 cx.assert_state_with_diff(both_hunks_expanded.clone());
16502
16503 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16504 let snapshot = editor.snapshot(window, cx);
16505 let hunks = editor
16506 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16507 .collect::<Vec<_>>();
16508 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16509 let buffer_id = hunks[0].buffer_id;
16510 hunks
16511 .into_iter()
16512 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16513 .collect::<Vec<_>>()
16514 });
16515 assert_eq!(hunk_ranges.len(), 2);
16516
16517 cx.update_editor(|editor, _, cx| {
16518 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16519 });
16520 executor.run_until_parked();
16521
16522 let second_hunk_expanded = r#"
16523 ˇA
16524 b
16525 - c
16526 + C
16527 "#
16528 .unindent();
16529
16530 cx.assert_state_with_diff(second_hunk_expanded);
16531
16532 cx.update_editor(|editor, _, cx| {
16533 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16534 });
16535 executor.run_until_parked();
16536
16537 cx.assert_state_with_diff(both_hunks_expanded.clone());
16538
16539 cx.update_editor(|editor, _, cx| {
16540 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16541 });
16542 executor.run_until_parked();
16543
16544 let first_hunk_expanded = r#"
16545 - a
16546 + ˇA
16547 b
16548 C
16549 "#
16550 .unindent();
16551
16552 cx.assert_state_with_diff(first_hunk_expanded);
16553
16554 cx.update_editor(|editor, _, cx| {
16555 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16556 });
16557 executor.run_until_parked();
16558
16559 cx.assert_state_with_diff(both_hunks_expanded);
16560
16561 cx.set_state(
16562 &r#"
16563 ˇA
16564 b
16565 "#
16566 .unindent(),
16567 );
16568 cx.run_until_parked();
16569
16570 // TODO this cursor position seems bad
16571 cx.assert_state_with_diff(
16572 r#"
16573 - ˇa
16574 + A
16575 b
16576 "#
16577 .unindent(),
16578 );
16579
16580 cx.update_editor(|editor, window, cx| {
16581 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16582 });
16583
16584 cx.assert_state_with_diff(
16585 r#"
16586 - ˇa
16587 + A
16588 b
16589 - c
16590 "#
16591 .unindent(),
16592 );
16593
16594 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16595 let snapshot = editor.snapshot(window, cx);
16596 let hunks = editor
16597 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16598 .collect::<Vec<_>>();
16599 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16600 let buffer_id = hunks[0].buffer_id;
16601 hunks
16602 .into_iter()
16603 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16604 .collect::<Vec<_>>()
16605 });
16606 assert_eq!(hunk_ranges.len(), 2);
16607
16608 cx.update_editor(|editor, _, cx| {
16609 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16610 });
16611 executor.run_until_parked();
16612
16613 cx.assert_state_with_diff(
16614 r#"
16615 - ˇa
16616 + A
16617 b
16618 "#
16619 .unindent(),
16620 );
16621}
16622
16623#[gpui::test]
16624async fn test_toggle_deletion_hunk_at_start_of_file(
16625 executor: BackgroundExecutor,
16626 cx: &mut TestAppContext,
16627) {
16628 init_test(cx, |_| {});
16629 let mut cx = EditorTestContext::new(cx).await;
16630
16631 let diff_base = r#"
16632 a
16633 b
16634 c
16635 "#
16636 .unindent();
16637
16638 cx.set_state(
16639 &r#"
16640 ˇb
16641 c
16642 "#
16643 .unindent(),
16644 );
16645 cx.set_head_text(&diff_base);
16646 cx.update_editor(|editor, window, cx| {
16647 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16648 });
16649 executor.run_until_parked();
16650
16651 let hunk_expanded = r#"
16652 - a
16653 ˇb
16654 c
16655 "#
16656 .unindent();
16657
16658 cx.assert_state_with_diff(hunk_expanded.clone());
16659
16660 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16661 let snapshot = editor.snapshot(window, cx);
16662 let hunks = editor
16663 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16664 .collect::<Vec<_>>();
16665 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16666 let buffer_id = hunks[0].buffer_id;
16667 hunks
16668 .into_iter()
16669 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16670 .collect::<Vec<_>>()
16671 });
16672 assert_eq!(hunk_ranges.len(), 1);
16673
16674 cx.update_editor(|editor, _, cx| {
16675 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16676 });
16677 executor.run_until_parked();
16678
16679 let hunk_collapsed = r#"
16680 ˇb
16681 c
16682 "#
16683 .unindent();
16684
16685 cx.assert_state_with_diff(hunk_collapsed);
16686
16687 cx.update_editor(|editor, _, cx| {
16688 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16689 });
16690 executor.run_until_parked();
16691
16692 cx.assert_state_with_diff(hunk_expanded.clone());
16693}
16694
16695#[gpui::test]
16696async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16697 init_test(cx, |_| {});
16698
16699 let fs = FakeFs::new(cx.executor());
16700 fs.insert_tree(
16701 path!("/test"),
16702 json!({
16703 ".git": {},
16704 "file-1": "ONE\n",
16705 "file-2": "TWO\n",
16706 "file-3": "THREE\n",
16707 }),
16708 )
16709 .await;
16710
16711 fs.set_head_for_repo(
16712 path!("/test/.git").as_ref(),
16713 &[
16714 ("file-1".into(), "one\n".into()),
16715 ("file-2".into(), "two\n".into()),
16716 ("file-3".into(), "three\n".into()),
16717 ],
16718 );
16719
16720 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16721 let mut buffers = vec![];
16722 for i in 1..=3 {
16723 let buffer = project
16724 .update(cx, |project, cx| {
16725 let path = format!(path!("/test/file-{}"), i);
16726 project.open_local_buffer(path, cx)
16727 })
16728 .await
16729 .unwrap();
16730 buffers.push(buffer);
16731 }
16732
16733 let multibuffer = cx.new(|cx| {
16734 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16735 multibuffer.set_all_diff_hunks_expanded(cx);
16736 for buffer in &buffers {
16737 let snapshot = buffer.read(cx).snapshot();
16738 multibuffer.set_excerpts_for_path(
16739 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16740 buffer.clone(),
16741 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16742 DEFAULT_MULTIBUFFER_CONTEXT,
16743 cx,
16744 );
16745 }
16746 multibuffer
16747 });
16748
16749 let editor = cx.add_window(|window, cx| {
16750 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
16751 });
16752 cx.run_until_parked();
16753
16754 let snapshot = editor
16755 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16756 .unwrap();
16757 let hunks = snapshot
16758 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16759 .map(|hunk| match hunk {
16760 DisplayDiffHunk::Unfolded {
16761 display_row_range, ..
16762 } => display_row_range,
16763 DisplayDiffHunk::Folded { .. } => unreachable!(),
16764 })
16765 .collect::<Vec<_>>();
16766 assert_eq!(
16767 hunks,
16768 [
16769 DisplayRow(2)..DisplayRow(4),
16770 DisplayRow(7)..DisplayRow(9),
16771 DisplayRow(12)..DisplayRow(14),
16772 ]
16773 );
16774}
16775
16776#[gpui::test]
16777async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16778 init_test(cx, |_| {});
16779
16780 let mut cx = EditorTestContext::new(cx).await;
16781 cx.set_head_text(indoc! { "
16782 one
16783 two
16784 three
16785 four
16786 five
16787 "
16788 });
16789 cx.set_index_text(indoc! { "
16790 one
16791 two
16792 three
16793 four
16794 five
16795 "
16796 });
16797 cx.set_state(indoc! {"
16798 one
16799 TWO
16800 ˇTHREE
16801 FOUR
16802 five
16803 "});
16804 cx.run_until_parked();
16805 cx.update_editor(|editor, window, cx| {
16806 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16807 });
16808 cx.run_until_parked();
16809 cx.assert_index_text(Some(indoc! {"
16810 one
16811 TWO
16812 THREE
16813 FOUR
16814 five
16815 "}));
16816 cx.set_state(indoc! { "
16817 one
16818 TWO
16819 ˇTHREE-HUNDRED
16820 FOUR
16821 five
16822 "});
16823 cx.run_until_parked();
16824 cx.update_editor(|editor, window, cx| {
16825 let snapshot = editor.snapshot(window, cx);
16826 let hunks = editor
16827 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16828 .collect::<Vec<_>>();
16829 assert_eq!(hunks.len(), 1);
16830 assert_eq!(
16831 hunks[0].status(),
16832 DiffHunkStatus {
16833 kind: DiffHunkStatusKind::Modified,
16834 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16835 }
16836 );
16837
16838 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16839 });
16840 cx.run_until_parked();
16841 cx.assert_index_text(Some(indoc! {"
16842 one
16843 TWO
16844 THREE-HUNDRED
16845 FOUR
16846 five
16847 "}));
16848}
16849
16850#[gpui::test]
16851fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16852 init_test(cx, |_| {});
16853
16854 let editor = cx.add_window(|window, cx| {
16855 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16856 build_editor(buffer, window, cx)
16857 });
16858
16859 let render_args = Arc::new(Mutex::new(None));
16860 let snapshot = editor
16861 .update(cx, |editor, window, cx| {
16862 let snapshot = editor.buffer().read(cx).snapshot(cx);
16863 let range =
16864 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16865
16866 struct RenderArgs {
16867 row: MultiBufferRow,
16868 folded: bool,
16869 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16870 }
16871
16872 let crease = Crease::inline(
16873 range,
16874 FoldPlaceholder::test(),
16875 {
16876 let toggle_callback = render_args.clone();
16877 move |row, folded, callback, _window, _cx| {
16878 *toggle_callback.lock() = Some(RenderArgs {
16879 row,
16880 folded,
16881 callback,
16882 });
16883 div()
16884 }
16885 },
16886 |_row, _folded, _window, _cx| div(),
16887 );
16888
16889 editor.insert_creases(Some(crease), cx);
16890 let snapshot = editor.snapshot(window, cx);
16891 let _div = snapshot.render_crease_toggle(
16892 MultiBufferRow(1),
16893 false,
16894 cx.entity().clone(),
16895 window,
16896 cx,
16897 );
16898 snapshot
16899 })
16900 .unwrap();
16901
16902 let render_args = render_args.lock().take().unwrap();
16903 assert_eq!(render_args.row, MultiBufferRow(1));
16904 assert!(!render_args.folded);
16905 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16906
16907 cx.update_window(*editor, |_, window, cx| {
16908 (render_args.callback)(true, window, cx)
16909 })
16910 .unwrap();
16911 let snapshot = editor
16912 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16913 .unwrap();
16914 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16915
16916 cx.update_window(*editor, |_, window, cx| {
16917 (render_args.callback)(false, window, cx)
16918 })
16919 .unwrap();
16920 let snapshot = editor
16921 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16922 .unwrap();
16923 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16924}
16925
16926#[gpui::test]
16927async fn test_input_text(cx: &mut TestAppContext) {
16928 init_test(cx, |_| {});
16929 let mut cx = EditorTestContext::new(cx).await;
16930
16931 cx.set_state(
16932 &r#"ˇone
16933 two
16934
16935 three
16936 fourˇ
16937 five
16938
16939 siˇx"#
16940 .unindent(),
16941 );
16942
16943 cx.dispatch_action(HandleInput(String::new()));
16944 cx.assert_editor_state(
16945 &r#"ˇone
16946 two
16947
16948 three
16949 fourˇ
16950 five
16951
16952 siˇx"#
16953 .unindent(),
16954 );
16955
16956 cx.dispatch_action(HandleInput("AAAA".to_string()));
16957 cx.assert_editor_state(
16958 &r#"AAAAˇone
16959 two
16960
16961 three
16962 fourAAAAˇ
16963 five
16964
16965 siAAAAˇx"#
16966 .unindent(),
16967 );
16968}
16969
16970#[gpui::test]
16971async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16972 init_test(cx, |_| {});
16973
16974 let mut cx = EditorTestContext::new(cx).await;
16975 cx.set_state(
16976 r#"let foo = 1;
16977let foo = 2;
16978let foo = 3;
16979let fooˇ = 4;
16980let foo = 5;
16981let foo = 6;
16982let foo = 7;
16983let foo = 8;
16984let foo = 9;
16985let foo = 10;
16986let foo = 11;
16987let foo = 12;
16988let foo = 13;
16989let foo = 14;
16990let foo = 15;"#,
16991 );
16992
16993 cx.update_editor(|e, window, cx| {
16994 assert_eq!(
16995 e.next_scroll_position,
16996 NextScrollCursorCenterTopBottom::Center,
16997 "Default next scroll direction is center",
16998 );
16999
17000 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17001 assert_eq!(
17002 e.next_scroll_position,
17003 NextScrollCursorCenterTopBottom::Top,
17004 "After center, next scroll direction should be top",
17005 );
17006
17007 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17008 assert_eq!(
17009 e.next_scroll_position,
17010 NextScrollCursorCenterTopBottom::Bottom,
17011 "After top, next scroll direction should be bottom",
17012 );
17013
17014 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17015 assert_eq!(
17016 e.next_scroll_position,
17017 NextScrollCursorCenterTopBottom::Center,
17018 "After bottom, scrolling should start over",
17019 );
17020
17021 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17022 assert_eq!(
17023 e.next_scroll_position,
17024 NextScrollCursorCenterTopBottom::Top,
17025 "Scrolling continues if retriggered fast enough"
17026 );
17027 });
17028
17029 cx.executor()
17030 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17031 cx.executor().run_until_parked();
17032 cx.update_editor(|e, _, _| {
17033 assert_eq!(
17034 e.next_scroll_position,
17035 NextScrollCursorCenterTopBottom::Center,
17036 "If scrolling is not triggered fast enough, it should reset"
17037 );
17038 });
17039}
17040
17041#[gpui::test]
17042async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17043 init_test(cx, |_| {});
17044 let mut cx = EditorLspTestContext::new_rust(
17045 lsp::ServerCapabilities {
17046 definition_provider: Some(lsp::OneOf::Left(true)),
17047 references_provider: Some(lsp::OneOf::Left(true)),
17048 ..lsp::ServerCapabilities::default()
17049 },
17050 cx,
17051 )
17052 .await;
17053
17054 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17055 let go_to_definition = cx
17056 .lsp
17057 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17058 move |params, _| async move {
17059 if empty_go_to_definition {
17060 Ok(None)
17061 } else {
17062 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17063 uri: params.text_document_position_params.text_document.uri,
17064 range: lsp::Range::new(
17065 lsp::Position::new(4, 3),
17066 lsp::Position::new(4, 6),
17067 ),
17068 })))
17069 }
17070 },
17071 );
17072 let references = cx
17073 .lsp
17074 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17075 Ok(Some(vec![lsp::Location {
17076 uri: params.text_document_position.text_document.uri,
17077 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17078 }]))
17079 });
17080 (go_to_definition, references)
17081 };
17082
17083 cx.set_state(
17084 &r#"fn one() {
17085 let mut a = ˇtwo();
17086 }
17087
17088 fn two() {}"#
17089 .unindent(),
17090 );
17091 set_up_lsp_handlers(false, &mut cx);
17092 let navigated = cx
17093 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17094 .await
17095 .expect("Failed to navigate to definition");
17096 assert_eq!(
17097 navigated,
17098 Navigated::Yes,
17099 "Should have navigated to definition from the GetDefinition response"
17100 );
17101 cx.assert_editor_state(
17102 &r#"fn one() {
17103 let mut a = two();
17104 }
17105
17106 fn «twoˇ»() {}"#
17107 .unindent(),
17108 );
17109
17110 let editors = cx.update_workspace(|workspace, _, cx| {
17111 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17112 });
17113 cx.update_editor(|_, _, test_editor_cx| {
17114 assert_eq!(
17115 editors.len(),
17116 1,
17117 "Initially, only one, test, editor should be open in the workspace"
17118 );
17119 assert_eq!(
17120 test_editor_cx.entity(),
17121 editors.last().expect("Asserted len is 1").clone()
17122 );
17123 });
17124
17125 set_up_lsp_handlers(true, &mut cx);
17126 let navigated = cx
17127 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17128 .await
17129 .expect("Failed to navigate to lookup references");
17130 assert_eq!(
17131 navigated,
17132 Navigated::Yes,
17133 "Should have navigated to references as a fallback after empty GoToDefinition response"
17134 );
17135 // We should not change the selections in the existing file,
17136 // if opening another milti buffer with the references
17137 cx.assert_editor_state(
17138 &r#"fn one() {
17139 let mut a = two();
17140 }
17141
17142 fn «twoˇ»() {}"#
17143 .unindent(),
17144 );
17145 let editors = cx.update_workspace(|workspace, _, cx| {
17146 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17147 });
17148 cx.update_editor(|_, _, test_editor_cx| {
17149 assert_eq!(
17150 editors.len(),
17151 2,
17152 "After falling back to references search, we open a new editor with the results"
17153 );
17154 let references_fallback_text = editors
17155 .into_iter()
17156 .find(|new_editor| *new_editor != test_editor_cx.entity())
17157 .expect("Should have one non-test editor now")
17158 .read(test_editor_cx)
17159 .text(test_editor_cx);
17160 assert_eq!(
17161 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17162 "Should use the range from the references response and not the GoToDefinition one"
17163 );
17164 });
17165}
17166
17167#[gpui::test]
17168async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17169 init_test(cx, |_| {});
17170 cx.update(|cx| {
17171 let mut editor_settings = EditorSettings::get_global(cx).clone();
17172 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17173 EditorSettings::override_global(editor_settings, cx);
17174 });
17175 let mut cx = EditorLspTestContext::new_rust(
17176 lsp::ServerCapabilities {
17177 definition_provider: Some(lsp::OneOf::Left(true)),
17178 references_provider: Some(lsp::OneOf::Left(true)),
17179 ..lsp::ServerCapabilities::default()
17180 },
17181 cx,
17182 )
17183 .await;
17184 let original_state = r#"fn one() {
17185 let mut a = ˇtwo();
17186 }
17187
17188 fn two() {}"#
17189 .unindent();
17190 cx.set_state(&original_state);
17191
17192 let mut go_to_definition = cx
17193 .lsp
17194 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17195 move |_, _| async move { Ok(None) },
17196 );
17197 let _references = cx
17198 .lsp
17199 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17200 panic!("Should not call for references with no go to definition fallback")
17201 });
17202
17203 let navigated = cx
17204 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17205 .await
17206 .expect("Failed to navigate to lookup references");
17207 go_to_definition
17208 .next()
17209 .await
17210 .expect("Should have called the go_to_definition handler");
17211
17212 assert_eq!(
17213 navigated,
17214 Navigated::No,
17215 "Should have navigated to references as a fallback after empty GoToDefinition response"
17216 );
17217 cx.assert_editor_state(&original_state);
17218 let editors = cx.update_workspace(|workspace, _, cx| {
17219 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17220 });
17221 cx.update_editor(|_, _, _| {
17222 assert_eq!(
17223 editors.len(),
17224 1,
17225 "After unsuccessful fallback, no other editor should have been opened"
17226 );
17227 });
17228}
17229
17230#[gpui::test]
17231async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17232 init_test(cx, |_| {});
17233
17234 let language = Arc::new(Language::new(
17235 LanguageConfig::default(),
17236 Some(tree_sitter_rust::LANGUAGE.into()),
17237 ));
17238
17239 let text = r#"
17240 #[cfg(test)]
17241 mod tests() {
17242 #[test]
17243 fn runnable_1() {
17244 let a = 1;
17245 }
17246
17247 #[test]
17248 fn runnable_2() {
17249 let a = 1;
17250 let b = 2;
17251 }
17252 }
17253 "#
17254 .unindent();
17255
17256 let fs = FakeFs::new(cx.executor());
17257 fs.insert_file("/file.rs", Default::default()).await;
17258
17259 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17260 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17261 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17262 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17263 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17264
17265 let editor = cx.new_window_entity(|window, cx| {
17266 Editor::new(
17267 EditorMode::full(),
17268 multi_buffer,
17269 Some(project.clone()),
17270 window,
17271 cx,
17272 )
17273 });
17274
17275 editor.update_in(cx, |editor, window, cx| {
17276 let snapshot = editor.buffer().read(cx).snapshot(cx);
17277 editor.tasks.insert(
17278 (buffer.read(cx).remote_id(), 3),
17279 RunnableTasks {
17280 templates: vec![],
17281 offset: snapshot.anchor_before(43),
17282 column: 0,
17283 extra_variables: HashMap::default(),
17284 context_range: BufferOffset(43)..BufferOffset(85),
17285 },
17286 );
17287 editor.tasks.insert(
17288 (buffer.read(cx).remote_id(), 8),
17289 RunnableTasks {
17290 templates: vec![],
17291 offset: snapshot.anchor_before(86),
17292 column: 0,
17293 extra_variables: HashMap::default(),
17294 context_range: BufferOffset(86)..BufferOffset(191),
17295 },
17296 );
17297
17298 // Test finding task when cursor is inside function body
17299 editor.change_selections(None, window, cx, |s| {
17300 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17301 });
17302 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17303 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17304
17305 // Test finding task when cursor is on function name
17306 editor.change_selections(None, window, cx, |s| {
17307 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17308 });
17309 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17310 assert_eq!(row, 8, "Should find task when cursor is on function name");
17311 });
17312}
17313
17314#[gpui::test]
17315async fn test_folding_buffers(cx: &mut TestAppContext) {
17316 init_test(cx, |_| {});
17317
17318 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17319 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17320 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17321
17322 let fs = FakeFs::new(cx.executor());
17323 fs.insert_tree(
17324 path!("/a"),
17325 json!({
17326 "first.rs": sample_text_1,
17327 "second.rs": sample_text_2,
17328 "third.rs": sample_text_3,
17329 }),
17330 )
17331 .await;
17332 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17333 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17334 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17335 let worktree = project.update(cx, |project, cx| {
17336 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17337 assert_eq!(worktrees.len(), 1);
17338 worktrees.pop().unwrap()
17339 });
17340 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17341
17342 let buffer_1 = project
17343 .update(cx, |project, cx| {
17344 project.open_buffer((worktree_id, "first.rs"), cx)
17345 })
17346 .await
17347 .unwrap();
17348 let buffer_2 = project
17349 .update(cx, |project, cx| {
17350 project.open_buffer((worktree_id, "second.rs"), cx)
17351 })
17352 .await
17353 .unwrap();
17354 let buffer_3 = project
17355 .update(cx, |project, cx| {
17356 project.open_buffer((worktree_id, "third.rs"), cx)
17357 })
17358 .await
17359 .unwrap();
17360
17361 let multi_buffer = cx.new(|cx| {
17362 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17363 multi_buffer.push_excerpts(
17364 buffer_1.clone(),
17365 [
17366 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17367 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17368 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17369 ],
17370 cx,
17371 );
17372 multi_buffer.push_excerpts(
17373 buffer_2.clone(),
17374 [
17375 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17376 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17377 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17378 ],
17379 cx,
17380 );
17381 multi_buffer.push_excerpts(
17382 buffer_3.clone(),
17383 [
17384 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17385 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17386 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17387 ],
17388 cx,
17389 );
17390 multi_buffer
17391 });
17392 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17393 Editor::new(
17394 EditorMode::full(),
17395 multi_buffer.clone(),
17396 Some(project.clone()),
17397 window,
17398 cx,
17399 )
17400 });
17401
17402 assert_eq!(
17403 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17404 "\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",
17405 );
17406
17407 multi_buffer_editor.update(cx, |editor, cx| {
17408 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17409 });
17410 assert_eq!(
17411 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17412 "\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",
17413 "After folding the first buffer, its text should not be displayed"
17414 );
17415
17416 multi_buffer_editor.update(cx, |editor, cx| {
17417 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17418 });
17419 assert_eq!(
17420 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17421 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17422 "After folding the second buffer, its text should not be displayed"
17423 );
17424
17425 multi_buffer_editor.update(cx, |editor, cx| {
17426 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17427 });
17428 assert_eq!(
17429 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17430 "\n\n\n\n\n",
17431 "After folding the third buffer, its text should not be displayed"
17432 );
17433
17434 // Emulate selection inside the fold logic, that should work
17435 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17436 editor
17437 .snapshot(window, cx)
17438 .next_line_boundary(Point::new(0, 4));
17439 });
17440
17441 multi_buffer_editor.update(cx, |editor, cx| {
17442 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17443 });
17444 assert_eq!(
17445 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17446 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17447 "After unfolding the second buffer, its text should be displayed"
17448 );
17449
17450 // Typing inside of buffer 1 causes that buffer to be unfolded.
17451 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17452 assert_eq!(
17453 multi_buffer
17454 .read(cx)
17455 .snapshot(cx)
17456 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17457 .collect::<String>(),
17458 "bbbb"
17459 );
17460 editor.change_selections(None, window, cx, |selections| {
17461 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17462 });
17463 editor.handle_input("B", window, cx);
17464 });
17465
17466 assert_eq!(
17467 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17468 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17469 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17470 );
17471
17472 multi_buffer_editor.update(cx, |editor, cx| {
17473 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17474 });
17475 assert_eq!(
17476 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17477 "\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",
17478 "After unfolding the all buffers, all original text should be displayed"
17479 );
17480}
17481
17482#[gpui::test]
17483async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17484 init_test(cx, |_| {});
17485
17486 let sample_text_1 = "1111\n2222\n3333".to_string();
17487 let sample_text_2 = "4444\n5555\n6666".to_string();
17488 let sample_text_3 = "7777\n8888\n9999".to_string();
17489
17490 let fs = FakeFs::new(cx.executor());
17491 fs.insert_tree(
17492 path!("/a"),
17493 json!({
17494 "first.rs": sample_text_1,
17495 "second.rs": sample_text_2,
17496 "third.rs": sample_text_3,
17497 }),
17498 )
17499 .await;
17500 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17501 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17502 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17503 let worktree = project.update(cx, |project, cx| {
17504 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17505 assert_eq!(worktrees.len(), 1);
17506 worktrees.pop().unwrap()
17507 });
17508 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17509
17510 let buffer_1 = project
17511 .update(cx, |project, cx| {
17512 project.open_buffer((worktree_id, "first.rs"), cx)
17513 })
17514 .await
17515 .unwrap();
17516 let buffer_2 = project
17517 .update(cx, |project, cx| {
17518 project.open_buffer((worktree_id, "second.rs"), cx)
17519 })
17520 .await
17521 .unwrap();
17522 let buffer_3 = project
17523 .update(cx, |project, cx| {
17524 project.open_buffer((worktree_id, "third.rs"), cx)
17525 })
17526 .await
17527 .unwrap();
17528
17529 let multi_buffer = cx.new(|cx| {
17530 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17531 multi_buffer.push_excerpts(
17532 buffer_1.clone(),
17533 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17534 cx,
17535 );
17536 multi_buffer.push_excerpts(
17537 buffer_2.clone(),
17538 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17539 cx,
17540 );
17541 multi_buffer.push_excerpts(
17542 buffer_3.clone(),
17543 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17544 cx,
17545 );
17546 multi_buffer
17547 });
17548
17549 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17550 Editor::new(
17551 EditorMode::full(),
17552 multi_buffer,
17553 Some(project.clone()),
17554 window,
17555 cx,
17556 )
17557 });
17558
17559 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17560 assert_eq!(
17561 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17562 full_text,
17563 );
17564
17565 multi_buffer_editor.update(cx, |editor, cx| {
17566 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17567 });
17568 assert_eq!(
17569 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17570 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17571 "After folding the first buffer, its text should not be displayed"
17572 );
17573
17574 multi_buffer_editor.update(cx, |editor, cx| {
17575 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17576 });
17577
17578 assert_eq!(
17579 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17580 "\n\n\n\n\n\n7777\n8888\n9999",
17581 "After folding the second buffer, its text should not be displayed"
17582 );
17583
17584 multi_buffer_editor.update(cx, |editor, cx| {
17585 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17586 });
17587 assert_eq!(
17588 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17589 "\n\n\n\n\n",
17590 "After folding the third buffer, its text should not be displayed"
17591 );
17592
17593 multi_buffer_editor.update(cx, |editor, cx| {
17594 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17595 });
17596 assert_eq!(
17597 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17598 "\n\n\n\n4444\n5555\n6666\n\n",
17599 "After unfolding the second buffer, its text should be displayed"
17600 );
17601
17602 multi_buffer_editor.update(cx, |editor, cx| {
17603 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17604 });
17605 assert_eq!(
17606 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17607 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17608 "After unfolding the first buffer, its text should be displayed"
17609 );
17610
17611 multi_buffer_editor.update(cx, |editor, cx| {
17612 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17613 });
17614 assert_eq!(
17615 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17616 full_text,
17617 "After unfolding all buffers, all original text should be displayed"
17618 );
17619}
17620
17621#[gpui::test]
17622async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17623 init_test(cx, |_| {});
17624
17625 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17626
17627 let fs = FakeFs::new(cx.executor());
17628 fs.insert_tree(
17629 path!("/a"),
17630 json!({
17631 "main.rs": sample_text,
17632 }),
17633 )
17634 .await;
17635 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17636 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17637 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17638 let worktree = project.update(cx, |project, cx| {
17639 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17640 assert_eq!(worktrees.len(), 1);
17641 worktrees.pop().unwrap()
17642 });
17643 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17644
17645 let buffer_1 = project
17646 .update(cx, |project, cx| {
17647 project.open_buffer((worktree_id, "main.rs"), cx)
17648 })
17649 .await
17650 .unwrap();
17651
17652 let multi_buffer = cx.new(|cx| {
17653 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17654 multi_buffer.push_excerpts(
17655 buffer_1.clone(),
17656 [ExcerptRange::new(
17657 Point::new(0, 0)
17658 ..Point::new(
17659 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17660 0,
17661 ),
17662 )],
17663 cx,
17664 );
17665 multi_buffer
17666 });
17667 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17668 Editor::new(
17669 EditorMode::full(),
17670 multi_buffer,
17671 Some(project.clone()),
17672 window,
17673 cx,
17674 )
17675 });
17676
17677 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17678 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17679 enum TestHighlight {}
17680 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17681 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17682 editor.highlight_text::<TestHighlight>(
17683 vec![highlight_range.clone()],
17684 HighlightStyle::color(Hsla::green()),
17685 cx,
17686 );
17687 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17688 });
17689
17690 let full_text = format!("\n\n{sample_text}");
17691 assert_eq!(
17692 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17693 full_text,
17694 );
17695}
17696
17697#[gpui::test]
17698async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17699 init_test(cx, |_| {});
17700 cx.update(|cx| {
17701 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17702 "keymaps/default-linux.json",
17703 cx,
17704 )
17705 .unwrap();
17706 cx.bind_keys(default_key_bindings);
17707 });
17708
17709 let (editor, cx) = cx.add_window_view(|window, cx| {
17710 let multi_buffer = MultiBuffer::build_multi(
17711 [
17712 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17713 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17714 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17715 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17716 ],
17717 cx,
17718 );
17719 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
17720
17721 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17722 // fold all but the second buffer, so that we test navigating between two
17723 // adjacent folded buffers, as well as folded buffers at the start and
17724 // end the multibuffer
17725 editor.fold_buffer(buffer_ids[0], cx);
17726 editor.fold_buffer(buffer_ids[2], cx);
17727 editor.fold_buffer(buffer_ids[3], cx);
17728
17729 editor
17730 });
17731 cx.simulate_resize(size(px(1000.), px(1000.)));
17732
17733 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17734 cx.assert_excerpts_with_selections(indoc! {"
17735 [EXCERPT]
17736 ˇ[FOLDED]
17737 [EXCERPT]
17738 a1
17739 b1
17740 [EXCERPT]
17741 [FOLDED]
17742 [EXCERPT]
17743 [FOLDED]
17744 "
17745 });
17746 cx.simulate_keystroke("down");
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 cx.simulate_keystroke("down");
17760 cx.assert_excerpts_with_selections(indoc! {"
17761 [EXCERPT]
17762 [FOLDED]
17763 [EXCERPT]
17764 a1
17765 ˇb1
17766 [EXCERPT]
17767 [FOLDED]
17768 [EXCERPT]
17769 [FOLDED]
17770 "
17771 });
17772 cx.simulate_keystroke("down");
17773 cx.assert_excerpts_with_selections(indoc! {"
17774 [EXCERPT]
17775 [FOLDED]
17776 [EXCERPT]
17777 a1
17778 b1
17779 ˇ[EXCERPT]
17780 [FOLDED]
17781 [EXCERPT]
17782 [FOLDED]
17783 "
17784 });
17785 cx.simulate_keystroke("down");
17786 cx.assert_excerpts_with_selections(indoc! {"
17787 [EXCERPT]
17788 [FOLDED]
17789 [EXCERPT]
17790 a1
17791 b1
17792 [EXCERPT]
17793 ˇ[FOLDED]
17794 [EXCERPT]
17795 [FOLDED]
17796 "
17797 });
17798 for _ in 0..5 {
17799 cx.simulate_keystroke("down");
17800 cx.assert_excerpts_with_selections(indoc! {"
17801 [EXCERPT]
17802 [FOLDED]
17803 [EXCERPT]
17804 a1
17805 b1
17806 [EXCERPT]
17807 [FOLDED]
17808 [EXCERPT]
17809 ˇ[FOLDED]
17810 "
17811 });
17812 }
17813
17814 cx.simulate_keystroke("up");
17815 cx.assert_excerpts_with_selections(indoc! {"
17816 [EXCERPT]
17817 [FOLDED]
17818 [EXCERPT]
17819 a1
17820 b1
17821 [EXCERPT]
17822 ˇ[FOLDED]
17823 [EXCERPT]
17824 [FOLDED]
17825 "
17826 });
17827 cx.simulate_keystroke("up");
17828 cx.assert_excerpts_with_selections(indoc! {"
17829 [EXCERPT]
17830 [FOLDED]
17831 [EXCERPT]
17832 a1
17833 b1
17834 ˇ[EXCERPT]
17835 [FOLDED]
17836 [EXCERPT]
17837 [FOLDED]
17838 "
17839 });
17840 cx.simulate_keystroke("up");
17841 cx.assert_excerpts_with_selections(indoc! {"
17842 [EXCERPT]
17843 [FOLDED]
17844 [EXCERPT]
17845 a1
17846 ˇb1
17847 [EXCERPT]
17848 [FOLDED]
17849 [EXCERPT]
17850 [FOLDED]
17851 "
17852 });
17853 cx.simulate_keystroke("up");
17854 cx.assert_excerpts_with_selections(indoc! {"
17855 [EXCERPT]
17856 [FOLDED]
17857 [EXCERPT]
17858 ˇa1
17859 b1
17860 [EXCERPT]
17861 [FOLDED]
17862 [EXCERPT]
17863 [FOLDED]
17864 "
17865 });
17866 for _ in 0..5 {
17867 cx.simulate_keystroke("up");
17868 cx.assert_excerpts_with_selections(indoc! {"
17869 [EXCERPT]
17870 ˇ[FOLDED]
17871 [EXCERPT]
17872 a1
17873 b1
17874 [EXCERPT]
17875 [FOLDED]
17876 [EXCERPT]
17877 [FOLDED]
17878 "
17879 });
17880 }
17881}
17882
17883#[gpui::test]
17884async fn test_inline_completion_text(cx: &mut TestAppContext) {
17885 init_test(cx, |_| {});
17886
17887 // Simple insertion
17888 assert_highlighted_edits(
17889 "Hello, world!",
17890 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17891 true,
17892 cx,
17893 |highlighted_edits, cx| {
17894 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17895 assert_eq!(highlighted_edits.highlights.len(), 1);
17896 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17897 assert_eq!(
17898 highlighted_edits.highlights[0].1.background_color,
17899 Some(cx.theme().status().created_background)
17900 );
17901 },
17902 )
17903 .await;
17904
17905 // Replacement
17906 assert_highlighted_edits(
17907 "This is a test.",
17908 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17909 false,
17910 cx,
17911 |highlighted_edits, cx| {
17912 assert_eq!(highlighted_edits.text, "That is a test.");
17913 assert_eq!(highlighted_edits.highlights.len(), 1);
17914 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17915 assert_eq!(
17916 highlighted_edits.highlights[0].1.background_color,
17917 Some(cx.theme().status().created_background)
17918 );
17919 },
17920 )
17921 .await;
17922
17923 // Multiple edits
17924 assert_highlighted_edits(
17925 "Hello, world!",
17926 vec![
17927 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17928 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17929 ],
17930 false,
17931 cx,
17932 |highlighted_edits, cx| {
17933 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17934 assert_eq!(highlighted_edits.highlights.len(), 2);
17935 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17936 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17937 assert_eq!(
17938 highlighted_edits.highlights[0].1.background_color,
17939 Some(cx.theme().status().created_background)
17940 );
17941 assert_eq!(
17942 highlighted_edits.highlights[1].1.background_color,
17943 Some(cx.theme().status().created_background)
17944 );
17945 },
17946 )
17947 .await;
17948
17949 // Multiple lines with edits
17950 assert_highlighted_edits(
17951 "First line\nSecond line\nThird line\nFourth line",
17952 vec![
17953 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17954 (
17955 Point::new(2, 0)..Point::new(2, 10),
17956 "New third line".to_string(),
17957 ),
17958 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17959 ],
17960 false,
17961 cx,
17962 |highlighted_edits, cx| {
17963 assert_eq!(
17964 highlighted_edits.text,
17965 "Second modified\nNew third line\nFourth updated line"
17966 );
17967 assert_eq!(highlighted_edits.highlights.len(), 3);
17968 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17969 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17970 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17971 for highlight in &highlighted_edits.highlights {
17972 assert_eq!(
17973 highlight.1.background_color,
17974 Some(cx.theme().status().created_background)
17975 );
17976 }
17977 },
17978 )
17979 .await;
17980}
17981
17982#[gpui::test]
17983async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17984 init_test(cx, |_| {});
17985
17986 // Deletion
17987 assert_highlighted_edits(
17988 "Hello, world!",
17989 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17990 true,
17991 cx,
17992 |highlighted_edits, cx| {
17993 assert_eq!(highlighted_edits.text, "Hello, world!");
17994 assert_eq!(highlighted_edits.highlights.len(), 1);
17995 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17996 assert_eq!(
17997 highlighted_edits.highlights[0].1.background_color,
17998 Some(cx.theme().status().deleted_background)
17999 );
18000 },
18001 )
18002 .await;
18003
18004 // Insertion
18005 assert_highlighted_edits(
18006 "Hello, world!",
18007 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18008 true,
18009 cx,
18010 |highlighted_edits, cx| {
18011 assert_eq!(highlighted_edits.highlights.len(), 1);
18012 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18013 assert_eq!(
18014 highlighted_edits.highlights[0].1.background_color,
18015 Some(cx.theme().status().created_background)
18016 );
18017 },
18018 )
18019 .await;
18020}
18021
18022async fn assert_highlighted_edits(
18023 text: &str,
18024 edits: Vec<(Range<Point>, String)>,
18025 include_deletions: bool,
18026 cx: &mut TestAppContext,
18027 assertion_fn: impl Fn(HighlightedText, &App),
18028) {
18029 let window = cx.add_window(|window, cx| {
18030 let buffer = MultiBuffer::build_simple(text, cx);
18031 Editor::new(EditorMode::full(), buffer, None, window, cx)
18032 });
18033 let cx = &mut VisualTestContext::from_window(*window, cx);
18034
18035 let (buffer, snapshot) = window
18036 .update(cx, |editor, _window, cx| {
18037 (
18038 editor.buffer().clone(),
18039 editor.buffer().read(cx).snapshot(cx),
18040 )
18041 })
18042 .unwrap();
18043
18044 let edits = edits
18045 .into_iter()
18046 .map(|(range, edit)| {
18047 (
18048 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18049 edit,
18050 )
18051 })
18052 .collect::<Vec<_>>();
18053
18054 let text_anchor_edits = edits
18055 .clone()
18056 .into_iter()
18057 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18058 .collect::<Vec<_>>();
18059
18060 let edit_preview = window
18061 .update(cx, |_, _window, cx| {
18062 buffer
18063 .read(cx)
18064 .as_singleton()
18065 .unwrap()
18066 .read(cx)
18067 .preview_edits(text_anchor_edits.into(), cx)
18068 })
18069 .unwrap()
18070 .await;
18071
18072 cx.update(|_window, cx| {
18073 let highlighted_edits = inline_completion_edit_text(
18074 &snapshot.as_singleton().unwrap().2,
18075 &edits,
18076 &edit_preview,
18077 include_deletions,
18078 cx,
18079 );
18080 assertion_fn(highlighted_edits, cx)
18081 });
18082}
18083
18084#[track_caller]
18085fn assert_breakpoint(
18086 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18087 path: &Arc<Path>,
18088 expected: Vec<(u32, Breakpoint)>,
18089) {
18090 if expected.len() == 0usize {
18091 assert!(!breakpoints.contains_key(path), "{}", path.display());
18092 } else {
18093 let mut breakpoint = breakpoints
18094 .get(path)
18095 .unwrap()
18096 .into_iter()
18097 .map(|breakpoint| {
18098 (
18099 breakpoint.row,
18100 Breakpoint {
18101 message: breakpoint.message.clone(),
18102 state: breakpoint.state,
18103 condition: breakpoint.condition.clone(),
18104 hit_condition: breakpoint.hit_condition.clone(),
18105 },
18106 )
18107 })
18108 .collect::<Vec<_>>();
18109
18110 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18111
18112 assert_eq!(expected, breakpoint);
18113 }
18114}
18115
18116fn add_log_breakpoint_at_cursor(
18117 editor: &mut Editor,
18118 log_message: &str,
18119 window: &mut Window,
18120 cx: &mut Context<Editor>,
18121) {
18122 let (anchor, bp) = editor
18123 .breakpoints_at_cursors(window, cx)
18124 .first()
18125 .and_then(|(anchor, bp)| {
18126 if let Some(bp) = bp {
18127 Some((*anchor, bp.clone()))
18128 } else {
18129 None
18130 }
18131 })
18132 .unwrap_or_else(|| {
18133 let cursor_position: Point = editor.selections.newest(cx).head();
18134
18135 let breakpoint_position = editor
18136 .snapshot(window, cx)
18137 .display_snapshot
18138 .buffer_snapshot
18139 .anchor_before(Point::new(cursor_position.row, 0));
18140
18141 (breakpoint_position, Breakpoint::new_log(&log_message))
18142 });
18143
18144 editor.edit_breakpoint_at_anchor(
18145 anchor,
18146 bp,
18147 BreakpointEditAction::EditLogMessage(log_message.into()),
18148 cx,
18149 );
18150}
18151
18152#[gpui::test]
18153async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18154 init_test(cx, |_| {});
18155
18156 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18157 let fs = FakeFs::new(cx.executor());
18158 fs.insert_tree(
18159 path!("/a"),
18160 json!({
18161 "main.rs": sample_text,
18162 }),
18163 )
18164 .await;
18165 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18166 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18167 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18168
18169 let fs = FakeFs::new(cx.executor());
18170 fs.insert_tree(
18171 path!("/a"),
18172 json!({
18173 "main.rs": sample_text,
18174 }),
18175 )
18176 .await;
18177 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18178 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18179 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18180 let worktree_id = workspace
18181 .update(cx, |workspace, _window, cx| {
18182 workspace.project().update(cx, |project, cx| {
18183 project.worktrees(cx).next().unwrap().read(cx).id()
18184 })
18185 })
18186 .unwrap();
18187
18188 let buffer = project
18189 .update(cx, |project, cx| {
18190 project.open_buffer((worktree_id, "main.rs"), cx)
18191 })
18192 .await
18193 .unwrap();
18194
18195 let (editor, cx) = cx.add_window_view(|window, cx| {
18196 Editor::new(
18197 EditorMode::full(),
18198 MultiBuffer::build_from_buffer(buffer, cx),
18199 Some(project.clone()),
18200 window,
18201 cx,
18202 )
18203 });
18204
18205 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18206 let abs_path = project.read_with(cx, |project, cx| {
18207 project
18208 .absolute_path(&project_path, cx)
18209 .map(|path_buf| Arc::from(path_buf.to_owned()))
18210 .unwrap()
18211 });
18212
18213 // assert we can add breakpoint on the first line
18214 editor.update_in(cx, |editor, window, cx| {
18215 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18216 editor.move_to_end(&MoveToEnd, window, cx);
18217 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18218 });
18219
18220 let breakpoints = editor.update(cx, |editor, cx| {
18221 editor
18222 .breakpoint_store()
18223 .as_ref()
18224 .unwrap()
18225 .read(cx)
18226 .all_breakpoints(cx)
18227 .clone()
18228 });
18229
18230 assert_eq!(1, breakpoints.len());
18231 assert_breakpoint(
18232 &breakpoints,
18233 &abs_path,
18234 vec![
18235 (0, Breakpoint::new_standard()),
18236 (3, Breakpoint::new_standard()),
18237 ],
18238 );
18239
18240 editor.update_in(cx, |editor, window, cx| {
18241 editor.move_to_beginning(&MoveToBeginning, window, cx);
18242 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18243 });
18244
18245 let breakpoints = editor.update(cx, |editor, cx| {
18246 editor
18247 .breakpoint_store()
18248 .as_ref()
18249 .unwrap()
18250 .read(cx)
18251 .all_breakpoints(cx)
18252 .clone()
18253 });
18254
18255 assert_eq!(1, breakpoints.len());
18256 assert_breakpoint(
18257 &breakpoints,
18258 &abs_path,
18259 vec![(3, Breakpoint::new_standard())],
18260 );
18261
18262 editor.update_in(cx, |editor, window, cx| {
18263 editor.move_to_end(&MoveToEnd, window, cx);
18264 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18265 });
18266
18267 let breakpoints = editor.update(cx, |editor, cx| {
18268 editor
18269 .breakpoint_store()
18270 .as_ref()
18271 .unwrap()
18272 .read(cx)
18273 .all_breakpoints(cx)
18274 .clone()
18275 });
18276
18277 assert_eq!(0, breakpoints.len());
18278 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18279}
18280
18281#[gpui::test]
18282async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18283 init_test(cx, |_| {});
18284
18285 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18286
18287 let fs = FakeFs::new(cx.executor());
18288 fs.insert_tree(
18289 path!("/a"),
18290 json!({
18291 "main.rs": sample_text,
18292 }),
18293 )
18294 .await;
18295 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18296 let (workspace, cx) =
18297 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18298
18299 let worktree_id = workspace.update(cx, |workspace, cx| {
18300 workspace.project().update(cx, |project, cx| {
18301 project.worktrees(cx).next().unwrap().read(cx).id()
18302 })
18303 });
18304
18305 let buffer = project
18306 .update(cx, |project, cx| {
18307 project.open_buffer((worktree_id, "main.rs"), cx)
18308 })
18309 .await
18310 .unwrap();
18311
18312 let (editor, cx) = cx.add_window_view(|window, cx| {
18313 Editor::new(
18314 EditorMode::full(),
18315 MultiBuffer::build_from_buffer(buffer, cx),
18316 Some(project.clone()),
18317 window,
18318 cx,
18319 )
18320 });
18321
18322 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18323 let abs_path = project.read_with(cx, |project, cx| {
18324 project
18325 .absolute_path(&project_path, cx)
18326 .map(|path_buf| Arc::from(path_buf.to_owned()))
18327 .unwrap()
18328 });
18329
18330 editor.update_in(cx, |editor, window, cx| {
18331 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18332 });
18333
18334 let breakpoints = editor.update(cx, |editor, cx| {
18335 editor
18336 .breakpoint_store()
18337 .as_ref()
18338 .unwrap()
18339 .read(cx)
18340 .all_breakpoints(cx)
18341 .clone()
18342 });
18343
18344 assert_breakpoint(
18345 &breakpoints,
18346 &abs_path,
18347 vec![(0, Breakpoint::new_log("hello world"))],
18348 );
18349
18350 // Removing a log message from a log breakpoint should remove it
18351 editor.update_in(cx, |editor, window, cx| {
18352 add_log_breakpoint_at_cursor(editor, "", window, cx);
18353 });
18354
18355 let breakpoints = editor.update(cx, |editor, cx| {
18356 editor
18357 .breakpoint_store()
18358 .as_ref()
18359 .unwrap()
18360 .read(cx)
18361 .all_breakpoints(cx)
18362 .clone()
18363 });
18364
18365 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18366
18367 editor.update_in(cx, |editor, window, cx| {
18368 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18369 editor.move_to_end(&MoveToEnd, window, cx);
18370 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18371 // Not adding a log message to a standard breakpoint shouldn't remove it
18372 add_log_breakpoint_at_cursor(editor, "", window, cx);
18373 });
18374
18375 let breakpoints = editor.update(cx, |editor, cx| {
18376 editor
18377 .breakpoint_store()
18378 .as_ref()
18379 .unwrap()
18380 .read(cx)
18381 .all_breakpoints(cx)
18382 .clone()
18383 });
18384
18385 assert_breakpoint(
18386 &breakpoints,
18387 &abs_path,
18388 vec![
18389 (0, Breakpoint::new_standard()),
18390 (3, Breakpoint::new_standard()),
18391 ],
18392 );
18393
18394 editor.update_in(cx, |editor, window, cx| {
18395 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18396 });
18397
18398 let breakpoints = editor.update(cx, |editor, cx| {
18399 editor
18400 .breakpoint_store()
18401 .as_ref()
18402 .unwrap()
18403 .read(cx)
18404 .all_breakpoints(cx)
18405 .clone()
18406 });
18407
18408 assert_breakpoint(
18409 &breakpoints,
18410 &abs_path,
18411 vec![
18412 (0, Breakpoint::new_standard()),
18413 (3, Breakpoint::new_log("hello world")),
18414 ],
18415 );
18416
18417 editor.update_in(cx, |editor, window, cx| {
18418 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18419 });
18420
18421 let breakpoints = editor.update(cx, |editor, cx| {
18422 editor
18423 .breakpoint_store()
18424 .as_ref()
18425 .unwrap()
18426 .read(cx)
18427 .all_breakpoints(cx)
18428 .clone()
18429 });
18430
18431 assert_breakpoint(
18432 &breakpoints,
18433 &abs_path,
18434 vec![
18435 (0, Breakpoint::new_standard()),
18436 (3, Breakpoint::new_log("hello Earth!!")),
18437 ],
18438 );
18439}
18440
18441/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18442/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18443/// or when breakpoints were placed out of order. This tests for a regression too
18444#[gpui::test]
18445async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18446 init_test(cx, |_| {});
18447
18448 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18449 let fs = FakeFs::new(cx.executor());
18450 fs.insert_tree(
18451 path!("/a"),
18452 json!({
18453 "main.rs": sample_text,
18454 }),
18455 )
18456 .await;
18457 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18458 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18459 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18460
18461 let fs = FakeFs::new(cx.executor());
18462 fs.insert_tree(
18463 path!("/a"),
18464 json!({
18465 "main.rs": sample_text,
18466 }),
18467 )
18468 .await;
18469 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18470 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18471 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18472 let worktree_id = workspace
18473 .update(cx, |workspace, _window, cx| {
18474 workspace.project().update(cx, |project, cx| {
18475 project.worktrees(cx).next().unwrap().read(cx).id()
18476 })
18477 })
18478 .unwrap();
18479
18480 let buffer = project
18481 .update(cx, |project, cx| {
18482 project.open_buffer((worktree_id, "main.rs"), cx)
18483 })
18484 .await
18485 .unwrap();
18486
18487 let (editor, cx) = cx.add_window_view(|window, cx| {
18488 Editor::new(
18489 EditorMode::full(),
18490 MultiBuffer::build_from_buffer(buffer, cx),
18491 Some(project.clone()),
18492 window,
18493 cx,
18494 )
18495 });
18496
18497 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18498 let abs_path = project.read_with(cx, |project, cx| {
18499 project
18500 .absolute_path(&project_path, cx)
18501 .map(|path_buf| Arc::from(path_buf.to_owned()))
18502 .unwrap()
18503 });
18504
18505 // assert we can add breakpoint on the first line
18506 editor.update_in(cx, |editor, window, cx| {
18507 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18508 editor.move_to_end(&MoveToEnd, window, cx);
18509 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18510 editor.move_up(&MoveUp, window, cx);
18511 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18512 });
18513
18514 let breakpoints = editor.update(cx, |editor, cx| {
18515 editor
18516 .breakpoint_store()
18517 .as_ref()
18518 .unwrap()
18519 .read(cx)
18520 .all_breakpoints(cx)
18521 .clone()
18522 });
18523
18524 assert_eq!(1, breakpoints.len());
18525 assert_breakpoint(
18526 &breakpoints,
18527 &abs_path,
18528 vec![
18529 (0, Breakpoint::new_standard()),
18530 (2, Breakpoint::new_standard()),
18531 (3, Breakpoint::new_standard()),
18532 ],
18533 );
18534
18535 editor.update_in(cx, |editor, window, cx| {
18536 editor.move_to_beginning(&MoveToBeginning, window, cx);
18537 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18538 editor.move_to_end(&MoveToEnd, window, cx);
18539 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18540 // Disabling a breakpoint that doesn't exist should do nothing
18541 editor.move_up(&MoveUp, window, cx);
18542 editor.move_up(&MoveUp, window, cx);
18543 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18544 });
18545
18546 let breakpoints = editor.update(cx, |editor, cx| {
18547 editor
18548 .breakpoint_store()
18549 .as_ref()
18550 .unwrap()
18551 .read(cx)
18552 .all_breakpoints(cx)
18553 .clone()
18554 });
18555
18556 let disable_breakpoint = {
18557 let mut bp = Breakpoint::new_standard();
18558 bp.state = BreakpointState::Disabled;
18559 bp
18560 };
18561
18562 assert_eq!(1, breakpoints.len());
18563 assert_breakpoint(
18564 &breakpoints,
18565 &abs_path,
18566 vec![
18567 (0, disable_breakpoint.clone()),
18568 (2, Breakpoint::new_standard()),
18569 (3, disable_breakpoint.clone()),
18570 ],
18571 );
18572
18573 editor.update_in(cx, |editor, window, cx| {
18574 editor.move_to_beginning(&MoveToBeginning, window, cx);
18575 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18576 editor.move_to_end(&MoveToEnd, window, cx);
18577 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18578 editor.move_up(&MoveUp, window, cx);
18579 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18580 });
18581
18582 let breakpoints = editor.update(cx, |editor, cx| {
18583 editor
18584 .breakpoint_store()
18585 .as_ref()
18586 .unwrap()
18587 .read(cx)
18588 .all_breakpoints(cx)
18589 .clone()
18590 });
18591
18592 assert_eq!(1, breakpoints.len());
18593 assert_breakpoint(
18594 &breakpoints,
18595 &abs_path,
18596 vec![
18597 (0, Breakpoint::new_standard()),
18598 (2, disable_breakpoint),
18599 (3, Breakpoint::new_standard()),
18600 ],
18601 );
18602}
18603
18604#[gpui::test]
18605async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18606 init_test(cx, |_| {});
18607 let capabilities = lsp::ServerCapabilities {
18608 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18609 prepare_provider: Some(true),
18610 work_done_progress_options: Default::default(),
18611 })),
18612 ..Default::default()
18613 };
18614 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18615
18616 cx.set_state(indoc! {"
18617 struct Fˇoo {}
18618 "});
18619
18620 cx.update_editor(|editor, _, cx| {
18621 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18622 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18623 editor.highlight_background::<DocumentHighlightRead>(
18624 &[highlight_range],
18625 |c| c.editor_document_highlight_read_background,
18626 cx,
18627 );
18628 });
18629
18630 let mut prepare_rename_handler = cx
18631 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18632 move |_, _, _| async move {
18633 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18634 start: lsp::Position {
18635 line: 0,
18636 character: 7,
18637 },
18638 end: lsp::Position {
18639 line: 0,
18640 character: 10,
18641 },
18642 })))
18643 },
18644 );
18645 let prepare_rename_task = cx
18646 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18647 .expect("Prepare rename was not started");
18648 prepare_rename_handler.next().await.unwrap();
18649 prepare_rename_task.await.expect("Prepare rename failed");
18650
18651 let mut rename_handler =
18652 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18653 let edit = lsp::TextEdit {
18654 range: lsp::Range {
18655 start: lsp::Position {
18656 line: 0,
18657 character: 7,
18658 },
18659 end: lsp::Position {
18660 line: 0,
18661 character: 10,
18662 },
18663 },
18664 new_text: "FooRenamed".to_string(),
18665 };
18666 Ok(Some(lsp::WorkspaceEdit::new(
18667 // Specify the same edit twice
18668 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18669 )))
18670 });
18671 let rename_task = cx
18672 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18673 .expect("Confirm rename was not started");
18674 rename_handler.next().await.unwrap();
18675 rename_task.await.expect("Confirm rename failed");
18676 cx.run_until_parked();
18677
18678 // Despite two edits, only one is actually applied as those are identical
18679 cx.assert_editor_state(indoc! {"
18680 struct FooRenamedˇ {}
18681 "});
18682}
18683
18684#[gpui::test]
18685async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18686 init_test(cx, |_| {});
18687 // These capabilities indicate that the server does not support prepare rename.
18688 let capabilities = lsp::ServerCapabilities {
18689 rename_provider: Some(lsp::OneOf::Left(true)),
18690 ..Default::default()
18691 };
18692 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18693
18694 cx.set_state(indoc! {"
18695 struct Fˇoo {}
18696 "});
18697
18698 cx.update_editor(|editor, _window, cx| {
18699 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18700 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18701 editor.highlight_background::<DocumentHighlightRead>(
18702 &[highlight_range],
18703 |c| c.editor_document_highlight_read_background,
18704 cx,
18705 );
18706 });
18707
18708 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18709 .expect("Prepare rename was not started")
18710 .await
18711 .expect("Prepare rename failed");
18712
18713 let mut rename_handler =
18714 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18715 let edit = lsp::TextEdit {
18716 range: lsp::Range {
18717 start: lsp::Position {
18718 line: 0,
18719 character: 7,
18720 },
18721 end: lsp::Position {
18722 line: 0,
18723 character: 10,
18724 },
18725 },
18726 new_text: "FooRenamed".to_string(),
18727 };
18728 Ok(Some(lsp::WorkspaceEdit::new(
18729 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18730 )))
18731 });
18732 let rename_task = cx
18733 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18734 .expect("Confirm rename was not started");
18735 rename_handler.next().await.unwrap();
18736 rename_task.await.expect("Confirm rename failed");
18737 cx.run_until_parked();
18738
18739 // Correct range is renamed, as `surrounding_word` is used to find it.
18740 cx.assert_editor_state(indoc! {"
18741 struct FooRenamedˇ {}
18742 "});
18743}
18744
18745#[gpui::test]
18746async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18747 init_test(cx, |_| {});
18748 let mut cx = EditorTestContext::new(cx).await;
18749
18750 let language = Arc::new(
18751 Language::new(
18752 LanguageConfig::default(),
18753 Some(tree_sitter_html::LANGUAGE.into()),
18754 )
18755 .with_brackets_query(
18756 r#"
18757 ("<" @open "/>" @close)
18758 ("</" @open ">" @close)
18759 ("<" @open ">" @close)
18760 ("\"" @open "\"" @close)
18761 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18762 "#,
18763 )
18764 .unwrap(),
18765 );
18766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18767
18768 cx.set_state(indoc! {"
18769 <span>ˇ</span>
18770 "});
18771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18772 cx.assert_editor_state(indoc! {"
18773 <span>
18774 ˇ
18775 </span>
18776 "});
18777
18778 cx.set_state(indoc! {"
18779 <span><span></span>ˇ</span>
18780 "});
18781 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18782 cx.assert_editor_state(indoc! {"
18783 <span><span></span>
18784 ˇ</span>
18785 "});
18786
18787 cx.set_state(indoc! {"
18788 <span>ˇ
18789 </span>
18790 "});
18791 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18792 cx.assert_editor_state(indoc! {"
18793 <span>
18794 ˇ
18795 </span>
18796 "});
18797}
18798
18799#[gpui::test(iterations = 10)]
18800async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18801 init_test(cx, |_| {});
18802
18803 let fs = FakeFs::new(cx.executor());
18804 fs.insert_tree(
18805 path!("/dir"),
18806 json!({
18807 "a.ts": "a",
18808 }),
18809 )
18810 .await;
18811
18812 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18813 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18814 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18815
18816 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18817 language_registry.add(Arc::new(Language::new(
18818 LanguageConfig {
18819 name: "TypeScript".into(),
18820 matcher: LanguageMatcher {
18821 path_suffixes: vec!["ts".to_string()],
18822 ..Default::default()
18823 },
18824 ..Default::default()
18825 },
18826 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18827 )));
18828 let mut fake_language_servers = language_registry.register_fake_lsp(
18829 "TypeScript",
18830 FakeLspAdapter {
18831 capabilities: lsp::ServerCapabilities {
18832 code_lens_provider: Some(lsp::CodeLensOptions {
18833 resolve_provider: Some(true),
18834 }),
18835 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18836 commands: vec!["_the/command".to_string()],
18837 ..lsp::ExecuteCommandOptions::default()
18838 }),
18839 ..lsp::ServerCapabilities::default()
18840 },
18841 ..FakeLspAdapter::default()
18842 },
18843 );
18844
18845 let (buffer, _handle) = project
18846 .update(cx, |p, cx| {
18847 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18848 })
18849 .await
18850 .unwrap();
18851 cx.executor().run_until_parked();
18852
18853 let fake_server = fake_language_servers.next().await.unwrap();
18854
18855 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18856 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18857 drop(buffer_snapshot);
18858 let actions = cx
18859 .update_window(*workspace, |_, window, cx| {
18860 project.code_actions(&buffer, anchor..anchor, window, cx)
18861 })
18862 .unwrap();
18863
18864 fake_server
18865 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18866 Ok(Some(vec![
18867 lsp::CodeLens {
18868 range: lsp::Range::default(),
18869 command: Some(lsp::Command {
18870 title: "Code lens command".to_owned(),
18871 command: "_the/command".to_owned(),
18872 arguments: None,
18873 }),
18874 data: None,
18875 },
18876 lsp::CodeLens {
18877 range: lsp::Range::default(),
18878 command: Some(lsp::Command {
18879 title: "Command not in capabilities".to_owned(),
18880 command: "not in capabilities".to_owned(),
18881 arguments: None,
18882 }),
18883 data: None,
18884 },
18885 lsp::CodeLens {
18886 range: lsp::Range {
18887 start: lsp::Position {
18888 line: 1,
18889 character: 1,
18890 },
18891 end: lsp::Position {
18892 line: 1,
18893 character: 1,
18894 },
18895 },
18896 command: Some(lsp::Command {
18897 title: "Command not in range".to_owned(),
18898 command: "_the/command".to_owned(),
18899 arguments: None,
18900 }),
18901 data: None,
18902 },
18903 ]))
18904 })
18905 .next()
18906 .await;
18907
18908 let actions = actions.await.unwrap();
18909 assert_eq!(
18910 actions.len(),
18911 1,
18912 "Should have only one valid action for the 0..0 range"
18913 );
18914 let action = actions[0].clone();
18915 let apply = project.update(cx, |project, cx| {
18916 project.apply_code_action(buffer.clone(), action, true, cx)
18917 });
18918
18919 // Resolving the code action does not populate its edits. In absence of
18920 // edits, we must execute the given command.
18921 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18922 |mut lens, _| async move {
18923 let lens_command = lens.command.as_mut().expect("should have a command");
18924 assert_eq!(lens_command.title, "Code lens command");
18925 lens_command.arguments = Some(vec![json!("the-argument")]);
18926 Ok(lens)
18927 },
18928 );
18929
18930 // While executing the command, the language server sends the editor
18931 // a `workspaceEdit` request.
18932 fake_server
18933 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18934 let fake = fake_server.clone();
18935 move |params, _| {
18936 assert_eq!(params.command, "_the/command");
18937 let fake = fake.clone();
18938 async move {
18939 fake.server
18940 .request::<lsp::request::ApplyWorkspaceEdit>(
18941 lsp::ApplyWorkspaceEditParams {
18942 label: None,
18943 edit: lsp::WorkspaceEdit {
18944 changes: Some(
18945 [(
18946 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18947 vec![lsp::TextEdit {
18948 range: lsp::Range::new(
18949 lsp::Position::new(0, 0),
18950 lsp::Position::new(0, 0),
18951 ),
18952 new_text: "X".into(),
18953 }],
18954 )]
18955 .into_iter()
18956 .collect(),
18957 ),
18958 ..Default::default()
18959 },
18960 },
18961 )
18962 .await
18963 .unwrap();
18964 Ok(Some(json!(null)))
18965 }
18966 }
18967 })
18968 .next()
18969 .await;
18970
18971 // Applying the code lens command returns a project transaction containing the edits
18972 // sent by the language server in its `workspaceEdit` request.
18973 let transaction = apply.await.unwrap();
18974 assert!(transaction.0.contains_key(&buffer));
18975 buffer.update(cx, |buffer, cx| {
18976 assert_eq!(buffer.text(), "Xa");
18977 buffer.undo(cx);
18978 assert_eq!(buffer.text(), "a");
18979 });
18980}
18981
18982#[gpui::test]
18983async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
18984 init_test(cx, |_| {});
18985
18986 let fs = FakeFs::new(cx.executor());
18987 let main_text = r#"fn main() {
18988println!("1");
18989println!("2");
18990println!("3");
18991println!("4");
18992println!("5");
18993}"#;
18994 let lib_text = "mod foo {}";
18995 fs.insert_tree(
18996 path!("/a"),
18997 json!({
18998 "lib.rs": lib_text,
18999 "main.rs": main_text,
19000 }),
19001 )
19002 .await;
19003
19004 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19005 let (workspace, cx) =
19006 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19007 let worktree_id = workspace.update(cx, |workspace, cx| {
19008 workspace.project().update(cx, |project, cx| {
19009 project.worktrees(cx).next().unwrap().read(cx).id()
19010 })
19011 });
19012
19013 let expected_ranges = vec![
19014 Point::new(0, 0)..Point::new(0, 0),
19015 Point::new(1, 0)..Point::new(1, 1),
19016 Point::new(2, 0)..Point::new(2, 2),
19017 Point::new(3, 0)..Point::new(3, 3),
19018 ];
19019
19020 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19021 let editor_1 = workspace
19022 .update_in(cx, |workspace, window, cx| {
19023 workspace.open_path(
19024 (worktree_id, "main.rs"),
19025 Some(pane_1.downgrade()),
19026 true,
19027 window,
19028 cx,
19029 )
19030 })
19031 .unwrap()
19032 .await
19033 .downcast::<Editor>()
19034 .unwrap();
19035 pane_1.update(cx, |pane, cx| {
19036 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19037 open_editor.update(cx, |editor, cx| {
19038 assert_eq!(
19039 editor.display_text(cx),
19040 main_text,
19041 "Original main.rs text on initial open",
19042 );
19043 assert_eq!(
19044 editor
19045 .selections
19046 .all::<Point>(cx)
19047 .into_iter()
19048 .map(|s| s.range())
19049 .collect::<Vec<_>>(),
19050 vec![Point::zero()..Point::zero()],
19051 "Default selections on initial open",
19052 );
19053 })
19054 });
19055 editor_1.update_in(cx, |editor, window, cx| {
19056 editor.change_selections(None, window, cx, |s| {
19057 s.select_ranges(expected_ranges.clone());
19058 });
19059 });
19060
19061 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19062 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19063 });
19064 let editor_2 = workspace
19065 .update_in(cx, |workspace, window, cx| {
19066 workspace.open_path(
19067 (worktree_id, "main.rs"),
19068 Some(pane_2.downgrade()),
19069 true,
19070 window,
19071 cx,
19072 )
19073 })
19074 .unwrap()
19075 .await
19076 .downcast::<Editor>()
19077 .unwrap();
19078 pane_2.update(cx, |pane, cx| {
19079 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19080 open_editor.update(cx, |editor, cx| {
19081 assert_eq!(
19082 editor.display_text(cx),
19083 main_text,
19084 "Original main.rs text on initial open in another panel",
19085 );
19086 assert_eq!(
19087 editor
19088 .selections
19089 .all::<Point>(cx)
19090 .into_iter()
19091 .map(|s| s.range())
19092 .collect::<Vec<_>>(),
19093 vec![Point::zero()..Point::zero()],
19094 "Default selections on initial open in another panel",
19095 );
19096 })
19097 });
19098
19099 editor_2.update_in(cx, |editor, window, cx| {
19100 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19101 });
19102
19103 let _other_editor_1 = workspace
19104 .update_in(cx, |workspace, window, cx| {
19105 workspace.open_path(
19106 (worktree_id, "lib.rs"),
19107 Some(pane_1.downgrade()),
19108 true,
19109 window,
19110 cx,
19111 )
19112 })
19113 .unwrap()
19114 .await
19115 .downcast::<Editor>()
19116 .unwrap();
19117 pane_1
19118 .update_in(cx, |pane, window, cx| {
19119 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19120 .unwrap()
19121 })
19122 .await
19123 .unwrap();
19124 drop(editor_1);
19125 pane_1.update(cx, |pane, cx| {
19126 pane.active_item()
19127 .unwrap()
19128 .downcast::<Editor>()
19129 .unwrap()
19130 .update(cx, |editor, cx| {
19131 assert_eq!(
19132 editor.display_text(cx),
19133 lib_text,
19134 "Other file should be open and active",
19135 );
19136 });
19137 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19138 });
19139
19140 let _other_editor_2 = workspace
19141 .update_in(cx, |workspace, window, cx| {
19142 workspace.open_path(
19143 (worktree_id, "lib.rs"),
19144 Some(pane_2.downgrade()),
19145 true,
19146 window,
19147 cx,
19148 )
19149 })
19150 .unwrap()
19151 .await
19152 .downcast::<Editor>()
19153 .unwrap();
19154 pane_2
19155 .update_in(cx, |pane, window, cx| {
19156 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19157 .unwrap()
19158 })
19159 .await
19160 .unwrap();
19161 drop(editor_2);
19162 pane_2.update(cx, |pane, cx| {
19163 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19164 open_editor.update(cx, |editor, cx| {
19165 assert_eq!(
19166 editor.display_text(cx),
19167 lib_text,
19168 "Other file should be open and active in another panel too",
19169 );
19170 });
19171 assert_eq!(
19172 pane.items().count(),
19173 1,
19174 "No other editors should be open in another pane",
19175 );
19176 });
19177
19178 let _editor_1_reopened = workspace
19179 .update_in(cx, |workspace, window, cx| {
19180 workspace.open_path(
19181 (worktree_id, "main.rs"),
19182 Some(pane_1.downgrade()),
19183 true,
19184 window,
19185 cx,
19186 )
19187 })
19188 .unwrap()
19189 .await
19190 .downcast::<Editor>()
19191 .unwrap();
19192 let _editor_2_reopened = workspace
19193 .update_in(cx, |workspace, window, cx| {
19194 workspace.open_path(
19195 (worktree_id, "main.rs"),
19196 Some(pane_2.downgrade()),
19197 true,
19198 window,
19199 cx,
19200 )
19201 })
19202 .unwrap()
19203 .await
19204 .downcast::<Editor>()
19205 .unwrap();
19206 pane_1.update(cx, |pane, cx| {
19207 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19208 open_editor.update(cx, |editor, cx| {
19209 assert_eq!(
19210 editor.display_text(cx),
19211 main_text,
19212 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19213 );
19214 assert_eq!(
19215 editor
19216 .selections
19217 .all::<Point>(cx)
19218 .into_iter()
19219 .map(|s| s.range())
19220 .collect::<Vec<_>>(),
19221 expected_ranges,
19222 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19223 );
19224 })
19225 });
19226 pane_2.update(cx, |pane, cx| {
19227 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19228 open_editor.update(cx, |editor, cx| {
19229 assert_eq!(
19230 editor.display_text(cx),
19231 r#"fn main() {
19232⋯rintln!("1");
19233⋯intln!("2");
19234⋯ntln!("3");
19235println!("4");
19236println!("5");
19237}"#,
19238 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19239 );
19240 assert_eq!(
19241 editor
19242 .selections
19243 .all::<Point>(cx)
19244 .into_iter()
19245 .map(|s| s.range())
19246 .collect::<Vec<_>>(),
19247 vec![Point::zero()..Point::zero()],
19248 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19249 );
19250 })
19251 });
19252}
19253
19254#[gpui::test]
19255async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19256 init_test(cx, |_| {});
19257
19258 let fs = FakeFs::new(cx.executor());
19259 let main_text = r#"fn main() {
19260println!("1");
19261println!("2");
19262println!("3");
19263println!("4");
19264println!("5");
19265}"#;
19266 let lib_text = "mod foo {}";
19267 fs.insert_tree(
19268 path!("/a"),
19269 json!({
19270 "lib.rs": lib_text,
19271 "main.rs": main_text,
19272 }),
19273 )
19274 .await;
19275
19276 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19277 let (workspace, cx) =
19278 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19279 let worktree_id = workspace.update(cx, |workspace, cx| {
19280 workspace.project().update(cx, |project, cx| {
19281 project.worktrees(cx).next().unwrap().read(cx).id()
19282 })
19283 });
19284
19285 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19286 let editor = workspace
19287 .update_in(cx, |workspace, window, cx| {
19288 workspace.open_path(
19289 (worktree_id, "main.rs"),
19290 Some(pane.downgrade()),
19291 true,
19292 window,
19293 cx,
19294 )
19295 })
19296 .unwrap()
19297 .await
19298 .downcast::<Editor>()
19299 .unwrap();
19300 pane.update(cx, |pane, cx| {
19301 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19302 open_editor.update(cx, |editor, cx| {
19303 assert_eq!(
19304 editor.display_text(cx),
19305 main_text,
19306 "Original main.rs text on initial open",
19307 );
19308 })
19309 });
19310 editor.update_in(cx, |editor, window, cx| {
19311 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19312 });
19313
19314 cx.update_global(|store: &mut SettingsStore, cx| {
19315 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19316 s.restore_on_file_reopen = Some(false);
19317 });
19318 });
19319 editor.update_in(cx, |editor, window, cx| {
19320 editor.fold_ranges(
19321 vec![
19322 Point::new(1, 0)..Point::new(1, 1),
19323 Point::new(2, 0)..Point::new(2, 2),
19324 Point::new(3, 0)..Point::new(3, 3),
19325 ],
19326 false,
19327 window,
19328 cx,
19329 );
19330 });
19331 pane.update_in(cx, |pane, window, cx| {
19332 pane.close_all_items(&CloseAllItems::default(), window, cx)
19333 .unwrap()
19334 })
19335 .await
19336 .unwrap();
19337 pane.update(cx, |pane, _| {
19338 assert!(pane.active_item().is_none());
19339 });
19340 cx.update_global(|store: &mut SettingsStore, cx| {
19341 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19342 s.restore_on_file_reopen = Some(true);
19343 });
19344 });
19345
19346 let _editor_reopened = workspace
19347 .update_in(cx, |workspace, window, cx| {
19348 workspace.open_path(
19349 (worktree_id, "main.rs"),
19350 Some(pane.downgrade()),
19351 true,
19352 window,
19353 cx,
19354 )
19355 })
19356 .unwrap()
19357 .await
19358 .downcast::<Editor>()
19359 .unwrap();
19360 pane.update(cx, |pane, cx| {
19361 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19362 open_editor.update(cx, |editor, cx| {
19363 assert_eq!(
19364 editor.display_text(cx),
19365 main_text,
19366 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19367 );
19368 })
19369 });
19370}
19371
19372fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
19373 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
19374 point..point
19375}
19376
19377fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
19378 let (text, ranges) = marked_text_ranges(marked_text, true);
19379 assert_eq!(editor.text(cx), text);
19380 assert_eq!(
19381 editor.selections.ranges(cx),
19382 ranges,
19383 "Assert selections are {}",
19384 marked_text
19385 );
19386}
19387
19388pub fn handle_signature_help_request(
19389 cx: &mut EditorLspTestContext,
19390 mocked_response: lsp::SignatureHelp,
19391) -> impl Future<Output = ()> + use<> {
19392 let mut request =
19393 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
19394 let mocked_response = mocked_response.clone();
19395 async move { Ok(Some(mocked_response)) }
19396 });
19397
19398 async move {
19399 request.next().await;
19400 }
19401}
19402
19403/// Handle completion request passing a marked string specifying where the completion
19404/// should be triggered from using '|' character, what range should be replaced, and what completions
19405/// should be returned using '<' and '>' to delimit the range.
19406///
19407/// Also see `handle_completion_request_with_insert_and_replace`.
19408#[track_caller]
19409pub fn handle_completion_request(
19410 cx: &mut EditorLspTestContext,
19411 marked_string: &str,
19412 completions: Vec<&'static str>,
19413 counter: Arc<AtomicUsize>,
19414) -> impl Future<Output = ()> {
19415 let complete_from_marker: TextRangeMarker = '|'.into();
19416 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19417 let (_, mut marked_ranges) = marked_text_ranges_by(
19418 marked_string,
19419 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19420 );
19421
19422 let complete_from_position =
19423 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19424 let replace_range =
19425 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19426
19427 let mut request =
19428 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19429 let completions = completions.clone();
19430 counter.fetch_add(1, atomic::Ordering::Release);
19431 async move {
19432 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19433 assert_eq!(
19434 params.text_document_position.position,
19435 complete_from_position
19436 );
19437 Ok(Some(lsp::CompletionResponse::Array(
19438 completions
19439 .iter()
19440 .map(|completion_text| lsp::CompletionItem {
19441 label: completion_text.to_string(),
19442 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
19443 range: replace_range,
19444 new_text: completion_text.to_string(),
19445 })),
19446 ..Default::default()
19447 })
19448 .collect(),
19449 )))
19450 }
19451 });
19452
19453 async move {
19454 request.next().await;
19455 }
19456}
19457
19458/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
19459/// given instead, which also contains an `insert` range.
19460///
19461/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
19462/// that is, `replace_range.start..cursor_pos`.
19463pub fn handle_completion_request_with_insert_and_replace(
19464 cx: &mut EditorLspTestContext,
19465 marked_string: &str,
19466 completions: Vec<&'static str>,
19467 counter: Arc<AtomicUsize>,
19468) -> impl Future<Output = ()> {
19469 let complete_from_marker: TextRangeMarker = '|'.into();
19470 let replace_range_marker: TextRangeMarker = ('<', '>').into();
19471 let (_, mut marked_ranges) = marked_text_ranges_by(
19472 marked_string,
19473 vec![complete_from_marker.clone(), replace_range_marker.clone()],
19474 );
19475
19476 let complete_from_position =
19477 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
19478 let replace_range =
19479 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
19480
19481 let mut request =
19482 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
19483 let completions = completions.clone();
19484 counter.fetch_add(1, atomic::Ordering::Release);
19485 async move {
19486 assert_eq!(params.text_document_position.text_document.uri, url.clone());
19487 assert_eq!(
19488 params.text_document_position.position, complete_from_position,
19489 "marker `|` position doesn't match",
19490 );
19491 Ok(Some(lsp::CompletionResponse::Array(
19492 completions
19493 .iter()
19494 .map(|completion_text| lsp::CompletionItem {
19495 label: completion_text.to_string(),
19496 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19497 lsp::InsertReplaceEdit {
19498 insert: lsp::Range {
19499 start: replace_range.start,
19500 end: complete_from_position,
19501 },
19502 replace: replace_range,
19503 new_text: completion_text.to_string(),
19504 },
19505 )),
19506 ..Default::default()
19507 })
19508 .collect(),
19509 )))
19510 }
19511 });
19512
19513 async move {
19514 request.next().await;
19515 }
19516}
19517
19518fn handle_resolve_completion_request(
19519 cx: &mut EditorLspTestContext,
19520 edits: Option<Vec<(&'static str, &'static str)>>,
19521) -> impl Future<Output = ()> {
19522 let edits = edits.map(|edits| {
19523 edits
19524 .iter()
19525 .map(|(marked_string, new_text)| {
19526 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
19527 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
19528 lsp::TextEdit::new(replace_range, new_text.to_string())
19529 })
19530 .collect::<Vec<_>>()
19531 });
19532
19533 let mut request =
19534 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19535 let edits = edits.clone();
19536 async move {
19537 Ok(lsp::CompletionItem {
19538 additional_text_edits: edits,
19539 ..Default::default()
19540 })
19541 }
19542 });
19543
19544 async move {
19545 request.next().await;
19546 }
19547}
19548
19549pub(crate) fn update_test_language_settings(
19550 cx: &mut TestAppContext,
19551 f: impl Fn(&mut AllLanguageSettingsContent),
19552) {
19553 cx.update(|cx| {
19554 SettingsStore::update_global(cx, |store, cx| {
19555 store.update_user_settings::<AllLanguageSettings>(cx, f);
19556 });
19557 });
19558}
19559
19560pub(crate) fn update_test_project_settings(
19561 cx: &mut TestAppContext,
19562 f: impl Fn(&mut ProjectSettings),
19563) {
19564 cx.update(|cx| {
19565 SettingsStore::update_global(cx, |store, cx| {
19566 store.update_user_settings::<ProjectSettings>(cx, f);
19567 });
19568 });
19569}
19570
19571pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
19572 cx.update(|cx| {
19573 assets::Assets.load_test_fonts(cx);
19574 let store = SettingsStore::test(cx);
19575 cx.set_global(store);
19576 theme::init(theme::LoadThemes::JustBase, cx);
19577 release_channel::init(SemanticVersion::default(), cx);
19578 client::init_settings(cx);
19579 language::init(cx);
19580 Project::init_settings(cx);
19581 workspace::init_settings(cx);
19582 crate::init(cx);
19583 });
19584
19585 update_test_language_settings(cx, f);
19586}
19587
19588#[track_caller]
19589fn assert_hunk_revert(
19590 not_reverted_text_with_selections: &str,
19591 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
19592 expected_reverted_text_with_selections: &str,
19593 base_text: &str,
19594 cx: &mut EditorLspTestContext,
19595) {
19596 cx.set_state(not_reverted_text_with_selections);
19597 cx.set_head_text(base_text);
19598 cx.executor().run_until_parked();
19599
19600 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
19601 let snapshot = editor.snapshot(window, cx);
19602 let reverted_hunk_statuses = snapshot
19603 .buffer_snapshot
19604 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
19605 .map(|hunk| hunk.status().kind)
19606 .collect::<Vec<_>>();
19607
19608 editor.git_restore(&Default::default(), window, cx);
19609 reverted_hunk_statuses
19610 });
19611 cx.executor().run_until_parked();
19612 cx.assert_editor_state(expected_reverted_text_with_selections);
19613 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
19614}