1use super::*;
2use crate::{
3 JoinLines,
4 scroll::scroll_amount::ScrollAmount,
5 test::{
6 assert_text_with_selections, build_editor,
7 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
8 editor_test_context::EditorTestContext,
9 select_ranges,
10 },
11};
12use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
13use futures::StreamExt;
14use gpui::{
15 BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal, VisualTestContext,
16 WindowBounds, WindowOptions, div,
17};
18use indoc::indoc;
19use language::{
20 BracketPairConfig,
21 Capability::ReadWrite,
22 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
23 Override, Point,
24 language_settings::{
25 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
26 LanguageSettingsContent, LspInsertMode, PrettierSettings,
27 },
28};
29use language_settings::{Formatter, FormatterList, IndentGuideSettings};
30use lsp::CompletionParams;
31use multi_buffer::{IndentGuide, PathKey};
32use parking_lot::Mutex;
33use pretty_assertions::{assert_eq, assert_ne};
34use project::{
35 FakeFs,
36 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
37 project_settings::{LspSettings, ProjectSettings},
38};
39use serde_json::{self, json};
40use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
41use std::{
42 iter,
43 sync::atomic::{self, AtomicUsize},
44};
45use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
46use text::ToPoint as _;
47use unindent::Unindent;
48use util::{
49 assert_set_eq, path,
50 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
51 uri,
52};
53use workspace::{
54 CloseAllItems, CloseInactiveItems, NavigationEntry, ViewId,
55 item::{FollowEvent, FollowableItem, Item, ItemHandle},
56};
57
58#[gpui::test]
59fn test_edit_events(cx: &mut TestAppContext) {
60 init_test(cx, |_| {});
61
62 let buffer = cx.new(|cx| {
63 let mut buffer = language::Buffer::local("123456", cx);
64 buffer.set_group_interval(Duration::from_secs(1));
65 buffer
66 });
67
68 let events = Rc::new(RefCell::new(Vec::new()));
69 let editor1 = cx.add_window({
70 let events = events.clone();
71 |window, cx| {
72 let entity = cx.entity().clone();
73 cx.subscribe_in(
74 &entity,
75 window,
76 move |_, _, event: &EditorEvent, _, _| match event {
77 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
78 EditorEvent::BufferEdited => {
79 events.borrow_mut().push(("editor1", "buffer edited"))
80 }
81 _ => {}
82 },
83 )
84 .detach();
85 Editor::for_buffer(buffer.clone(), None, window, cx)
86 }
87 });
88
89 let editor2 = cx.add_window({
90 let events = events.clone();
91 |window, cx| {
92 cx.subscribe_in(
93 &cx.entity().clone(),
94 window,
95 move |_, _, event: &EditorEvent, _, _| match event {
96 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
97 EditorEvent::BufferEdited => {
98 events.borrow_mut().push(("editor2", "buffer edited"))
99 }
100 _ => {}
101 },
102 )
103 .detach();
104 Editor::for_buffer(buffer.clone(), None, window, cx)
105 }
106 });
107
108 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
109
110 // Mutating editor 1 will emit an `Edited` event only for that editor.
111 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
112 assert_eq!(
113 mem::take(&mut *events.borrow_mut()),
114 [
115 ("editor1", "edited"),
116 ("editor1", "buffer edited"),
117 ("editor2", "buffer edited"),
118 ]
119 );
120
121 // Mutating editor 2 will emit an `Edited` event only for that editor.
122 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
123 assert_eq!(
124 mem::take(&mut *events.borrow_mut()),
125 [
126 ("editor2", "edited"),
127 ("editor1", "buffer edited"),
128 ("editor2", "buffer edited"),
129 ]
130 );
131
132 // Undoing on editor 1 will emit an `Edited` event only for that editor.
133 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
134 assert_eq!(
135 mem::take(&mut *events.borrow_mut()),
136 [
137 ("editor1", "edited"),
138 ("editor1", "buffer edited"),
139 ("editor2", "buffer edited"),
140 ]
141 );
142
143 // Redoing on editor 1 will emit an `Edited` event only for that editor.
144 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
145 assert_eq!(
146 mem::take(&mut *events.borrow_mut()),
147 [
148 ("editor1", "edited"),
149 ("editor1", "buffer edited"),
150 ("editor2", "buffer edited"),
151 ]
152 );
153
154 // Undoing on editor 2 will emit an `Edited` event only for that editor.
155 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
156 assert_eq!(
157 mem::take(&mut *events.borrow_mut()),
158 [
159 ("editor2", "edited"),
160 ("editor1", "buffer edited"),
161 ("editor2", "buffer edited"),
162 ]
163 );
164
165 // Redoing on editor 2 will emit an `Edited` event only for that editor.
166 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
167 assert_eq!(
168 mem::take(&mut *events.borrow_mut()),
169 [
170 ("editor2", "edited"),
171 ("editor1", "buffer edited"),
172 ("editor2", "buffer edited"),
173 ]
174 );
175
176 // No event is emitted when the mutation is a no-op.
177 _ = editor2.update(cx, |editor, window, cx| {
178 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
179
180 editor.backspace(&Backspace, window, cx);
181 });
182 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
183}
184
185#[gpui::test]
186fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
187 init_test(cx, |_| {});
188
189 let mut now = Instant::now();
190 let group_interval = Duration::from_millis(1);
191 let buffer = cx.new(|cx| {
192 let mut buf = language::Buffer::local("123456", cx);
193 buf.set_group_interval(group_interval);
194 buf
195 });
196 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
197 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
198
199 _ = editor.update(cx, |editor, window, cx| {
200 editor.start_transaction_at(now, window, cx);
201 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
202
203 editor.insert("cd", window, cx);
204 editor.end_transaction_at(now, cx);
205 assert_eq!(editor.text(cx), "12cd56");
206 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
207
208 editor.start_transaction_at(now, window, cx);
209 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
210 editor.insert("e", window, cx);
211 editor.end_transaction_at(now, cx);
212 assert_eq!(editor.text(cx), "12cde6");
213 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
214
215 now += group_interval + Duration::from_millis(1);
216 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
217
218 // Simulate an edit in another editor
219 buffer.update(cx, |buffer, cx| {
220 buffer.start_transaction_at(now, cx);
221 buffer.edit([(0..1, "a")], None, cx);
222 buffer.edit([(1..1, "b")], None, cx);
223 buffer.end_transaction_at(now, cx);
224 });
225
226 assert_eq!(editor.text(cx), "ab2cde6");
227 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
228
229 // Last transaction happened past the group interval in a different editor.
230 // Undo it individually and don't restore selections.
231 editor.undo(&Undo, window, cx);
232 assert_eq!(editor.text(cx), "12cde6");
233 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
234
235 // First two transactions happened within the group interval in this editor.
236 // Undo them together and restore selections.
237 editor.undo(&Undo, window, cx);
238 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
239 assert_eq!(editor.text(cx), "123456");
240 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
241
242 // Redo the first two transactions together.
243 editor.redo(&Redo, window, cx);
244 assert_eq!(editor.text(cx), "12cde6");
245 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
246
247 // Redo the last transaction on its own.
248 editor.redo(&Redo, window, cx);
249 assert_eq!(editor.text(cx), "ab2cde6");
250 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
251
252 // Test empty transactions.
253 editor.start_transaction_at(now, window, cx);
254 editor.end_transaction_at(now, cx);
255 editor.undo(&Undo, window, cx);
256 assert_eq!(editor.text(cx), "12cde6");
257 });
258}
259
260#[gpui::test]
261fn test_ime_composition(cx: &mut TestAppContext) {
262 init_test(cx, |_| {});
263
264 let buffer = cx.new(|cx| {
265 let mut buffer = language::Buffer::local("abcde", cx);
266 // Ensure automatic grouping doesn't occur.
267 buffer.set_group_interval(Duration::ZERO);
268 buffer
269 });
270
271 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
272 cx.add_window(|window, cx| {
273 let mut editor = build_editor(buffer.clone(), window, cx);
274
275 // Start a new IME composition.
276 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
277 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
278 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
279 assert_eq!(editor.text(cx), "äbcde");
280 assert_eq!(
281 editor.marked_text_ranges(cx),
282 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
283 );
284
285 // Finalize IME composition.
286 editor.replace_text_in_range(None, "ā", window, cx);
287 assert_eq!(editor.text(cx), "ābcde");
288 assert_eq!(editor.marked_text_ranges(cx), None);
289
290 // IME composition edits are grouped and are undone/redone at once.
291 editor.undo(&Default::default(), window, cx);
292 assert_eq!(editor.text(cx), "abcde");
293 assert_eq!(editor.marked_text_ranges(cx), None);
294 editor.redo(&Default::default(), window, cx);
295 assert_eq!(editor.text(cx), "ābcde");
296 assert_eq!(editor.marked_text_ranges(cx), None);
297
298 // Start a new IME composition.
299 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
303 );
304
305 // Undoing during an IME composition cancels it.
306 editor.undo(&Default::default(), window, cx);
307 assert_eq!(editor.text(cx), "ābcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309
310 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
311 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
312 assert_eq!(editor.text(cx), "ābcdè");
313 assert_eq!(
314 editor.marked_text_ranges(cx),
315 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
316 );
317
318 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
319 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
320 assert_eq!(editor.text(cx), "ābcdę");
321 assert_eq!(editor.marked_text_ranges(cx), None);
322
323 // Start a new IME composition with multiple cursors.
324 editor.change_selections(None, window, cx, |s| {
325 s.select_ranges([
326 OffsetUtf16(1)..OffsetUtf16(1),
327 OffsetUtf16(3)..OffsetUtf16(3),
328 OffsetUtf16(5)..OffsetUtf16(5),
329 ])
330 });
331 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
332 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
333 assert_eq!(
334 editor.marked_text_ranges(cx),
335 Some(vec![
336 OffsetUtf16(0)..OffsetUtf16(3),
337 OffsetUtf16(4)..OffsetUtf16(7),
338 OffsetUtf16(8)..OffsetUtf16(11)
339 ])
340 );
341
342 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
343 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
344 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
345 assert_eq!(
346 editor.marked_text_ranges(cx),
347 Some(vec![
348 OffsetUtf16(1)..OffsetUtf16(2),
349 OffsetUtf16(5)..OffsetUtf16(6),
350 OffsetUtf16(9)..OffsetUtf16(10)
351 ])
352 );
353
354 // Finalize IME composition with multiple cursors.
355 editor.replace_text_in_range(Some(9..10), "2", window, cx);
356 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358
359 editor
360 });
361}
362
363#[gpui::test]
364fn test_selection_with_mouse(cx: &mut TestAppContext) {
365 init_test(cx, |_| {});
366
367 let editor = cx.add_window(|window, cx| {
368 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
369 build_editor(buffer, window, cx)
370 });
371
372 _ = editor.update(cx, |editor, window, cx| {
373 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
374 });
375 assert_eq!(
376 editor
377 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
378 .unwrap(),
379 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
380 );
381
382 _ = editor.update(cx, |editor, window, cx| {
383 editor.update_selection(
384 DisplayPoint::new(DisplayRow(3), 3),
385 0,
386 gpui::Point::<f32>::default(),
387 window,
388 cx,
389 );
390 });
391
392 assert_eq!(
393 editor
394 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
395 .unwrap(),
396 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
397 );
398
399 _ = editor.update(cx, |editor, window, cx| {
400 editor.update_selection(
401 DisplayPoint::new(DisplayRow(1), 1),
402 0,
403 gpui::Point::<f32>::default(),
404 window,
405 cx,
406 );
407 });
408
409 assert_eq!(
410 editor
411 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
412 .unwrap(),
413 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
414 );
415
416 _ = editor.update(cx, |editor, window, cx| {
417 editor.end_selection(window, cx);
418 editor.update_selection(
419 DisplayPoint::new(DisplayRow(3), 3),
420 0,
421 gpui::Point::<f32>::default(),
422 window,
423 cx,
424 );
425 });
426
427 assert_eq!(
428 editor
429 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
430 .unwrap(),
431 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
432 );
433
434 _ = editor.update(cx, |editor, window, cx| {
435 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
436 editor.update_selection(
437 DisplayPoint::new(DisplayRow(0), 0),
438 0,
439 gpui::Point::<f32>::default(),
440 window,
441 cx,
442 );
443 });
444
445 assert_eq!(
446 editor
447 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
448 .unwrap(),
449 [
450 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
451 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
452 ]
453 );
454
455 _ = editor.update(cx, |editor, window, cx| {
456 editor.end_selection(window, cx);
457 });
458
459 assert_eq!(
460 editor
461 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
462 .unwrap(),
463 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
464 );
465}
466
467#[gpui::test]
468fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
469 init_test(cx, |_| {});
470
471 let editor = cx.add_window(|window, cx| {
472 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
473 build_editor(buffer, window, cx)
474 });
475
476 _ = editor.update(cx, |editor, window, cx| {
477 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
478 });
479
480 _ = editor.update(cx, |editor, window, cx| {
481 editor.end_selection(window, cx);
482 });
483
484 _ = editor.update(cx, |editor, window, cx| {
485 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
486 });
487
488 _ = editor.update(cx, |editor, window, cx| {
489 editor.end_selection(window, cx);
490 });
491
492 assert_eq!(
493 editor
494 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
495 .unwrap(),
496 [
497 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
498 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
499 ]
500 );
501
502 _ = editor.update(cx, |editor, window, cx| {
503 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
504 });
505
506 _ = editor.update(cx, |editor, window, cx| {
507 editor.end_selection(window, cx);
508 });
509
510 assert_eq!(
511 editor
512 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
513 .unwrap(),
514 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
515 );
516}
517
518#[gpui::test]
519fn test_canceling_pending_selection(cx: &mut TestAppContext) {
520 init_test(cx, |_| {});
521
522 let editor = cx.add_window(|window, cx| {
523 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
524 build_editor(buffer, window, cx)
525 });
526
527 _ = editor.update(cx, |editor, window, cx| {
528 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
529 assert_eq!(
530 editor.selections.display_ranges(cx),
531 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
532 );
533 });
534
535 _ = editor.update(cx, |editor, window, cx| {
536 editor.update_selection(
537 DisplayPoint::new(DisplayRow(3), 3),
538 0,
539 gpui::Point::<f32>::default(),
540 window,
541 cx,
542 );
543 assert_eq!(
544 editor.selections.display_ranges(cx),
545 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
546 );
547 });
548
549 _ = editor.update(cx, |editor, window, cx| {
550 editor.cancel(&Cancel, window, cx);
551 editor.update_selection(
552 DisplayPoint::new(DisplayRow(1), 1),
553 0,
554 gpui::Point::<f32>::default(),
555 window,
556 cx,
557 );
558 assert_eq!(
559 editor.selections.display_ranges(cx),
560 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
561 );
562 });
563}
564
565#[gpui::test]
566fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
567 init_test(cx, |_| {});
568
569 let editor = cx.add_window(|window, cx| {
570 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
571 build_editor(buffer, window, cx)
572 });
573
574 _ = editor.update(cx, |editor, window, cx| {
575 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
576 assert_eq!(
577 editor.selections.display_ranges(cx),
578 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
579 );
580
581 editor.move_down(&Default::default(), window, cx);
582 assert_eq!(
583 editor.selections.display_ranges(cx),
584 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
585 );
586
587 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
588 assert_eq!(
589 editor.selections.display_ranges(cx),
590 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
591 );
592
593 editor.move_up(&Default::default(), window, cx);
594 assert_eq!(
595 editor.selections.display_ranges(cx),
596 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
597 );
598 });
599}
600
601#[gpui::test]
602fn test_clone(cx: &mut TestAppContext) {
603 init_test(cx, |_| {});
604
605 let (text, selection_ranges) = marked_text_ranges(
606 indoc! {"
607 one
608 two
609 threeˇ
610 four
611 fiveˇ
612 "},
613 true,
614 );
615
616 let editor = cx.add_window(|window, cx| {
617 let buffer = MultiBuffer::build_simple(&text, cx);
618 build_editor(buffer, window, cx)
619 });
620
621 _ = editor.update(cx, |editor, window, cx| {
622 editor.change_selections(None, window, cx, |s| {
623 s.select_ranges(selection_ranges.clone())
624 });
625 editor.fold_creases(
626 vec![
627 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
628 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
629 ],
630 true,
631 window,
632 cx,
633 );
634 });
635
636 let cloned_editor = editor
637 .update(cx, |editor, _, cx| {
638 cx.open_window(Default::default(), |window, cx| {
639 cx.new(|cx| editor.clone(window, cx))
640 })
641 })
642 .unwrap()
643 .unwrap();
644
645 let snapshot = editor
646 .update(cx, |e, window, cx| e.snapshot(window, cx))
647 .unwrap();
648 let cloned_snapshot = cloned_editor
649 .update(cx, |e, window, cx| e.snapshot(window, cx))
650 .unwrap();
651
652 assert_eq!(
653 cloned_editor
654 .update(cx, |e, _, cx| e.display_text(cx))
655 .unwrap(),
656 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
657 );
658 assert_eq!(
659 cloned_snapshot
660 .folds_in_range(0..text.len())
661 .collect::<Vec<_>>(),
662 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
663 );
664 assert_set_eq!(
665 cloned_editor
666 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
667 .unwrap(),
668 editor
669 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
670 .unwrap()
671 );
672 assert_set_eq!(
673 cloned_editor
674 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
675 .unwrap(),
676 editor
677 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
678 .unwrap()
679 );
680}
681
682#[gpui::test]
683async fn test_navigation_history(cx: &mut TestAppContext) {
684 init_test(cx, |_| {});
685
686 use workspace::item::Item;
687
688 let fs = FakeFs::new(cx.executor());
689 let project = Project::test(fs, [], cx).await;
690 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
691 let pane = workspace
692 .update(cx, |workspace, _, _| workspace.active_pane().clone())
693 .unwrap();
694
695 _ = workspace.update(cx, |_v, window, cx| {
696 cx.new(|cx| {
697 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
698 let mut editor = build_editor(buffer.clone(), window, cx);
699 let handle = cx.entity();
700 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
701
702 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
703 editor.nav_history.as_mut().unwrap().pop_backward(cx)
704 }
705
706 // Move the cursor a small distance.
707 // Nothing is added to the navigation history.
708 editor.change_selections(None, window, cx, |s| {
709 s.select_display_ranges([
710 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
711 ])
712 });
713 editor.change_selections(None, window, cx, |s| {
714 s.select_display_ranges([
715 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
716 ])
717 });
718 assert!(pop_history(&mut editor, cx).is_none());
719
720 // Move the cursor a large distance.
721 // The history can jump back to the previous position.
722 editor.change_selections(None, window, cx, |s| {
723 s.select_display_ranges([
724 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
725 ])
726 });
727 let nav_entry = pop_history(&mut editor, cx).unwrap();
728 editor.navigate(nav_entry.data.unwrap(), window, cx);
729 assert_eq!(nav_entry.item.id(), cx.entity_id());
730 assert_eq!(
731 editor.selections.display_ranges(cx),
732 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
733 );
734 assert!(pop_history(&mut editor, cx).is_none());
735
736 // Move the cursor a small distance via the mouse.
737 // Nothing is added to the navigation history.
738 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
739 editor.end_selection(window, cx);
740 assert_eq!(
741 editor.selections.display_ranges(cx),
742 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
743 );
744 assert!(pop_history(&mut editor, cx).is_none());
745
746 // Move the cursor a large distance via the mouse.
747 // The history can jump back to the previous position.
748 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
749 editor.end_selection(window, cx);
750 assert_eq!(
751 editor.selections.display_ranges(cx),
752 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
753 );
754 let nav_entry = pop_history(&mut editor, cx).unwrap();
755 editor.navigate(nav_entry.data.unwrap(), window, cx);
756 assert_eq!(nav_entry.item.id(), cx.entity_id());
757 assert_eq!(
758 editor.selections.display_ranges(cx),
759 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
760 );
761 assert!(pop_history(&mut editor, cx).is_none());
762
763 // Set scroll position to check later
764 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
765 let original_scroll_position = editor.scroll_manager.anchor();
766
767 // Jump to the end of the document and adjust scroll
768 editor.move_to_end(&MoveToEnd, window, cx);
769 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
770 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
771
772 let nav_entry = pop_history(&mut editor, cx).unwrap();
773 editor.navigate(nav_entry.data.unwrap(), window, cx);
774 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
775
776 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
777 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
778 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
779 let invalid_point = Point::new(9999, 0);
780 editor.navigate(
781 Box::new(NavigationData {
782 cursor_anchor: invalid_anchor,
783 cursor_position: invalid_point,
784 scroll_anchor: ScrollAnchor {
785 anchor: invalid_anchor,
786 offset: Default::default(),
787 },
788 scroll_top_row: invalid_point.row,
789 }),
790 window,
791 cx,
792 );
793 assert_eq!(
794 editor.selections.display_ranges(cx),
795 &[editor.max_point(cx)..editor.max_point(cx)]
796 );
797 assert_eq!(
798 editor.scroll_position(cx),
799 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
800 );
801
802 editor
803 })
804 });
805}
806
807#[gpui::test]
808fn test_cancel(cx: &mut TestAppContext) {
809 init_test(cx, |_| {});
810
811 let editor = cx.add_window(|window, cx| {
812 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
813 build_editor(buffer, window, cx)
814 });
815
816 _ = editor.update(cx, |editor, window, cx| {
817 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
818 editor.update_selection(
819 DisplayPoint::new(DisplayRow(1), 1),
820 0,
821 gpui::Point::<f32>::default(),
822 window,
823 cx,
824 );
825 editor.end_selection(window, cx);
826
827 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
828 editor.update_selection(
829 DisplayPoint::new(DisplayRow(0), 3),
830 0,
831 gpui::Point::<f32>::default(),
832 window,
833 cx,
834 );
835 editor.end_selection(window, cx);
836 assert_eq!(
837 editor.selections.display_ranges(cx),
838 [
839 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
840 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
841 ]
842 );
843 });
844
845 _ = editor.update(cx, |editor, window, cx| {
846 editor.cancel(&Cancel, window, cx);
847 assert_eq!(
848 editor.selections.display_ranges(cx),
849 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
850 );
851 });
852
853 _ = editor.update(cx, |editor, window, cx| {
854 editor.cancel(&Cancel, window, cx);
855 assert_eq!(
856 editor.selections.display_ranges(cx),
857 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
858 );
859 });
860}
861
862#[gpui::test]
863fn test_fold_action(cx: &mut TestAppContext) {
864 init_test(cx, |_| {});
865
866 let editor = cx.add_window(|window, cx| {
867 let buffer = MultiBuffer::build_simple(
868 &"
869 impl Foo {
870 // Hello!
871
872 fn a() {
873 1
874 }
875
876 fn b() {
877 2
878 }
879
880 fn c() {
881 3
882 }
883 }
884 "
885 .unindent(),
886 cx,
887 );
888 build_editor(buffer.clone(), window, cx)
889 });
890
891 _ = editor.update(cx, |editor, window, cx| {
892 editor.change_selections(None, window, cx, |s| {
893 s.select_display_ranges([
894 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
895 ]);
896 });
897 editor.fold(&Fold, window, cx);
898 assert_eq!(
899 editor.display_text(cx),
900 "
901 impl Foo {
902 // Hello!
903
904 fn a() {
905 1
906 }
907
908 fn b() {⋯
909 }
910
911 fn c() {⋯
912 }
913 }
914 "
915 .unindent(),
916 );
917
918 editor.fold(&Fold, window, cx);
919 assert_eq!(
920 editor.display_text(cx),
921 "
922 impl Foo {⋯
923 }
924 "
925 .unindent(),
926 );
927
928 editor.unfold_lines(&UnfoldLines, window, cx);
929 assert_eq!(
930 editor.display_text(cx),
931 "
932 impl Foo {
933 // Hello!
934
935 fn a() {
936 1
937 }
938
939 fn b() {⋯
940 }
941
942 fn c() {⋯
943 }
944 }
945 "
946 .unindent(),
947 );
948
949 editor.unfold_lines(&UnfoldLines, window, cx);
950 assert_eq!(
951 editor.display_text(cx),
952 editor.buffer.read(cx).read(cx).text()
953 );
954 });
955}
956
957#[gpui::test]
958fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
959 init_test(cx, |_| {});
960
961 let editor = cx.add_window(|window, cx| {
962 let buffer = MultiBuffer::build_simple(
963 &"
964 class Foo:
965 # Hello!
966
967 def a():
968 print(1)
969
970 def b():
971 print(2)
972
973 def c():
974 print(3)
975 "
976 .unindent(),
977 cx,
978 );
979 build_editor(buffer.clone(), window, cx)
980 });
981
982 _ = editor.update(cx, |editor, window, cx| {
983 editor.change_selections(None, window, cx, |s| {
984 s.select_display_ranges([
985 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
986 ]);
987 });
988 editor.fold(&Fold, window, cx);
989 assert_eq!(
990 editor.display_text(cx),
991 "
992 class Foo:
993 # Hello!
994
995 def a():
996 print(1)
997
998 def b():⋯
999
1000 def c():⋯
1001 "
1002 .unindent(),
1003 );
1004
1005 editor.fold(&Fold, window, cx);
1006 assert_eq!(
1007 editor.display_text(cx),
1008 "
1009 class Foo:⋯
1010 "
1011 .unindent(),
1012 );
1013
1014 editor.unfold_lines(&UnfoldLines, window, cx);
1015 assert_eq!(
1016 editor.display_text(cx),
1017 "
1018 class Foo:
1019 # Hello!
1020
1021 def a():
1022 print(1)
1023
1024 def b():⋯
1025
1026 def c():⋯
1027 "
1028 .unindent(),
1029 );
1030
1031 editor.unfold_lines(&UnfoldLines, window, cx);
1032 assert_eq!(
1033 editor.display_text(cx),
1034 editor.buffer.read(cx).read(cx).text()
1035 );
1036 });
1037}
1038
1039#[gpui::test]
1040fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1041 init_test(cx, |_| {});
1042
1043 let editor = cx.add_window(|window, cx| {
1044 let buffer = MultiBuffer::build_simple(
1045 &"
1046 class Foo:
1047 # Hello!
1048
1049 def a():
1050 print(1)
1051
1052 def b():
1053 print(2)
1054
1055
1056 def c():
1057 print(3)
1058
1059
1060 "
1061 .unindent(),
1062 cx,
1063 );
1064 build_editor(buffer.clone(), window, cx)
1065 });
1066
1067 _ = editor.update(cx, |editor, window, cx| {
1068 editor.change_selections(None, window, cx, |s| {
1069 s.select_display_ranges([
1070 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1071 ]);
1072 });
1073 editor.fold(&Fold, window, cx);
1074 assert_eq!(
1075 editor.display_text(cx),
1076 "
1077 class Foo:
1078 # Hello!
1079
1080 def a():
1081 print(1)
1082
1083 def b():⋯
1084
1085
1086 def c():⋯
1087
1088
1089 "
1090 .unindent(),
1091 );
1092
1093 editor.fold(&Fold, window, cx);
1094 assert_eq!(
1095 editor.display_text(cx),
1096 "
1097 class Foo:⋯
1098
1099
1100 "
1101 .unindent(),
1102 );
1103
1104 editor.unfold_lines(&UnfoldLines, window, cx);
1105 assert_eq!(
1106 editor.display_text(cx),
1107 "
1108 class Foo:
1109 # Hello!
1110
1111 def a():
1112 print(1)
1113
1114 def b():⋯
1115
1116
1117 def c():⋯
1118
1119
1120 "
1121 .unindent(),
1122 );
1123
1124 editor.unfold_lines(&UnfoldLines, window, cx);
1125 assert_eq!(
1126 editor.display_text(cx),
1127 editor.buffer.read(cx).read(cx).text()
1128 );
1129 });
1130}
1131
1132#[gpui::test]
1133fn test_fold_at_level(cx: &mut TestAppContext) {
1134 init_test(cx, |_| {});
1135
1136 let editor = cx.add_window(|window, cx| {
1137 let buffer = MultiBuffer::build_simple(
1138 &"
1139 class Foo:
1140 # Hello!
1141
1142 def a():
1143 print(1)
1144
1145 def b():
1146 print(2)
1147
1148
1149 class Bar:
1150 # World!
1151
1152 def a():
1153 print(1)
1154
1155 def b():
1156 print(2)
1157
1158
1159 "
1160 .unindent(),
1161 cx,
1162 );
1163 build_editor(buffer.clone(), window, cx)
1164 });
1165
1166 _ = editor.update(cx, |editor, window, cx| {
1167 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1168 assert_eq!(
1169 editor.display_text(cx),
1170 "
1171 class Foo:
1172 # Hello!
1173
1174 def a():⋯
1175
1176 def b():⋯
1177
1178
1179 class Bar:
1180 # World!
1181
1182 def a():⋯
1183
1184 def b():⋯
1185
1186
1187 "
1188 .unindent(),
1189 );
1190
1191 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1192 assert_eq!(
1193 editor.display_text(cx),
1194 "
1195 class Foo:⋯
1196
1197
1198 class Bar:⋯
1199
1200
1201 "
1202 .unindent(),
1203 );
1204
1205 editor.unfold_all(&UnfoldAll, window, cx);
1206 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1207 assert_eq!(
1208 editor.display_text(cx),
1209 "
1210 class Foo:
1211 # Hello!
1212
1213 def a():
1214 print(1)
1215
1216 def b():
1217 print(2)
1218
1219
1220 class Bar:
1221 # World!
1222
1223 def a():
1224 print(1)
1225
1226 def b():
1227 print(2)
1228
1229
1230 "
1231 .unindent(),
1232 );
1233
1234 assert_eq!(
1235 editor.display_text(cx),
1236 editor.buffer.read(cx).read(cx).text()
1237 );
1238 });
1239}
1240
1241#[gpui::test]
1242fn test_move_cursor(cx: &mut TestAppContext) {
1243 init_test(cx, |_| {});
1244
1245 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1246 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1247
1248 buffer.update(cx, |buffer, cx| {
1249 buffer.edit(
1250 vec![
1251 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1252 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1253 ],
1254 None,
1255 cx,
1256 );
1257 });
1258 _ = editor.update(cx, |editor, window, cx| {
1259 assert_eq!(
1260 editor.selections.display_ranges(cx),
1261 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1262 );
1263
1264 editor.move_down(&MoveDown, window, cx);
1265 assert_eq!(
1266 editor.selections.display_ranges(cx),
1267 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1268 );
1269
1270 editor.move_right(&MoveRight, window, cx);
1271 assert_eq!(
1272 editor.selections.display_ranges(cx),
1273 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1274 );
1275
1276 editor.move_left(&MoveLeft, window, cx);
1277 assert_eq!(
1278 editor.selections.display_ranges(cx),
1279 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1280 );
1281
1282 editor.move_up(&MoveUp, window, cx);
1283 assert_eq!(
1284 editor.selections.display_ranges(cx),
1285 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1286 );
1287
1288 editor.move_to_end(&MoveToEnd, window, cx);
1289 assert_eq!(
1290 editor.selections.display_ranges(cx),
1291 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1292 );
1293
1294 editor.move_to_beginning(&MoveToBeginning, window, cx);
1295 assert_eq!(
1296 editor.selections.display_ranges(cx),
1297 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1298 );
1299
1300 editor.change_selections(None, window, cx, |s| {
1301 s.select_display_ranges([
1302 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1303 ]);
1304 });
1305 editor.select_to_beginning(&SelectToBeginning, window, cx);
1306 assert_eq!(
1307 editor.selections.display_ranges(cx),
1308 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1309 );
1310
1311 editor.select_to_end(&SelectToEnd, window, cx);
1312 assert_eq!(
1313 editor.selections.display_ranges(cx),
1314 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1315 );
1316 });
1317}
1318
1319// TODO: Re-enable this test
1320#[cfg(target_os = "macos")]
1321#[gpui::test]
1322fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1323 init_test(cx, |_| {});
1324
1325 let editor = cx.add_window(|window, cx| {
1326 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1327 build_editor(buffer.clone(), window, cx)
1328 });
1329
1330 assert_eq!('🟥'.len_utf8(), 4);
1331 assert_eq!('α'.len_utf8(), 2);
1332
1333 _ = editor.update(cx, |editor, window, cx| {
1334 editor.fold_creases(
1335 vec![
1336 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1338 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1339 ],
1340 true,
1341 window,
1342 cx,
1343 );
1344 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1345
1346 editor.move_right(&MoveRight, window, cx);
1347 assert_eq!(
1348 editor.selections.display_ranges(cx),
1349 &[empty_range(0, "🟥".len())]
1350 );
1351 editor.move_right(&MoveRight, window, cx);
1352 assert_eq!(
1353 editor.selections.display_ranges(cx),
1354 &[empty_range(0, "🟥🟧".len())]
1355 );
1356 editor.move_right(&MoveRight, window, cx);
1357 assert_eq!(
1358 editor.selections.display_ranges(cx),
1359 &[empty_range(0, "🟥🟧⋯".len())]
1360 );
1361
1362 editor.move_down(&MoveDown, window, cx);
1363 assert_eq!(
1364 editor.selections.display_ranges(cx),
1365 &[empty_range(1, "ab⋯e".len())]
1366 );
1367 editor.move_left(&MoveLeft, window, cx);
1368 assert_eq!(
1369 editor.selections.display_ranges(cx),
1370 &[empty_range(1, "ab⋯".len())]
1371 );
1372 editor.move_left(&MoveLeft, window, cx);
1373 assert_eq!(
1374 editor.selections.display_ranges(cx),
1375 &[empty_range(1, "ab".len())]
1376 );
1377 editor.move_left(&MoveLeft, window, cx);
1378 assert_eq!(
1379 editor.selections.display_ranges(cx),
1380 &[empty_range(1, "a".len())]
1381 );
1382
1383 editor.move_down(&MoveDown, window, cx);
1384 assert_eq!(
1385 editor.selections.display_ranges(cx),
1386 &[empty_range(2, "α".len())]
1387 );
1388 editor.move_right(&MoveRight, window, cx);
1389 assert_eq!(
1390 editor.selections.display_ranges(cx),
1391 &[empty_range(2, "αβ".len())]
1392 );
1393 editor.move_right(&MoveRight, window, cx);
1394 assert_eq!(
1395 editor.selections.display_ranges(cx),
1396 &[empty_range(2, "αβ⋯".len())]
1397 );
1398 editor.move_right(&MoveRight, window, cx);
1399 assert_eq!(
1400 editor.selections.display_ranges(cx),
1401 &[empty_range(2, "αβ⋯ε".len())]
1402 );
1403
1404 editor.move_up(&MoveUp, window, cx);
1405 assert_eq!(
1406 editor.selections.display_ranges(cx),
1407 &[empty_range(1, "ab⋯e".len())]
1408 );
1409 editor.move_down(&MoveDown, window, cx);
1410 assert_eq!(
1411 editor.selections.display_ranges(cx),
1412 &[empty_range(2, "αβ⋯ε".len())]
1413 );
1414 editor.move_up(&MoveUp, window, cx);
1415 assert_eq!(
1416 editor.selections.display_ranges(cx),
1417 &[empty_range(1, "ab⋯e".len())]
1418 );
1419
1420 editor.move_up(&MoveUp, window, cx);
1421 assert_eq!(
1422 editor.selections.display_ranges(cx),
1423 &[empty_range(0, "🟥🟧".len())]
1424 );
1425 editor.move_left(&MoveLeft, window, cx);
1426 assert_eq!(
1427 editor.selections.display_ranges(cx),
1428 &[empty_range(0, "🟥".len())]
1429 );
1430 editor.move_left(&MoveLeft, window, cx);
1431 assert_eq!(
1432 editor.selections.display_ranges(cx),
1433 &[empty_range(0, "".len())]
1434 );
1435 });
1436}
1437
1438#[gpui::test]
1439fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1440 init_test(cx, |_| {});
1441
1442 let editor = cx.add_window(|window, cx| {
1443 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1444 build_editor(buffer.clone(), window, cx)
1445 });
1446 _ = editor.update(cx, |editor, window, cx| {
1447 editor.change_selections(None, window, cx, |s| {
1448 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1449 });
1450
1451 // moving above start of document should move selection to start of document,
1452 // but the next move down should still be at the original goal_x
1453 editor.move_up(&MoveUp, window, cx);
1454 assert_eq!(
1455 editor.selections.display_ranges(cx),
1456 &[empty_range(0, "".len())]
1457 );
1458
1459 editor.move_down(&MoveDown, window, cx);
1460 assert_eq!(
1461 editor.selections.display_ranges(cx),
1462 &[empty_range(1, "abcd".len())]
1463 );
1464
1465 editor.move_down(&MoveDown, window, cx);
1466 assert_eq!(
1467 editor.selections.display_ranges(cx),
1468 &[empty_range(2, "αβγ".len())]
1469 );
1470
1471 editor.move_down(&MoveDown, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[empty_range(3, "abcd".len())]
1475 );
1476
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1481 );
1482
1483 // moving past end of document should not change goal_x
1484 editor.move_down(&MoveDown, window, cx);
1485 assert_eq!(
1486 editor.selections.display_ranges(cx),
1487 &[empty_range(5, "".len())]
1488 );
1489
1490 editor.move_down(&MoveDown, window, cx);
1491 assert_eq!(
1492 editor.selections.display_ranges(cx),
1493 &[empty_range(5, "".len())]
1494 );
1495
1496 editor.move_up(&MoveUp, window, cx);
1497 assert_eq!(
1498 editor.selections.display_ranges(cx),
1499 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1500 );
1501
1502 editor.move_up(&MoveUp, window, cx);
1503 assert_eq!(
1504 editor.selections.display_ranges(cx),
1505 &[empty_range(3, "abcd".len())]
1506 );
1507
1508 editor.move_up(&MoveUp, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(2, "αβγ".len())]
1512 );
1513 });
1514}
1515
1516#[gpui::test]
1517fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1518 init_test(cx, |_| {});
1519 let move_to_beg = MoveToBeginningOfLine {
1520 stop_at_soft_wraps: true,
1521 stop_at_indent: true,
1522 };
1523
1524 let delete_to_beg = DeleteToBeginningOfLine {
1525 stop_at_indent: false,
1526 };
1527
1528 let move_to_end = MoveToEndOfLine {
1529 stop_at_soft_wraps: true,
1530 };
1531
1532 let editor = cx.add_window(|window, cx| {
1533 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1534 build_editor(buffer, window, cx)
1535 });
1536 _ = editor.update(cx, |editor, window, cx| {
1537 editor.change_selections(None, window, cx, |s| {
1538 s.select_display_ranges([
1539 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1540 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1541 ]);
1542 });
1543 });
1544
1545 _ = editor.update(cx, |editor, window, cx| {
1546 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1547 assert_eq!(
1548 editor.selections.display_ranges(cx),
1549 &[
1550 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1551 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1552 ]
1553 );
1554 });
1555
1556 _ = editor.update(cx, |editor, window, cx| {
1557 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1558 assert_eq!(
1559 editor.selections.display_ranges(cx),
1560 &[
1561 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1562 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1563 ]
1564 );
1565 });
1566
1567 _ = editor.update(cx, |editor, window, cx| {
1568 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1569 assert_eq!(
1570 editor.selections.display_ranges(cx),
1571 &[
1572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1573 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1574 ]
1575 );
1576 });
1577
1578 _ = editor.update(cx, |editor, window, cx| {
1579 editor.move_to_end_of_line(&move_to_end, window, cx);
1580 assert_eq!(
1581 editor.selections.display_ranges(cx),
1582 &[
1583 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1584 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1585 ]
1586 );
1587 });
1588
1589 // Moving to the end of line again is a no-op.
1590 _ = editor.update(cx, |editor, window, cx| {
1591 editor.move_to_end_of_line(&move_to_end, window, cx);
1592 assert_eq!(
1593 editor.selections.display_ranges(cx),
1594 &[
1595 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1596 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1597 ]
1598 );
1599 });
1600
1601 _ = editor.update(cx, |editor, window, cx| {
1602 editor.move_left(&MoveLeft, window, cx);
1603 editor.select_to_beginning_of_line(
1604 &SelectToBeginningOfLine {
1605 stop_at_soft_wraps: true,
1606 stop_at_indent: true,
1607 },
1608 window,
1609 cx,
1610 );
1611 assert_eq!(
1612 editor.selections.display_ranges(cx),
1613 &[
1614 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1615 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1616 ]
1617 );
1618 });
1619
1620 _ = editor.update(cx, |editor, window, cx| {
1621 editor.select_to_beginning_of_line(
1622 &SelectToBeginningOfLine {
1623 stop_at_soft_wraps: true,
1624 stop_at_indent: true,
1625 },
1626 window,
1627 cx,
1628 );
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[
1632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1633 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1634 ]
1635 );
1636 });
1637
1638 _ = editor.update(cx, |editor, window, cx| {
1639 editor.select_to_beginning_of_line(
1640 &SelectToBeginningOfLine {
1641 stop_at_soft_wraps: true,
1642 stop_at_indent: true,
1643 },
1644 window,
1645 cx,
1646 );
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[
1650 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1651 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1652 ]
1653 );
1654 });
1655
1656 _ = editor.update(cx, |editor, window, cx| {
1657 editor.select_to_end_of_line(
1658 &SelectToEndOfLine {
1659 stop_at_soft_wraps: true,
1660 },
1661 window,
1662 cx,
1663 );
1664 assert_eq!(
1665 editor.selections.display_ranges(cx),
1666 &[
1667 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1669 ]
1670 );
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1675 assert_eq!(editor.display_text(cx), "ab\n de");
1676 assert_eq!(
1677 editor.selections.display_ranges(cx),
1678 &[
1679 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1680 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1681 ]
1682 );
1683 });
1684
1685 _ = editor.update(cx, |editor, window, cx| {
1686 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1687 assert_eq!(editor.display_text(cx), "\n");
1688 assert_eq!(
1689 editor.selections.display_ranges(cx),
1690 &[
1691 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1692 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1693 ]
1694 );
1695 });
1696}
1697
1698#[gpui::test]
1699fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1700 init_test(cx, |_| {});
1701 let move_to_beg = MoveToBeginningOfLine {
1702 stop_at_soft_wraps: false,
1703 stop_at_indent: false,
1704 };
1705
1706 let move_to_end = MoveToEndOfLine {
1707 stop_at_soft_wraps: false,
1708 };
1709
1710 let editor = cx.add_window(|window, cx| {
1711 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1712 build_editor(buffer, window, cx)
1713 });
1714
1715 _ = editor.update(cx, |editor, window, cx| {
1716 editor.set_wrap_width(Some(140.0.into()), cx);
1717
1718 // We expect the following lines after wrapping
1719 // ```
1720 // thequickbrownfox
1721 // jumpedoverthelazydo
1722 // gs
1723 // ```
1724 // The final `gs` was soft-wrapped onto a new line.
1725 assert_eq!(
1726 "thequickbrownfox\njumpedoverthelaz\nydogs",
1727 editor.display_text(cx),
1728 );
1729
1730 // First, let's assert behavior on the first line, that was not soft-wrapped.
1731 // Start the cursor at the `k` on the first line
1732 editor.change_selections(None, window, cx, |s| {
1733 s.select_display_ranges([
1734 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1735 ]);
1736 });
1737
1738 // Moving to the beginning of the line should put us at the beginning of the line.
1739 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1740 assert_eq!(
1741 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1742 editor.selections.display_ranges(cx)
1743 );
1744
1745 // Moving to the end of the line should put us at the end of the line.
1746 editor.move_to_end_of_line(&move_to_end, window, cx);
1747 assert_eq!(
1748 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1749 editor.selections.display_ranges(cx)
1750 );
1751
1752 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1753 // Start the cursor at the last line (`y` that was wrapped to a new line)
1754 editor.change_selections(None, window, cx, |s| {
1755 s.select_display_ranges([
1756 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1757 ]);
1758 });
1759
1760 // Moving to the beginning of the line should put us at the start of the second line of
1761 // display text, i.e., the `j`.
1762 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1763 assert_eq!(
1764 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1765 editor.selections.display_ranges(cx)
1766 );
1767
1768 // Moving to the beginning of the line again should be a no-op.
1769 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1770 assert_eq!(
1771 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1772 editor.selections.display_ranges(cx)
1773 );
1774
1775 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1776 // next display line.
1777 editor.move_to_end_of_line(&move_to_end, window, cx);
1778 assert_eq!(
1779 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1780 editor.selections.display_ranges(cx)
1781 );
1782
1783 // Moving to the end of the line again should be a no-op.
1784 editor.move_to_end_of_line(&move_to_end, window, cx);
1785 assert_eq!(
1786 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1787 editor.selections.display_ranges(cx)
1788 );
1789 });
1790}
1791
1792#[gpui::test]
1793fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1794 init_test(cx, |_| {});
1795
1796 let move_to_beg = MoveToBeginningOfLine {
1797 stop_at_soft_wraps: true,
1798 stop_at_indent: true,
1799 };
1800
1801 let select_to_beg = SelectToBeginningOfLine {
1802 stop_at_soft_wraps: true,
1803 stop_at_indent: true,
1804 };
1805
1806 let delete_to_beg = DeleteToBeginningOfLine {
1807 stop_at_indent: true,
1808 };
1809
1810 let move_to_end = MoveToEndOfLine {
1811 stop_at_soft_wraps: false,
1812 };
1813
1814 let editor = cx.add_window(|window, cx| {
1815 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1816 build_editor(buffer, window, cx)
1817 });
1818
1819 _ = editor.update(cx, |editor, window, cx| {
1820 editor.change_selections(None, window, cx, |s| {
1821 s.select_display_ranges([
1822 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1823 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1824 ]);
1825 });
1826
1827 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1828 // and the second cursor at the first non-whitespace character in the line.
1829 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1830 assert_eq!(
1831 editor.selections.display_ranges(cx),
1832 &[
1833 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1834 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1835 ]
1836 );
1837
1838 // Moving to the beginning of the line again should be a no-op for the first cursor,
1839 // and should move the second cursor to the beginning of the line.
1840 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1841 assert_eq!(
1842 editor.selections.display_ranges(cx),
1843 &[
1844 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1845 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1846 ]
1847 );
1848
1849 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1850 // and should move the second cursor back to the first non-whitespace character in the line.
1851 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1852 assert_eq!(
1853 editor.selections.display_ranges(cx),
1854 &[
1855 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1856 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1857 ]
1858 );
1859
1860 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1861 // and to the first non-whitespace character in the line for the second cursor.
1862 editor.move_to_end_of_line(&move_to_end, window, cx);
1863 editor.move_left(&MoveLeft, window, cx);
1864 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1865 assert_eq!(
1866 editor.selections.display_ranges(cx),
1867 &[
1868 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1869 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1870 ]
1871 );
1872
1873 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1874 // and should select to the beginning of the line for the second cursor.
1875 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1876 assert_eq!(
1877 editor.selections.display_ranges(cx),
1878 &[
1879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1880 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1881 ]
1882 );
1883
1884 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1885 // and should delete to the first non-whitespace character in the line for the second cursor.
1886 editor.move_to_end_of_line(&move_to_end, window, cx);
1887 editor.move_left(&MoveLeft, window, cx);
1888 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1889 assert_eq!(editor.text(cx), "c\n f");
1890 });
1891}
1892
1893#[gpui::test]
1894fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1895 init_test(cx, |_| {});
1896
1897 let editor = cx.add_window(|window, cx| {
1898 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1899 build_editor(buffer, window, cx)
1900 });
1901 _ = editor.update(cx, |editor, window, cx| {
1902 editor.change_selections(None, window, cx, |s| {
1903 s.select_display_ranges([
1904 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1905 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1906 ])
1907 });
1908
1909 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1910 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1911
1912 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1913 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1914
1915 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1916 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1917
1918 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1919 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1920
1921 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1922 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1923
1924 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1925 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1926
1927 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1928 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1929
1930 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1931 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1932
1933 editor.move_right(&MoveRight, window, cx);
1934 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1935 assert_selection_ranges(
1936 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1937 editor,
1938 cx,
1939 );
1940
1941 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1942 assert_selection_ranges(
1943 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1944 editor,
1945 cx,
1946 );
1947
1948 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1949 assert_selection_ranges(
1950 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1951 editor,
1952 cx,
1953 );
1954 });
1955}
1956
1957#[gpui::test]
1958fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1959 init_test(cx, |_| {});
1960
1961 let editor = cx.add_window(|window, cx| {
1962 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1963 build_editor(buffer, window, cx)
1964 });
1965
1966 _ = editor.update(cx, |editor, window, cx| {
1967 editor.set_wrap_width(Some(140.0.into()), cx);
1968 assert_eq!(
1969 editor.display_text(cx),
1970 "use one::{\n two::three::\n four::five\n};"
1971 );
1972
1973 editor.change_selections(None, window, cx, |s| {
1974 s.select_display_ranges([
1975 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1976 ]);
1977 });
1978
1979 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1980 assert_eq!(
1981 editor.selections.display_ranges(cx),
1982 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1983 );
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_eq!(
1987 editor.selections.display_ranges(cx),
1988 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1989 );
1990
1991 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1992 assert_eq!(
1993 editor.selections.display_ranges(cx),
1994 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1995 );
1996
1997 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1998 assert_eq!(
1999 editor.selections.display_ranges(cx),
2000 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2001 );
2002
2003 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2004 assert_eq!(
2005 editor.selections.display_ranges(cx),
2006 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2007 );
2008
2009 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2010 assert_eq!(
2011 editor.selections.display_ranges(cx),
2012 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2013 );
2014 });
2015}
2016
2017#[gpui::test]
2018async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2019 init_test(cx, |_| {});
2020 let mut cx = EditorTestContext::new(cx).await;
2021
2022 let line_height = cx.editor(|editor, window, _| {
2023 editor
2024 .style()
2025 .unwrap()
2026 .text
2027 .line_height_in_pixels(window.rem_size())
2028 });
2029 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2030
2031 cx.set_state(
2032 &r#"ˇone
2033 two
2034
2035 three
2036 fourˇ
2037 five
2038
2039 six"#
2040 .unindent(),
2041 );
2042
2043 cx.update_editor(|editor, window, cx| {
2044 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2045 });
2046 cx.assert_editor_state(
2047 &r#"one
2048 two
2049 ˇ
2050 three
2051 four
2052 five
2053 ˇ
2054 six"#
2055 .unindent(),
2056 );
2057
2058 cx.update_editor(|editor, window, cx| {
2059 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2060 });
2061 cx.assert_editor_state(
2062 &r#"one
2063 two
2064
2065 three
2066 four
2067 five
2068 ˇ
2069 sixˇ"#
2070 .unindent(),
2071 );
2072
2073 cx.update_editor(|editor, window, cx| {
2074 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2075 });
2076 cx.assert_editor_state(
2077 &r#"one
2078 two
2079
2080 three
2081 four
2082 five
2083
2084 sixˇ"#
2085 .unindent(),
2086 );
2087
2088 cx.update_editor(|editor, window, cx| {
2089 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2090 });
2091 cx.assert_editor_state(
2092 &r#"one
2093 two
2094
2095 three
2096 four
2097 five
2098 ˇ
2099 six"#
2100 .unindent(),
2101 );
2102
2103 cx.update_editor(|editor, window, cx| {
2104 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2105 });
2106 cx.assert_editor_state(
2107 &r#"one
2108 two
2109 ˇ
2110 three
2111 four
2112 five
2113
2114 six"#
2115 .unindent(),
2116 );
2117
2118 cx.update_editor(|editor, window, cx| {
2119 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2120 });
2121 cx.assert_editor_state(
2122 &r#"ˇone
2123 two
2124
2125 three
2126 four
2127 five
2128
2129 six"#
2130 .unindent(),
2131 );
2132}
2133
2134#[gpui::test]
2135async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2136 init_test(cx, |_| {});
2137 let mut cx = EditorTestContext::new(cx).await;
2138 let line_height = cx.editor(|editor, window, _| {
2139 editor
2140 .style()
2141 .unwrap()
2142 .text
2143 .line_height_in_pixels(window.rem_size())
2144 });
2145 let window = cx.window;
2146 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2147
2148 cx.set_state(
2149 r#"ˇone
2150 two
2151 three
2152 four
2153 five
2154 six
2155 seven
2156 eight
2157 nine
2158 ten
2159 "#,
2160 );
2161
2162 cx.update_editor(|editor, window, cx| {
2163 assert_eq!(
2164 editor.snapshot(window, cx).scroll_position(),
2165 gpui::Point::new(0., 0.)
2166 );
2167 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2168 assert_eq!(
2169 editor.snapshot(window, cx).scroll_position(),
2170 gpui::Point::new(0., 3.)
2171 );
2172 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2173 assert_eq!(
2174 editor.snapshot(window, cx).scroll_position(),
2175 gpui::Point::new(0., 6.)
2176 );
2177 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2178 assert_eq!(
2179 editor.snapshot(window, cx).scroll_position(),
2180 gpui::Point::new(0., 3.)
2181 );
2182
2183 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2184 assert_eq!(
2185 editor.snapshot(window, cx).scroll_position(),
2186 gpui::Point::new(0., 1.)
2187 );
2188 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2189 assert_eq!(
2190 editor.snapshot(window, cx).scroll_position(),
2191 gpui::Point::new(0., 3.)
2192 );
2193 });
2194}
2195
2196#[gpui::test]
2197async fn test_autoscroll(cx: &mut TestAppContext) {
2198 init_test(cx, |_| {});
2199 let mut cx = EditorTestContext::new(cx).await;
2200
2201 let line_height = cx.update_editor(|editor, window, cx| {
2202 editor.set_vertical_scroll_margin(2, cx);
2203 editor
2204 .style()
2205 .unwrap()
2206 .text
2207 .line_height_in_pixels(window.rem_size())
2208 });
2209 let window = cx.window;
2210 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2211
2212 cx.set_state(
2213 r#"ˇone
2214 two
2215 three
2216 four
2217 five
2218 six
2219 seven
2220 eight
2221 nine
2222 ten
2223 "#,
2224 );
2225 cx.update_editor(|editor, window, cx| {
2226 assert_eq!(
2227 editor.snapshot(window, cx).scroll_position(),
2228 gpui::Point::new(0., 0.0)
2229 );
2230 });
2231
2232 // Add a cursor below the visible area. Since both cursors cannot fit
2233 // on screen, the editor autoscrolls to reveal the newest cursor, and
2234 // allows the vertical scroll margin below that cursor.
2235 cx.update_editor(|editor, window, cx| {
2236 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2237 selections.select_ranges([
2238 Point::new(0, 0)..Point::new(0, 0),
2239 Point::new(6, 0)..Point::new(6, 0),
2240 ]);
2241 })
2242 });
2243 cx.update_editor(|editor, window, cx| {
2244 assert_eq!(
2245 editor.snapshot(window, cx).scroll_position(),
2246 gpui::Point::new(0., 3.0)
2247 );
2248 });
2249
2250 // Move down. The editor cursor scrolls down to track the newest cursor.
2251 cx.update_editor(|editor, window, cx| {
2252 editor.move_down(&Default::default(), window, cx);
2253 });
2254 cx.update_editor(|editor, window, cx| {
2255 assert_eq!(
2256 editor.snapshot(window, cx).scroll_position(),
2257 gpui::Point::new(0., 4.0)
2258 );
2259 });
2260
2261 // Add a cursor above the visible area. Since both cursors fit on screen,
2262 // the editor scrolls to show both.
2263 cx.update_editor(|editor, window, cx| {
2264 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2265 selections.select_ranges([
2266 Point::new(1, 0)..Point::new(1, 0),
2267 Point::new(6, 0)..Point::new(6, 0),
2268 ]);
2269 })
2270 });
2271 cx.update_editor(|editor, window, cx| {
2272 assert_eq!(
2273 editor.snapshot(window, cx).scroll_position(),
2274 gpui::Point::new(0., 1.0)
2275 );
2276 });
2277}
2278
2279#[gpui::test]
2280async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2281 init_test(cx, |_| {});
2282 let mut cx = EditorTestContext::new(cx).await;
2283
2284 let line_height = cx.editor(|editor, window, _cx| {
2285 editor
2286 .style()
2287 .unwrap()
2288 .text
2289 .line_height_in_pixels(window.rem_size())
2290 });
2291 let window = cx.window;
2292 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2293 cx.set_state(
2294 &r#"
2295 ˇone
2296 two
2297 threeˇ
2298 four
2299 five
2300 six
2301 seven
2302 eight
2303 nine
2304 ten
2305 "#
2306 .unindent(),
2307 );
2308
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_page_down(&MovePageDown::default(), window, cx)
2311 });
2312 cx.assert_editor_state(
2313 &r#"
2314 one
2315 two
2316 three
2317 ˇfour
2318 five
2319 sixˇ
2320 seven
2321 eight
2322 nine
2323 ten
2324 "#
2325 .unindent(),
2326 );
2327
2328 cx.update_editor(|editor, window, cx| {
2329 editor.move_page_down(&MovePageDown::default(), window, cx)
2330 });
2331 cx.assert_editor_state(
2332 &r#"
2333 one
2334 two
2335 three
2336 four
2337 five
2338 six
2339 ˇseven
2340 eight
2341 nineˇ
2342 ten
2343 "#
2344 .unindent(),
2345 );
2346
2347 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2348 cx.assert_editor_state(
2349 &r#"
2350 one
2351 two
2352 three
2353 ˇfour
2354 five
2355 sixˇ
2356 seven
2357 eight
2358 nine
2359 ten
2360 "#
2361 .unindent(),
2362 );
2363
2364 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2365 cx.assert_editor_state(
2366 &r#"
2367 ˇone
2368 two
2369 threeˇ
2370 four
2371 five
2372 six
2373 seven
2374 eight
2375 nine
2376 ten
2377 "#
2378 .unindent(),
2379 );
2380
2381 // Test select collapsing
2382 cx.update_editor(|editor, window, cx| {
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 });
2387 cx.assert_editor_state(
2388 &r#"
2389 one
2390 two
2391 three
2392 four
2393 five
2394 six
2395 seven
2396 eight
2397 nine
2398 ˇten
2399 ˇ"#
2400 .unindent(),
2401 );
2402}
2403
2404#[gpui::test]
2405async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2406 init_test(cx, |_| {});
2407 let mut cx = EditorTestContext::new(cx).await;
2408 cx.set_state("one «two threeˇ» four");
2409 cx.update_editor(|editor, window, cx| {
2410 editor.delete_to_beginning_of_line(
2411 &DeleteToBeginningOfLine {
2412 stop_at_indent: false,
2413 },
2414 window,
2415 cx,
2416 );
2417 assert_eq!(editor.text(cx), " four");
2418 });
2419}
2420
2421#[gpui::test]
2422fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2423 init_test(cx, |_| {});
2424
2425 let editor = cx.add_window(|window, cx| {
2426 let buffer = MultiBuffer::build_simple("one two three four", cx);
2427 build_editor(buffer.clone(), window, cx)
2428 });
2429
2430 _ = editor.update(cx, |editor, window, cx| {
2431 editor.change_selections(None, window, cx, |s| {
2432 s.select_display_ranges([
2433 // an empty selection - the preceding word fragment is deleted
2434 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2435 // characters selected - they are deleted
2436 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2437 ])
2438 });
2439 editor.delete_to_previous_word_start(
2440 &DeleteToPreviousWordStart {
2441 ignore_newlines: false,
2442 },
2443 window,
2444 cx,
2445 );
2446 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2447 });
2448
2449 _ = editor.update(cx, |editor, window, cx| {
2450 editor.change_selections(None, window, cx, |s| {
2451 s.select_display_ranges([
2452 // an empty selection - the following word fragment is deleted
2453 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2454 // characters selected - they are deleted
2455 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2456 ])
2457 });
2458 editor.delete_to_next_word_end(
2459 &DeleteToNextWordEnd {
2460 ignore_newlines: false,
2461 },
2462 window,
2463 cx,
2464 );
2465 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2466 });
2467}
2468
2469#[gpui::test]
2470fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2471 init_test(cx, |_| {});
2472
2473 let editor = cx.add_window(|window, cx| {
2474 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2475 build_editor(buffer.clone(), window, cx)
2476 });
2477 let del_to_prev_word_start = DeleteToPreviousWordStart {
2478 ignore_newlines: false,
2479 };
2480 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2481 ignore_newlines: true,
2482 };
2483
2484 _ = editor.update(cx, |editor, window, cx| {
2485 editor.change_selections(None, window, cx, |s| {
2486 s.select_display_ranges([
2487 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2488 ])
2489 });
2490 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2491 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2492 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2493 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2494 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2495 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2496 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2497 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2498 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2499 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2500 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2501 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2502 });
2503}
2504
2505#[gpui::test]
2506fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2507 init_test(cx, |_| {});
2508
2509 let editor = cx.add_window(|window, cx| {
2510 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2511 build_editor(buffer.clone(), window, cx)
2512 });
2513 let del_to_next_word_end = DeleteToNextWordEnd {
2514 ignore_newlines: false,
2515 };
2516 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2517 ignore_newlines: true,
2518 };
2519
2520 _ = editor.update(cx, |editor, window, cx| {
2521 editor.change_selections(None, window, cx, |s| {
2522 s.select_display_ranges([
2523 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2524 ])
2525 });
2526 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2527 assert_eq!(
2528 editor.buffer.read(cx).read(cx).text(),
2529 "one\n two\nthree\n four"
2530 );
2531 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2532 assert_eq!(
2533 editor.buffer.read(cx).read(cx).text(),
2534 "\n two\nthree\n four"
2535 );
2536 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2537 assert_eq!(
2538 editor.buffer.read(cx).read(cx).text(),
2539 "two\nthree\n four"
2540 );
2541 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2542 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2543 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2544 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2545 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2546 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2547 });
2548}
2549
2550#[gpui::test]
2551fn test_newline(cx: &mut TestAppContext) {
2552 init_test(cx, |_| {});
2553
2554 let editor = cx.add_window(|window, cx| {
2555 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2556 build_editor(buffer.clone(), window, cx)
2557 });
2558
2559 _ = editor.update(cx, |editor, window, cx| {
2560 editor.change_selections(None, window, cx, |s| {
2561 s.select_display_ranges([
2562 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2563 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2564 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2565 ])
2566 });
2567
2568 editor.newline(&Newline, window, cx);
2569 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2570 });
2571}
2572
2573#[gpui::test]
2574fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2575 init_test(cx, |_| {});
2576
2577 let editor = cx.add_window(|window, cx| {
2578 let buffer = MultiBuffer::build_simple(
2579 "
2580 a
2581 b(
2582 X
2583 )
2584 c(
2585 X
2586 )
2587 "
2588 .unindent()
2589 .as_str(),
2590 cx,
2591 );
2592 let mut editor = build_editor(buffer.clone(), window, cx);
2593 editor.change_selections(None, window, cx, |s| {
2594 s.select_ranges([
2595 Point::new(2, 4)..Point::new(2, 5),
2596 Point::new(5, 4)..Point::new(5, 5),
2597 ])
2598 });
2599 editor
2600 });
2601
2602 _ = editor.update(cx, |editor, window, cx| {
2603 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2604 editor.buffer.update(cx, |buffer, cx| {
2605 buffer.edit(
2606 [
2607 (Point::new(1, 2)..Point::new(3, 0), ""),
2608 (Point::new(4, 2)..Point::new(6, 0), ""),
2609 ],
2610 None,
2611 cx,
2612 );
2613 assert_eq!(
2614 buffer.read(cx).text(),
2615 "
2616 a
2617 b()
2618 c()
2619 "
2620 .unindent()
2621 );
2622 });
2623 assert_eq!(
2624 editor.selections.ranges(cx),
2625 &[
2626 Point::new(1, 2)..Point::new(1, 2),
2627 Point::new(2, 2)..Point::new(2, 2),
2628 ],
2629 );
2630
2631 editor.newline(&Newline, window, cx);
2632 assert_eq!(
2633 editor.text(cx),
2634 "
2635 a
2636 b(
2637 )
2638 c(
2639 )
2640 "
2641 .unindent()
2642 );
2643
2644 // The selections are moved after the inserted newlines
2645 assert_eq!(
2646 editor.selections.ranges(cx),
2647 &[
2648 Point::new(2, 0)..Point::new(2, 0),
2649 Point::new(4, 0)..Point::new(4, 0),
2650 ],
2651 );
2652 });
2653}
2654
2655#[gpui::test]
2656async fn test_newline_above(cx: &mut TestAppContext) {
2657 init_test(cx, |settings| {
2658 settings.defaults.tab_size = NonZeroU32::new(4)
2659 });
2660
2661 let language = Arc::new(
2662 Language::new(
2663 LanguageConfig::default(),
2664 Some(tree_sitter_rust::LANGUAGE.into()),
2665 )
2666 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2667 .unwrap(),
2668 );
2669
2670 let mut cx = EditorTestContext::new(cx).await;
2671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2672 cx.set_state(indoc! {"
2673 const a: ˇA = (
2674 (ˇ
2675 «const_functionˇ»(ˇ),
2676 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2677 )ˇ
2678 ˇ);ˇ
2679 "});
2680
2681 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2682 cx.assert_editor_state(indoc! {"
2683 ˇ
2684 const a: A = (
2685 ˇ
2686 (
2687 ˇ
2688 ˇ
2689 const_function(),
2690 ˇ
2691 ˇ
2692 ˇ
2693 ˇ
2694 something_else,
2695 ˇ
2696 )
2697 ˇ
2698 ˇ
2699 );
2700 "});
2701}
2702
2703#[gpui::test]
2704async fn test_newline_below(cx: &mut TestAppContext) {
2705 init_test(cx, |settings| {
2706 settings.defaults.tab_size = NonZeroU32::new(4)
2707 });
2708
2709 let language = Arc::new(
2710 Language::new(
2711 LanguageConfig::default(),
2712 Some(tree_sitter_rust::LANGUAGE.into()),
2713 )
2714 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2715 .unwrap(),
2716 );
2717
2718 let mut cx = EditorTestContext::new(cx).await;
2719 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2720 cx.set_state(indoc! {"
2721 const a: ˇA = (
2722 (ˇ
2723 «const_functionˇ»(ˇ),
2724 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2725 )ˇ
2726 ˇ);ˇ
2727 "});
2728
2729 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2730 cx.assert_editor_state(indoc! {"
2731 const a: A = (
2732 ˇ
2733 (
2734 ˇ
2735 const_function(),
2736 ˇ
2737 ˇ
2738 something_else,
2739 ˇ
2740 ˇ
2741 ˇ
2742 ˇ
2743 )
2744 ˇ
2745 );
2746 ˇ
2747 ˇ
2748 "});
2749}
2750
2751#[gpui::test]
2752async fn test_newline_comments(cx: &mut TestAppContext) {
2753 init_test(cx, |settings| {
2754 settings.defaults.tab_size = NonZeroU32::new(4)
2755 });
2756
2757 let language = Arc::new(Language::new(
2758 LanguageConfig {
2759 line_comments: vec!["//".into()],
2760 ..LanguageConfig::default()
2761 },
2762 None,
2763 ));
2764 {
2765 let mut cx = EditorTestContext::new(cx).await;
2766 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2767 cx.set_state(indoc! {"
2768 // Fooˇ
2769 "});
2770
2771 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2772 cx.assert_editor_state(indoc! {"
2773 // Foo
2774 //ˇ
2775 "});
2776 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2777 cx.set_state(indoc! {"
2778 ˇ// Foo
2779 "});
2780 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2781 cx.assert_editor_state(indoc! {"
2782
2783 ˇ// Foo
2784 "});
2785 }
2786 // Ensure that comment continuations can be disabled.
2787 update_test_language_settings(cx, |settings| {
2788 settings.defaults.extend_comment_on_newline = Some(false);
2789 });
2790 let mut cx = EditorTestContext::new(cx).await;
2791 cx.set_state(indoc! {"
2792 // Fooˇ
2793 "});
2794 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2795 cx.assert_editor_state(indoc! {"
2796 // Foo
2797 ˇ
2798 "});
2799}
2800
2801#[gpui::test]
2802fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2803 init_test(cx, |_| {});
2804
2805 let editor = cx.add_window(|window, cx| {
2806 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2807 let mut editor = build_editor(buffer.clone(), window, cx);
2808 editor.change_selections(None, window, cx, |s| {
2809 s.select_ranges([3..4, 11..12, 19..20])
2810 });
2811 editor
2812 });
2813
2814 _ = editor.update(cx, |editor, window, cx| {
2815 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2816 editor.buffer.update(cx, |buffer, cx| {
2817 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2818 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2819 });
2820 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2821
2822 editor.insert("Z", window, cx);
2823 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2824
2825 // The selections are moved after the inserted characters
2826 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2827 });
2828}
2829
2830#[gpui::test]
2831async fn test_tab(cx: &mut TestAppContext) {
2832 init_test(cx, |settings| {
2833 settings.defaults.tab_size = NonZeroU32::new(3)
2834 });
2835
2836 let mut cx = EditorTestContext::new(cx).await;
2837 cx.set_state(indoc! {"
2838 ˇabˇc
2839 ˇ🏀ˇ🏀ˇefg
2840 dˇ
2841 "});
2842 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2843 cx.assert_editor_state(indoc! {"
2844 ˇab ˇc
2845 ˇ🏀 ˇ🏀 ˇefg
2846 d ˇ
2847 "});
2848
2849 cx.set_state(indoc! {"
2850 a
2851 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2852 "});
2853 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2854 cx.assert_editor_state(indoc! {"
2855 a
2856 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2857 "});
2858}
2859
2860#[gpui::test]
2861async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2862 init_test(cx, |_| {});
2863
2864 let mut cx = EditorTestContext::new(cx).await;
2865 let language = Arc::new(
2866 Language::new(
2867 LanguageConfig::default(),
2868 Some(tree_sitter_rust::LANGUAGE.into()),
2869 )
2870 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2871 .unwrap(),
2872 );
2873 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2874
2875 // cursors that are already at the suggested indent level insert
2876 // a soft tab. cursors that are to the left of the suggested indent
2877 // auto-indent their line.
2878 cx.set_state(indoc! {"
2879 ˇ
2880 const a: B = (
2881 c(
2882 d(
2883 ˇ
2884 )
2885 ˇ
2886 ˇ )
2887 );
2888 "});
2889 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2890 cx.assert_editor_state(indoc! {"
2891 ˇ
2892 const a: B = (
2893 c(
2894 d(
2895 ˇ
2896 )
2897 ˇ
2898 ˇ)
2899 );
2900 "});
2901
2902 // handle auto-indent when there are multiple cursors on the same line
2903 cx.set_state(indoc! {"
2904 const a: B = (
2905 c(
2906 ˇ ˇ
2907 ˇ )
2908 );
2909 "});
2910 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912 const a: B = (
2913 c(
2914 ˇ
2915 ˇ)
2916 );
2917 "});
2918}
2919
2920#[gpui::test]
2921async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
2922 init_test(cx, |settings| {
2923 settings.defaults.tab_size = NonZeroU32::new(3)
2924 });
2925
2926 let mut cx = EditorTestContext::new(cx).await;
2927 cx.set_state(indoc! {"
2928 ˇ
2929 \t ˇ
2930 \t ˇ
2931 \t ˇ
2932 \t \t\t \t \t\t \t\t \t \t ˇ
2933 "});
2934
2935 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2936 cx.assert_editor_state(indoc! {"
2937 ˇ
2938 \t ˇ
2939 \t ˇ
2940 \t ˇ
2941 \t \t\t \t \t\t \t\t \t \t ˇ
2942 "});
2943}
2944
2945#[gpui::test]
2946async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
2947 init_test(cx, |settings| {
2948 settings.defaults.tab_size = NonZeroU32::new(4)
2949 });
2950
2951 let language = Arc::new(
2952 Language::new(
2953 LanguageConfig::default(),
2954 Some(tree_sitter_rust::LANGUAGE.into()),
2955 )
2956 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
2957 .unwrap(),
2958 );
2959
2960 let mut cx = EditorTestContext::new(cx).await;
2961 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2962 cx.set_state(indoc! {"
2963 fn a() {
2964 if b {
2965 \t ˇc
2966 }
2967 }
2968 "});
2969
2970 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2971 cx.assert_editor_state(indoc! {"
2972 fn a() {
2973 if b {
2974 ˇc
2975 }
2976 }
2977 "});
2978}
2979
2980#[gpui::test]
2981async fn test_indent_outdent(cx: &mut TestAppContext) {
2982 init_test(cx, |settings| {
2983 settings.defaults.tab_size = NonZeroU32::new(4);
2984 });
2985
2986 let mut cx = EditorTestContext::new(cx).await;
2987
2988 cx.set_state(indoc! {"
2989 «oneˇ» «twoˇ»
2990 three
2991 four
2992 "});
2993 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2994 cx.assert_editor_state(indoc! {"
2995 «oneˇ» «twoˇ»
2996 three
2997 four
2998 "});
2999
3000 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3001 cx.assert_editor_state(indoc! {"
3002 «oneˇ» «twoˇ»
3003 three
3004 four
3005 "});
3006
3007 // select across line ending
3008 cx.set_state(indoc! {"
3009 one two
3010 t«hree
3011 ˇ» four
3012 "});
3013 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3014 cx.assert_editor_state(indoc! {"
3015 one two
3016 t«hree
3017 ˇ» four
3018 "});
3019
3020 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3021 cx.assert_editor_state(indoc! {"
3022 one two
3023 t«hree
3024 ˇ» four
3025 "});
3026
3027 // Ensure that indenting/outdenting works when the cursor is at column 0.
3028 cx.set_state(indoc! {"
3029 one two
3030 ˇthree
3031 four
3032 "});
3033 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3034 cx.assert_editor_state(indoc! {"
3035 one two
3036 ˇthree
3037 four
3038 "});
3039
3040 cx.set_state(indoc! {"
3041 one two
3042 ˇ three
3043 four
3044 "});
3045 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3046 cx.assert_editor_state(indoc! {"
3047 one two
3048 ˇthree
3049 four
3050 "});
3051}
3052
3053#[gpui::test]
3054async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3055 init_test(cx, |settings| {
3056 settings.defaults.hard_tabs = Some(true);
3057 });
3058
3059 let mut cx = EditorTestContext::new(cx).await;
3060
3061 // select two ranges on one line
3062 cx.set_state(indoc! {"
3063 «oneˇ» «twoˇ»
3064 three
3065 four
3066 "});
3067 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3068 cx.assert_editor_state(indoc! {"
3069 \t«oneˇ» «twoˇ»
3070 three
3071 four
3072 "});
3073 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3074 cx.assert_editor_state(indoc! {"
3075 \t\t«oneˇ» «twoˇ»
3076 three
3077 four
3078 "});
3079 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3080 cx.assert_editor_state(indoc! {"
3081 \t«oneˇ» «twoˇ»
3082 three
3083 four
3084 "});
3085 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3086 cx.assert_editor_state(indoc! {"
3087 «oneˇ» «twoˇ»
3088 three
3089 four
3090 "});
3091
3092 // select across a line ending
3093 cx.set_state(indoc! {"
3094 one two
3095 t«hree
3096 ˇ»four
3097 "});
3098 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3099 cx.assert_editor_state(indoc! {"
3100 one two
3101 \tt«hree
3102 ˇ»four
3103 "});
3104 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3105 cx.assert_editor_state(indoc! {"
3106 one two
3107 \t\tt«hree
3108 ˇ»four
3109 "});
3110 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3111 cx.assert_editor_state(indoc! {"
3112 one two
3113 \tt«hree
3114 ˇ»four
3115 "});
3116 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3117 cx.assert_editor_state(indoc! {"
3118 one two
3119 t«hree
3120 ˇ»four
3121 "});
3122
3123 // Ensure that indenting/outdenting works when the cursor is at column 0.
3124 cx.set_state(indoc! {"
3125 one two
3126 ˇthree
3127 four
3128 "});
3129 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3130 cx.assert_editor_state(indoc! {"
3131 one two
3132 ˇthree
3133 four
3134 "});
3135 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3136 cx.assert_editor_state(indoc! {"
3137 one two
3138 \tˇthree
3139 four
3140 "});
3141 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3142 cx.assert_editor_state(indoc! {"
3143 one two
3144 ˇthree
3145 four
3146 "});
3147}
3148
3149#[gpui::test]
3150fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3151 init_test(cx, |settings| {
3152 settings.languages.extend([
3153 (
3154 "TOML".into(),
3155 LanguageSettingsContent {
3156 tab_size: NonZeroU32::new(2),
3157 ..Default::default()
3158 },
3159 ),
3160 (
3161 "Rust".into(),
3162 LanguageSettingsContent {
3163 tab_size: NonZeroU32::new(4),
3164 ..Default::default()
3165 },
3166 ),
3167 ]);
3168 });
3169
3170 let toml_language = Arc::new(Language::new(
3171 LanguageConfig {
3172 name: "TOML".into(),
3173 ..Default::default()
3174 },
3175 None,
3176 ));
3177 let rust_language = Arc::new(Language::new(
3178 LanguageConfig {
3179 name: "Rust".into(),
3180 ..Default::default()
3181 },
3182 None,
3183 ));
3184
3185 let toml_buffer =
3186 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3187 let rust_buffer =
3188 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3189 let multibuffer = cx.new(|cx| {
3190 let mut multibuffer = MultiBuffer::new(ReadWrite);
3191 multibuffer.push_excerpts(
3192 toml_buffer.clone(),
3193 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3194 cx,
3195 );
3196 multibuffer.push_excerpts(
3197 rust_buffer.clone(),
3198 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3199 cx,
3200 );
3201 multibuffer
3202 });
3203
3204 cx.add_window(|window, cx| {
3205 let mut editor = build_editor(multibuffer, window, cx);
3206
3207 assert_eq!(
3208 editor.text(cx),
3209 indoc! {"
3210 a = 1
3211 b = 2
3212
3213 const c: usize = 3;
3214 "}
3215 );
3216
3217 select_ranges(
3218 &mut editor,
3219 indoc! {"
3220 «aˇ» = 1
3221 b = 2
3222
3223 «const c:ˇ» usize = 3;
3224 "},
3225 window,
3226 cx,
3227 );
3228
3229 editor.tab(&Tab, window, cx);
3230 assert_text_with_selections(
3231 &mut editor,
3232 indoc! {"
3233 «aˇ» = 1
3234 b = 2
3235
3236 «const c:ˇ» usize = 3;
3237 "},
3238 cx,
3239 );
3240 editor.backtab(&Backtab, window, cx);
3241 assert_text_with_selections(
3242 &mut editor,
3243 indoc! {"
3244 «aˇ» = 1
3245 b = 2
3246
3247 «const c:ˇ» usize = 3;
3248 "},
3249 cx,
3250 );
3251
3252 editor
3253 });
3254}
3255
3256#[gpui::test]
3257async fn test_backspace(cx: &mut TestAppContext) {
3258 init_test(cx, |_| {});
3259
3260 let mut cx = EditorTestContext::new(cx).await;
3261
3262 // Basic backspace
3263 cx.set_state(indoc! {"
3264 onˇe two three
3265 fou«rˇ» five six
3266 seven «ˇeight nine
3267 »ten
3268 "});
3269 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3270 cx.assert_editor_state(indoc! {"
3271 oˇe two three
3272 fouˇ five six
3273 seven ˇten
3274 "});
3275
3276 // Test backspace inside and around indents
3277 cx.set_state(indoc! {"
3278 zero
3279 ˇone
3280 ˇtwo
3281 ˇ ˇ ˇ three
3282 ˇ ˇ four
3283 "});
3284 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3285 cx.assert_editor_state(indoc! {"
3286 zero
3287 ˇone
3288 ˇtwo
3289 ˇ threeˇ four
3290 "});
3291}
3292
3293#[gpui::test]
3294async fn test_delete(cx: &mut TestAppContext) {
3295 init_test(cx, |_| {});
3296
3297 let mut cx = EditorTestContext::new(cx).await;
3298 cx.set_state(indoc! {"
3299 onˇe two three
3300 fou«rˇ» five six
3301 seven «ˇeight nine
3302 »ten
3303 "});
3304 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3305 cx.assert_editor_state(indoc! {"
3306 onˇ two three
3307 fouˇ five six
3308 seven ˇten
3309 "});
3310}
3311
3312#[gpui::test]
3313fn test_delete_line(cx: &mut TestAppContext) {
3314 init_test(cx, |_| {});
3315
3316 let editor = cx.add_window(|window, cx| {
3317 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3318 build_editor(buffer, window, cx)
3319 });
3320 _ = editor.update(cx, |editor, window, cx| {
3321 editor.change_selections(None, window, cx, |s| {
3322 s.select_display_ranges([
3323 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3324 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3325 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3326 ])
3327 });
3328 editor.delete_line(&DeleteLine, window, cx);
3329 assert_eq!(editor.display_text(cx), "ghi");
3330 assert_eq!(
3331 editor.selections.display_ranges(cx),
3332 vec![
3333 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3334 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3335 ]
3336 );
3337 });
3338
3339 let editor = cx.add_window(|window, cx| {
3340 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3341 build_editor(buffer, window, cx)
3342 });
3343 _ = editor.update(cx, |editor, window, cx| {
3344 editor.change_selections(None, window, cx, |s| {
3345 s.select_display_ranges([
3346 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3347 ])
3348 });
3349 editor.delete_line(&DeleteLine, window, cx);
3350 assert_eq!(editor.display_text(cx), "ghi\n");
3351 assert_eq!(
3352 editor.selections.display_ranges(cx),
3353 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3354 );
3355 });
3356}
3357
3358#[gpui::test]
3359fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3360 init_test(cx, |_| {});
3361
3362 cx.add_window(|window, cx| {
3363 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3364 let mut editor = build_editor(buffer.clone(), window, cx);
3365 let buffer = buffer.read(cx).as_singleton().unwrap();
3366
3367 assert_eq!(
3368 editor.selections.ranges::<Point>(cx),
3369 &[Point::new(0, 0)..Point::new(0, 0)]
3370 );
3371
3372 // When on single line, replace newline at end by space
3373 editor.join_lines(&JoinLines, window, cx);
3374 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3375 assert_eq!(
3376 editor.selections.ranges::<Point>(cx),
3377 &[Point::new(0, 3)..Point::new(0, 3)]
3378 );
3379
3380 // When multiple lines are selected, remove newlines that are spanned by the selection
3381 editor.change_selections(None, window, cx, |s| {
3382 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3383 });
3384 editor.join_lines(&JoinLines, window, cx);
3385 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3386 assert_eq!(
3387 editor.selections.ranges::<Point>(cx),
3388 &[Point::new(0, 11)..Point::new(0, 11)]
3389 );
3390
3391 // Undo should be transactional
3392 editor.undo(&Undo, window, cx);
3393 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3394 assert_eq!(
3395 editor.selections.ranges::<Point>(cx),
3396 &[Point::new(0, 5)..Point::new(2, 2)]
3397 );
3398
3399 // When joining an empty line don't insert a space
3400 editor.change_selections(None, window, cx, |s| {
3401 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3402 });
3403 editor.join_lines(&JoinLines, window, cx);
3404 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3405 assert_eq!(
3406 editor.selections.ranges::<Point>(cx),
3407 [Point::new(2, 3)..Point::new(2, 3)]
3408 );
3409
3410 // We can remove trailing newlines
3411 editor.join_lines(&JoinLines, window, cx);
3412 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3413 assert_eq!(
3414 editor.selections.ranges::<Point>(cx),
3415 [Point::new(2, 3)..Point::new(2, 3)]
3416 );
3417
3418 // We don't blow up on the last line
3419 editor.join_lines(&JoinLines, window, cx);
3420 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3421 assert_eq!(
3422 editor.selections.ranges::<Point>(cx),
3423 [Point::new(2, 3)..Point::new(2, 3)]
3424 );
3425
3426 // reset to test indentation
3427 editor.buffer.update(cx, |buffer, cx| {
3428 buffer.edit(
3429 [
3430 (Point::new(1, 0)..Point::new(1, 2), " "),
3431 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3432 ],
3433 None,
3434 cx,
3435 )
3436 });
3437
3438 // We remove any leading spaces
3439 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3440 editor.change_selections(None, window, cx, |s| {
3441 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3442 });
3443 editor.join_lines(&JoinLines, window, cx);
3444 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3445
3446 // We don't insert a space for a line containing only spaces
3447 editor.join_lines(&JoinLines, window, cx);
3448 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3449
3450 // We ignore any leading tabs
3451 editor.join_lines(&JoinLines, window, cx);
3452 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3453
3454 editor
3455 });
3456}
3457
3458#[gpui::test]
3459fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3460 init_test(cx, |_| {});
3461
3462 cx.add_window(|window, cx| {
3463 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3464 let mut editor = build_editor(buffer.clone(), window, cx);
3465 let buffer = buffer.read(cx).as_singleton().unwrap();
3466
3467 editor.change_selections(None, window, cx, |s| {
3468 s.select_ranges([
3469 Point::new(0, 2)..Point::new(1, 1),
3470 Point::new(1, 2)..Point::new(1, 2),
3471 Point::new(3, 1)..Point::new(3, 2),
3472 ])
3473 });
3474
3475 editor.join_lines(&JoinLines, window, cx);
3476 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3477
3478 assert_eq!(
3479 editor.selections.ranges::<Point>(cx),
3480 [
3481 Point::new(0, 7)..Point::new(0, 7),
3482 Point::new(1, 3)..Point::new(1, 3)
3483 ]
3484 );
3485 editor
3486 });
3487}
3488
3489#[gpui::test]
3490async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3491 init_test(cx, |_| {});
3492
3493 let mut cx = EditorTestContext::new(cx).await;
3494
3495 let diff_base = r#"
3496 Line 0
3497 Line 1
3498 Line 2
3499 Line 3
3500 "#
3501 .unindent();
3502
3503 cx.set_state(
3504 &r#"
3505 ˇLine 0
3506 Line 1
3507 Line 2
3508 Line 3
3509 "#
3510 .unindent(),
3511 );
3512
3513 cx.set_head_text(&diff_base);
3514 executor.run_until_parked();
3515
3516 // Join lines
3517 cx.update_editor(|editor, window, cx| {
3518 editor.join_lines(&JoinLines, window, cx);
3519 });
3520 executor.run_until_parked();
3521
3522 cx.assert_editor_state(
3523 &r#"
3524 Line 0ˇ Line 1
3525 Line 2
3526 Line 3
3527 "#
3528 .unindent(),
3529 );
3530 // Join again
3531 cx.update_editor(|editor, window, cx| {
3532 editor.join_lines(&JoinLines, window, cx);
3533 });
3534 executor.run_until_parked();
3535
3536 cx.assert_editor_state(
3537 &r#"
3538 Line 0 Line 1ˇ Line 2
3539 Line 3
3540 "#
3541 .unindent(),
3542 );
3543}
3544
3545#[gpui::test]
3546async fn test_custom_newlines_cause_no_false_positive_diffs(
3547 executor: BackgroundExecutor,
3548 cx: &mut TestAppContext,
3549) {
3550 init_test(cx, |_| {});
3551 let mut cx = EditorTestContext::new(cx).await;
3552 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3553 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3554 executor.run_until_parked();
3555
3556 cx.update_editor(|editor, window, cx| {
3557 let snapshot = editor.snapshot(window, cx);
3558 assert_eq!(
3559 snapshot
3560 .buffer_snapshot
3561 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3562 .collect::<Vec<_>>(),
3563 Vec::new(),
3564 "Should not have any diffs for files with custom newlines"
3565 );
3566 });
3567}
3568
3569#[gpui::test]
3570async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3571 init_test(cx, |_| {});
3572
3573 let mut cx = EditorTestContext::new(cx).await;
3574
3575 // Test sort_lines_case_insensitive()
3576 cx.set_state(indoc! {"
3577 «z
3578 y
3579 x
3580 Z
3581 Y
3582 Xˇ»
3583 "});
3584 cx.update_editor(|e, window, cx| {
3585 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3586 });
3587 cx.assert_editor_state(indoc! {"
3588 «x
3589 X
3590 y
3591 Y
3592 z
3593 Zˇ»
3594 "});
3595
3596 // Test reverse_lines()
3597 cx.set_state(indoc! {"
3598 «5
3599 4
3600 3
3601 2
3602 1ˇ»
3603 "});
3604 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3605 cx.assert_editor_state(indoc! {"
3606 «1
3607 2
3608 3
3609 4
3610 5ˇ»
3611 "});
3612
3613 // Skip testing shuffle_line()
3614
3615 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3616 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3617
3618 // Don't manipulate when cursor is on single line, but expand the selection
3619 cx.set_state(indoc! {"
3620 ddˇdd
3621 ccc
3622 bb
3623 a
3624 "});
3625 cx.update_editor(|e, window, cx| {
3626 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3627 });
3628 cx.assert_editor_state(indoc! {"
3629 «ddddˇ»
3630 ccc
3631 bb
3632 a
3633 "});
3634
3635 // Basic manipulate case
3636 // Start selection moves to column 0
3637 // End of selection shrinks to fit shorter line
3638 cx.set_state(indoc! {"
3639 dd«d
3640 ccc
3641 bb
3642 aaaaaˇ»
3643 "});
3644 cx.update_editor(|e, window, cx| {
3645 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3646 });
3647 cx.assert_editor_state(indoc! {"
3648 «aaaaa
3649 bb
3650 ccc
3651 dddˇ»
3652 "});
3653
3654 // Manipulate case with newlines
3655 cx.set_state(indoc! {"
3656 dd«d
3657 ccc
3658
3659 bb
3660 aaaaa
3661
3662 ˇ»
3663 "});
3664 cx.update_editor(|e, window, cx| {
3665 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3666 });
3667 cx.assert_editor_state(indoc! {"
3668 «
3669
3670 aaaaa
3671 bb
3672 ccc
3673 dddˇ»
3674
3675 "});
3676
3677 // Adding new line
3678 cx.set_state(indoc! {"
3679 aa«a
3680 bbˇ»b
3681 "});
3682 cx.update_editor(|e, window, cx| {
3683 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3684 });
3685 cx.assert_editor_state(indoc! {"
3686 «aaa
3687 bbb
3688 added_lineˇ»
3689 "});
3690
3691 // Removing line
3692 cx.set_state(indoc! {"
3693 aa«a
3694 bbbˇ»
3695 "});
3696 cx.update_editor(|e, window, cx| {
3697 e.manipulate_lines(window, cx, |lines| {
3698 lines.pop();
3699 })
3700 });
3701 cx.assert_editor_state(indoc! {"
3702 «aaaˇ»
3703 "});
3704
3705 // Removing all lines
3706 cx.set_state(indoc! {"
3707 aa«a
3708 bbbˇ»
3709 "});
3710 cx.update_editor(|e, window, cx| {
3711 e.manipulate_lines(window, cx, |lines| {
3712 lines.drain(..);
3713 })
3714 });
3715 cx.assert_editor_state(indoc! {"
3716 ˇ
3717 "});
3718}
3719
3720#[gpui::test]
3721async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3722 init_test(cx, |_| {});
3723
3724 let mut cx = EditorTestContext::new(cx).await;
3725
3726 // Consider continuous selection as single selection
3727 cx.set_state(indoc! {"
3728 Aaa«aa
3729 cˇ»c«c
3730 bb
3731 aaaˇ»aa
3732 "});
3733 cx.update_editor(|e, window, cx| {
3734 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3735 });
3736 cx.assert_editor_state(indoc! {"
3737 «Aaaaa
3738 ccc
3739 bb
3740 aaaaaˇ»
3741 "});
3742
3743 cx.set_state(indoc! {"
3744 Aaa«aa
3745 cˇ»c«c
3746 bb
3747 aaaˇ»aa
3748 "});
3749 cx.update_editor(|e, window, cx| {
3750 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3751 });
3752 cx.assert_editor_state(indoc! {"
3753 «Aaaaa
3754 ccc
3755 bbˇ»
3756 "});
3757
3758 // Consider non continuous selection as distinct dedup operations
3759 cx.set_state(indoc! {"
3760 «aaaaa
3761 bb
3762 aaaaa
3763 aaaaaˇ»
3764
3765 aaa«aaˇ»
3766 "});
3767 cx.update_editor(|e, window, cx| {
3768 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3769 });
3770 cx.assert_editor_state(indoc! {"
3771 «aaaaa
3772 bbˇ»
3773
3774 «aaaaaˇ»
3775 "});
3776}
3777
3778#[gpui::test]
3779async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3780 init_test(cx, |_| {});
3781
3782 let mut cx = EditorTestContext::new(cx).await;
3783
3784 cx.set_state(indoc! {"
3785 «Aaa
3786 aAa
3787 Aaaˇ»
3788 "});
3789 cx.update_editor(|e, window, cx| {
3790 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3791 });
3792 cx.assert_editor_state(indoc! {"
3793 «Aaa
3794 aAaˇ»
3795 "});
3796
3797 cx.set_state(indoc! {"
3798 «Aaa
3799 aAa
3800 aaAˇ»
3801 "});
3802 cx.update_editor(|e, window, cx| {
3803 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3804 });
3805 cx.assert_editor_state(indoc! {"
3806 «Aaaˇ»
3807 "});
3808}
3809
3810#[gpui::test]
3811async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3812 init_test(cx, |_| {});
3813
3814 let mut cx = EditorTestContext::new(cx).await;
3815
3816 // Manipulate with multiple selections on a single line
3817 cx.set_state(indoc! {"
3818 dd«dd
3819 cˇ»c«c
3820 bb
3821 aaaˇ»aa
3822 "});
3823 cx.update_editor(|e, window, cx| {
3824 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3825 });
3826 cx.assert_editor_state(indoc! {"
3827 «aaaaa
3828 bb
3829 ccc
3830 ddddˇ»
3831 "});
3832
3833 // Manipulate with multiple disjoin selections
3834 cx.set_state(indoc! {"
3835 5«
3836 4
3837 3
3838 2
3839 1ˇ»
3840
3841 dd«dd
3842 ccc
3843 bb
3844 aaaˇ»aa
3845 "});
3846 cx.update_editor(|e, window, cx| {
3847 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3848 });
3849 cx.assert_editor_state(indoc! {"
3850 «1
3851 2
3852 3
3853 4
3854 5ˇ»
3855
3856 «aaaaa
3857 bb
3858 ccc
3859 ddddˇ»
3860 "});
3861
3862 // Adding lines on each selection
3863 cx.set_state(indoc! {"
3864 2«
3865 1ˇ»
3866
3867 bb«bb
3868 aaaˇ»aa
3869 "});
3870 cx.update_editor(|e, window, cx| {
3871 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
3872 });
3873 cx.assert_editor_state(indoc! {"
3874 «2
3875 1
3876 added lineˇ»
3877
3878 «bbbb
3879 aaaaa
3880 added lineˇ»
3881 "});
3882
3883 // Removing lines on each selection
3884 cx.set_state(indoc! {"
3885 2«
3886 1ˇ»
3887
3888 bb«bb
3889 aaaˇ»aa
3890 "});
3891 cx.update_editor(|e, window, cx| {
3892 e.manipulate_lines(window, cx, |lines| {
3893 lines.pop();
3894 })
3895 });
3896 cx.assert_editor_state(indoc! {"
3897 «2ˇ»
3898
3899 «bbbbˇ»
3900 "});
3901}
3902
3903#[gpui::test]
3904async fn test_toggle_case(cx: &mut TestAppContext) {
3905 init_test(cx, |_| {});
3906
3907 let mut cx = EditorTestContext::new(cx).await;
3908
3909 // If all lower case -> upper case
3910 cx.set_state(indoc! {"
3911 «hello worldˇ»
3912 "});
3913 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3914 cx.assert_editor_state(indoc! {"
3915 «HELLO WORLDˇ»
3916 "});
3917
3918 // If all upper case -> lower case
3919 cx.set_state(indoc! {"
3920 «HELLO WORLDˇ»
3921 "});
3922 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3923 cx.assert_editor_state(indoc! {"
3924 «hello worldˇ»
3925 "});
3926
3927 // If any upper case characters are identified -> lower case
3928 // This matches JetBrains IDEs
3929 cx.set_state(indoc! {"
3930 «hEllo worldˇ»
3931 "});
3932 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
3933 cx.assert_editor_state(indoc! {"
3934 «hello worldˇ»
3935 "});
3936}
3937
3938#[gpui::test]
3939async fn test_manipulate_text(cx: &mut TestAppContext) {
3940 init_test(cx, |_| {});
3941
3942 let mut cx = EditorTestContext::new(cx).await;
3943
3944 // Test convert_to_upper_case()
3945 cx.set_state(indoc! {"
3946 «hello worldˇ»
3947 "});
3948 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3949 cx.assert_editor_state(indoc! {"
3950 «HELLO WORLDˇ»
3951 "});
3952
3953 // Test convert_to_lower_case()
3954 cx.set_state(indoc! {"
3955 «HELLO WORLDˇ»
3956 "});
3957 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
3958 cx.assert_editor_state(indoc! {"
3959 «hello worldˇ»
3960 "});
3961
3962 // Test multiple line, single selection case
3963 cx.set_state(indoc! {"
3964 «The quick brown
3965 fox jumps over
3966 the lazy dogˇ»
3967 "});
3968 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
3969 cx.assert_editor_state(indoc! {"
3970 «The Quick Brown
3971 Fox Jumps Over
3972 The Lazy Dogˇ»
3973 "});
3974
3975 // Test multiple line, single selection case
3976 cx.set_state(indoc! {"
3977 «The quick brown
3978 fox jumps over
3979 the lazy dogˇ»
3980 "});
3981 cx.update_editor(|e, window, cx| {
3982 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
3983 });
3984 cx.assert_editor_state(indoc! {"
3985 «TheQuickBrown
3986 FoxJumpsOver
3987 TheLazyDogˇ»
3988 "});
3989
3990 // From here on out, test more complex cases of manipulate_text()
3991
3992 // Test no selection case - should affect words cursors are in
3993 // Cursor at beginning, middle, and end of word
3994 cx.set_state(indoc! {"
3995 ˇhello big beauˇtiful worldˇ
3996 "});
3997 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
3998 cx.assert_editor_state(indoc! {"
3999 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4000 "});
4001
4002 // Test multiple selections on a single line and across multiple lines
4003 cx.set_state(indoc! {"
4004 «Theˇ» quick «brown
4005 foxˇ» jumps «overˇ»
4006 the «lazyˇ» dog
4007 "});
4008 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4009 cx.assert_editor_state(indoc! {"
4010 «THEˇ» quick «BROWN
4011 FOXˇ» jumps «OVERˇ»
4012 the «LAZYˇ» dog
4013 "});
4014
4015 // Test case where text length grows
4016 cx.set_state(indoc! {"
4017 «tschüߡ»
4018 "});
4019 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4020 cx.assert_editor_state(indoc! {"
4021 «TSCHÜSSˇ»
4022 "});
4023
4024 // Test to make sure we don't crash when text shrinks
4025 cx.set_state(indoc! {"
4026 aaa_bbbˇ
4027 "});
4028 cx.update_editor(|e, window, cx| {
4029 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4030 });
4031 cx.assert_editor_state(indoc! {"
4032 «aaaBbbˇ»
4033 "});
4034
4035 // Test to make sure we all aware of the fact that each word can grow and shrink
4036 // Final selections should be aware of this fact
4037 cx.set_state(indoc! {"
4038 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4039 "});
4040 cx.update_editor(|e, window, cx| {
4041 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4042 });
4043 cx.assert_editor_state(indoc! {"
4044 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4045 "});
4046
4047 cx.set_state(indoc! {"
4048 «hElLo, WoRld!ˇ»
4049 "});
4050 cx.update_editor(|e, window, cx| {
4051 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4052 });
4053 cx.assert_editor_state(indoc! {"
4054 «HeLlO, wOrLD!ˇ»
4055 "});
4056}
4057
4058#[gpui::test]
4059fn test_duplicate_line(cx: &mut TestAppContext) {
4060 init_test(cx, |_| {});
4061
4062 let editor = cx.add_window(|window, cx| {
4063 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4064 build_editor(buffer, window, cx)
4065 });
4066 _ = editor.update(cx, |editor, window, cx| {
4067 editor.change_selections(None, window, cx, |s| {
4068 s.select_display_ranges([
4069 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4070 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4071 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4072 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4073 ])
4074 });
4075 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4076 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4077 assert_eq!(
4078 editor.selections.display_ranges(cx),
4079 vec![
4080 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4081 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4082 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4083 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4084 ]
4085 );
4086 });
4087
4088 let editor = cx.add_window(|window, cx| {
4089 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4090 build_editor(buffer, window, cx)
4091 });
4092 _ = editor.update(cx, |editor, window, cx| {
4093 editor.change_selections(None, window, cx, |s| {
4094 s.select_display_ranges([
4095 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4096 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4097 ])
4098 });
4099 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4100 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4101 assert_eq!(
4102 editor.selections.display_ranges(cx),
4103 vec![
4104 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4105 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4106 ]
4107 );
4108 });
4109
4110 // With `move_upwards` the selections stay in place, except for
4111 // the lines inserted above them
4112 let editor = cx.add_window(|window, cx| {
4113 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4114 build_editor(buffer, window, cx)
4115 });
4116 _ = editor.update(cx, |editor, window, cx| {
4117 editor.change_selections(None, window, cx, |s| {
4118 s.select_display_ranges([
4119 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4120 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4121 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4122 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4123 ])
4124 });
4125 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4126 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4127 assert_eq!(
4128 editor.selections.display_ranges(cx),
4129 vec![
4130 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4131 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4132 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4133 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4134 ]
4135 );
4136 });
4137
4138 let editor = cx.add_window(|window, cx| {
4139 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4140 build_editor(buffer, window, cx)
4141 });
4142 _ = editor.update(cx, |editor, window, cx| {
4143 editor.change_selections(None, window, cx, |s| {
4144 s.select_display_ranges([
4145 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4146 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4147 ])
4148 });
4149 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4150 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4151 assert_eq!(
4152 editor.selections.display_ranges(cx),
4153 vec![
4154 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4155 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4156 ]
4157 );
4158 });
4159
4160 let editor = cx.add_window(|window, cx| {
4161 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4162 build_editor(buffer, window, cx)
4163 });
4164 _ = editor.update(cx, |editor, window, cx| {
4165 editor.change_selections(None, window, cx, |s| {
4166 s.select_display_ranges([
4167 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4168 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4169 ])
4170 });
4171 editor.duplicate_selection(&DuplicateSelection, window, cx);
4172 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4173 assert_eq!(
4174 editor.selections.display_ranges(cx),
4175 vec![
4176 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4177 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4178 ]
4179 );
4180 });
4181}
4182
4183#[gpui::test]
4184fn test_move_line_up_down(cx: &mut TestAppContext) {
4185 init_test(cx, |_| {});
4186
4187 let editor = cx.add_window(|window, cx| {
4188 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4189 build_editor(buffer, window, cx)
4190 });
4191 _ = editor.update(cx, |editor, window, cx| {
4192 editor.fold_creases(
4193 vec![
4194 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4195 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4196 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4197 ],
4198 true,
4199 window,
4200 cx,
4201 );
4202 editor.change_selections(None, window, cx, |s| {
4203 s.select_display_ranges([
4204 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4205 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4206 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4207 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4208 ])
4209 });
4210 assert_eq!(
4211 editor.display_text(cx),
4212 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4213 );
4214
4215 editor.move_line_up(&MoveLineUp, window, cx);
4216 assert_eq!(
4217 editor.display_text(cx),
4218 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4219 );
4220 assert_eq!(
4221 editor.selections.display_ranges(cx),
4222 vec![
4223 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4224 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4225 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4226 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4227 ]
4228 );
4229 });
4230
4231 _ = editor.update(cx, |editor, window, cx| {
4232 editor.move_line_down(&MoveLineDown, window, cx);
4233 assert_eq!(
4234 editor.display_text(cx),
4235 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4236 );
4237 assert_eq!(
4238 editor.selections.display_ranges(cx),
4239 vec![
4240 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4241 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4242 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4243 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4244 ]
4245 );
4246 });
4247
4248 _ = editor.update(cx, |editor, window, cx| {
4249 editor.move_line_down(&MoveLineDown, window, cx);
4250 assert_eq!(
4251 editor.display_text(cx),
4252 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4253 );
4254 assert_eq!(
4255 editor.selections.display_ranges(cx),
4256 vec![
4257 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4258 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4259 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4260 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4261 ]
4262 );
4263 });
4264
4265 _ = editor.update(cx, |editor, window, cx| {
4266 editor.move_line_up(&MoveLineUp, window, cx);
4267 assert_eq!(
4268 editor.display_text(cx),
4269 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4270 );
4271 assert_eq!(
4272 editor.selections.display_ranges(cx),
4273 vec![
4274 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4275 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4276 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4277 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4278 ]
4279 );
4280 });
4281}
4282
4283#[gpui::test]
4284fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4285 init_test(cx, |_| {});
4286
4287 let editor = cx.add_window(|window, cx| {
4288 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4289 build_editor(buffer, window, cx)
4290 });
4291 _ = editor.update(cx, |editor, window, cx| {
4292 let snapshot = editor.buffer.read(cx).snapshot(cx);
4293 editor.insert_blocks(
4294 [BlockProperties {
4295 style: BlockStyle::Fixed,
4296 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4297 height: Some(1),
4298 render: Arc::new(|_| div().into_any()),
4299 priority: 0,
4300 }],
4301 Some(Autoscroll::fit()),
4302 cx,
4303 );
4304 editor.change_selections(None, window, cx, |s| {
4305 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4306 });
4307 editor.move_line_down(&MoveLineDown, window, cx);
4308 });
4309}
4310
4311#[gpui::test]
4312async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4313 init_test(cx, |_| {});
4314
4315 let mut cx = EditorTestContext::new(cx).await;
4316 cx.set_state(
4317 &"
4318 ˇzero
4319 one
4320 two
4321 three
4322 four
4323 five
4324 "
4325 .unindent(),
4326 );
4327
4328 // Create a four-line block that replaces three lines of text.
4329 cx.update_editor(|editor, window, cx| {
4330 let snapshot = editor.snapshot(window, cx);
4331 let snapshot = &snapshot.buffer_snapshot;
4332 let placement = BlockPlacement::Replace(
4333 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4334 );
4335 editor.insert_blocks(
4336 [BlockProperties {
4337 placement,
4338 height: Some(4),
4339 style: BlockStyle::Sticky,
4340 render: Arc::new(|_| gpui::div().into_any_element()),
4341 priority: 0,
4342 }],
4343 None,
4344 cx,
4345 );
4346 });
4347
4348 // Move down so that the cursor touches the block.
4349 cx.update_editor(|editor, window, cx| {
4350 editor.move_down(&Default::default(), window, cx);
4351 });
4352 cx.assert_editor_state(
4353 &"
4354 zero
4355 «one
4356 two
4357 threeˇ»
4358 four
4359 five
4360 "
4361 .unindent(),
4362 );
4363
4364 // Move down past the block.
4365 cx.update_editor(|editor, window, cx| {
4366 editor.move_down(&Default::default(), window, cx);
4367 });
4368 cx.assert_editor_state(
4369 &"
4370 zero
4371 one
4372 two
4373 three
4374 ˇfour
4375 five
4376 "
4377 .unindent(),
4378 );
4379}
4380
4381#[gpui::test]
4382fn test_transpose(cx: &mut TestAppContext) {
4383 init_test(cx, |_| {});
4384
4385 _ = cx.add_window(|window, cx| {
4386 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4387 editor.set_style(EditorStyle::default(), window, cx);
4388 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4389 editor.transpose(&Default::default(), window, cx);
4390 assert_eq!(editor.text(cx), "bac");
4391 assert_eq!(editor.selections.ranges(cx), [2..2]);
4392
4393 editor.transpose(&Default::default(), window, cx);
4394 assert_eq!(editor.text(cx), "bca");
4395 assert_eq!(editor.selections.ranges(cx), [3..3]);
4396
4397 editor.transpose(&Default::default(), window, cx);
4398 assert_eq!(editor.text(cx), "bac");
4399 assert_eq!(editor.selections.ranges(cx), [3..3]);
4400
4401 editor
4402 });
4403
4404 _ = cx.add_window(|window, cx| {
4405 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4406 editor.set_style(EditorStyle::default(), window, cx);
4407 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4408 editor.transpose(&Default::default(), window, cx);
4409 assert_eq!(editor.text(cx), "acb\nde");
4410 assert_eq!(editor.selections.ranges(cx), [3..3]);
4411
4412 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4413 editor.transpose(&Default::default(), window, cx);
4414 assert_eq!(editor.text(cx), "acbd\ne");
4415 assert_eq!(editor.selections.ranges(cx), [5..5]);
4416
4417 editor.transpose(&Default::default(), window, cx);
4418 assert_eq!(editor.text(cx), "acbde\n");
4419 assert_eq!(editor.selections.ranges(cx), [6..6]);
4420
4421 editor.transpose(&Default::default(), window, cx);
4422 assert_eq!(editor.text(cx), "acbd\ne");
4423 assert_eq!(editor.selections.ranges(cx), [6..6]);
4424
4425 editor
4426 });
4427
4428 _ = cx.add_window(|window, cx| {
4429 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4430 editor.set_style(EditorStyle::default(), window, cx);
4431 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4432 editor.transpose(&Default::default(), window, cx);
4433 assert_eq!(editor.text(cx), "bacd\ne");
4434 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4435
4436 editor.transpose(&Default::default(), window, cx);
4437 assert_eq!(editor.text(cx), "bcade\n");
4438 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4439
4440 editor.transpose(&Default::default(), window, cx);
4441 assert_eq!(editor.text(cx), "bcda\ne");
4442 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4443
4444 editor.transpose(&Default::default(), window, cx);
4445 assert_eq!(editor.text(cx), "bcade\n");
4446 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4447
4448 editor.transpose(&Default::default(), window, cx);
4449 assert_eq!(editor.text(cx), "bcaed\n");
4450 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4451
4452 editor
4453 });
4454
4455 _ = cx.add_window(|window, cx| {
4456 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4457 editor.set_style(EditorStyle::default(), window, cx);
4458 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4459 editor.transpose(&Default::default(), window, cx);
4460 assert_eq!(editor.text(cx), "🏀🍐✋");
4461 assert_eq!(editor.selections.ranges(cx), [8..8]);
4462
4463 editor.transpose(&Default::default(), window, cx);
4464 assert_eq!(editor.text(cx), "🏀✋🍐");
4465 assert_eq!(editor.selections.ranges(cx), [11..11]);
4466
4467 editor.transpose(&Default::default(), window, cx);
4468 assert_eq!(editor.text(cx), "🏀🍐✋");
4469 assert_eq!(editor.selections.ranges(cx), [11..11]);
4470
4471 editor
4472 });
4473}
4474
4475#[gpui::test]
4476async fn test_rewrap(cx: &mut TestAppContext) {
4477 init_test(cx, |settings| {
4478 settings.languages.extend([
4479 (
4480 "Markdown".into(),
4481 LanguageSettingsContent {
4482 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4483 ..Default::default()
4484 },
4485 ),
4486 (
4487 "Plain Text".into(),
4488 LanguageSettingsContent {
4489 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4490 ..Default::default()
4491 },
4492 ),
4493 ])
4494 });
4495
4496 let mut cx = EditorTestContext::new(cx).await;
4497
4498 let language_with_c_comments = Arc::new(Language::new(
4499 LanguageConfig {
4500 line_comments: vec!["// ".into()],
4501 ..LanguageConfig::default()
4502 },
4503 None,
4504 ));
4505 let language_with_pound_comments = Arc::new(Language::new(
4506 LanguageConfig {
4507 line_comments: vec!["# ".into()],
4508 ..LanguageConfig::default()
4509 },
4510 None,
4511 ));
4512 let markdown_language = Arc::new(Language::new(
4513 LanguageConfig {
4514 name: "Markdown".into(),
4515 ..LanguageConfig::default()
4516 },
4517 None,
4518 ));
4519 let language_with_doc_comments = Arc::new(Language::new(
4520 LanguageConfig {
4521 line_comments: vec!["// ".into(), "/// ".into()],
4522 ..LanguageConfig::default()
4523 },
4524 Some(tree_sitter_rust::LANGUAGE.into()),
4525 ));
4526
4527 let plaintext_language = Arc::new(Language::new(
4528 LanguageConfig {
4529 name: "Plain Text".into(),
4530 ..LanguageConfig::default()
4531 },
4532 None,
4533 ));
4534
4535 assert_rewrap(
4536 indoc! {"
4537 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4538 "},
4539 indoc! {"
4540 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4541 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4542 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4543 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4544 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4545 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4546 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4547 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4548 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4549 // porttitor id. Aliquam id accumsan eros.
4550 "},
4551 language_with_c_comments.clone(),
4552 &mut cx,
4553 );
4554
4555 // Test that rewrapping works inside of a selection
4556 assert_rewrap(
4557 indoc! {"
4558 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4559 "},
4560 indoc! {"
4561 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4562 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4563 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4564 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4565 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4566 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4567 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4568 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4569 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4570 // porttitor id. Aliquam id accumsan eros.ˇ»
4571 "},
4572 language_with_c_comments.clone(),
4573 &mut cx,
4574 );
4575
4576 // Test that cursors that expand to the same region are collapsed.
4577 assert_rewrap(
4578 indoc! {"
4579 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4580 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4581 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4582 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4583 "},
4584 indoc! {"
4585 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4586 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4587 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4588 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4589 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4590 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4591 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4592 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4593 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4594 // porttitor id. Aliquam id accumsan eros.
4595 "},
4596 language_with_c_comments.clone(),
4597 &mut cx,
4598 );
4599
4600 // Test that non-contiguous selections are treated separately.
4601 assert_rewrap(
4602 indoc! {"
4603 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4604 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4605 //
4606 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4607 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4608 "},
4609 indoc! {"
4610 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4611 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4612 // auctor, eu lacinia sapien scelerisque.
4613 //
4614 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4615 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4616 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4617 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4618 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4619 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4620 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4621 "},
4622 language_with_c_comments.clone(),
4623 &mut cx,
4624 );
4625
4626 // Test that different comment prefixes are supported.
4627 assert_rewrap(
4628 indoc! {"
4629 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4630 "},
4631 indoc! {"
4632 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4633 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4634 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4635 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4636 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4637 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4638 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4639 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4640 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4641 # accumsan eros.
4642 "},
4643 language_with_pound_comments.clone(),
4644 &mut cx,
4645 );
4646
4647 // Test that rewrapping is ignored outside of comments in most languages.
4648 assert_rewrap(
4649 indoc! {"
4650 /// Adds two numbers.
4651 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4652 fn add(a: u32, b: u32) -> u32 {
4653 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4654 }
4655 "},
4656 indoc! {"
4657 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4658 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4659 fn add(a: u32, b: u32) -> u32 {
4660 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
4661 }
4662 "},
4663 language_with_doc_comments.clone(),
4664 &mut cx,
4665 );
4666
4667 // Test that rewrapping works in Markdown and Plain Text languages.
4668 assert_rewrap(
4669 indoc! {"
4670 # Hello
4671
4672 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4673 "},
4674 indoc! {"
4675 # Hello
4676
4677 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4678 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4679 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4680 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4681 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4682 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4683 Integer sit amet scelerisque nisi.
4684 "},
4685 markdown_language,
4686 &mut cx,
4687 );
4688
4689 assert_rewrap(
4690 indoc! {"
4691 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
4692 "},
4693 indoc! {"
4694 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4695 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4696 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4697 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4698 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4699 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4700 Integer sit amet scelerisque nisi.
4701 "},
4702 plaintext_language,
4703 &mut cx,
4704 );
4705
4706 // Test rewrapping unaligned comments in a selection.
4707 assert_rewrap(
4708 indoc! {"
4709 fn foo() {
4710 if true {
4711 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4712 // Praesent semper egestas tellus id dignissim.ˇ»
4713 do_something();
4714 } else {
4715 //
4716 }
4717 }
4718 "},
4719 indoc! {"
4720 fn foo() {
4721 if true {
4722 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4723 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4724 // egestas tellus id dignissim.ˇ»
4725 do_something();
4726 } else {
4727 //
4728 }
4729 }
4730 "},
4731 language_with_doc_comments.clone(),
4732 &mut cx,
4733 );
4734
4735 assert_rewrap(
4736 indoc! {"
4737 fn foo() {
4738 if true {
4739 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4740 // Praesent semper egestas tellus id dignissim.»
4741 do_something();
4742 } else {
4743 //
4744 }
4745
4746 }
4747 "},
4748 indoc! {"
4749 fn foo() {
4750 if true {
4751 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4752 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4753 // egestas tellus id dignissim.»
4754 do_something();
4755 } else {
4756 //
4757 }
4758
4759 }
4760 "},
4761 language_with_doc_comments.clone(),
4762 &mut cx,
4763 );
4764
4765 #[track_caller]
4766 fn assert_rewrap(
4767 unwrapped_text: &str,
4768 wrapped_text: &str,
4769 language: Arc<Language>,
4770 cx: &mut EditorTestContext,
4771 ) {
4772 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4773 cx.set_state(unwrapped_text);
4774 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4775 cx.assert_editor_state(wrapped_text);
4776 }
4777}
4778
4779#[gpui::test]
4780async fn test_hard_wrap(cx: &mut TestAppContext) {
4781 init_test(cx, |_| {});
4782 let mut cx = EditorTestContext::new(cx).await;
4783
4784 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4785 cx.update_editor(|editor, _, cx| {
4786 editor.set_hard_wrap(Some(14), cx);
4787 });
4788
4789 cx.set_state(indoc!(
4790 "
4791 one two three ˇ
4792 "
4793 ));
4794 cx.simulate_input("four");
4795 cx.run_until_parked();
4796
4797 cx.assert_editor_state(indoc!(
4798 "
4799 one two three
4800 fourˇ
4801 "
4802 ));
4803
4804 cx.update_editor(|editor, window, cx| {
4805 editor.newline(&Default::default(), window, cx);
4806 });
4807 cx.run_until_parked();
4808 cx.assert_editor_state(indoc!(
4809 "
4810 one two three
4811 four
4812 ˇ
4813 "
4814 ));
4815
4816 cx.simulate_input("five");
4817 cx.run_until_parked();
4818 cx.assert_editor_state(indoc!(
4819 "
4820 one two three
4821 four
4822 fiveˇ
4823 "
4824 ));
4825
4826 cx.update_editor(|editor, window, cx| {
4827 editor.newline(&Default::default(), window, cx);
4828 });
4829 cx.run_until_parked();
4830 cx.simulate_input("# ");
4831 cx.run_until_parked();
4832 cx.assert_editor_state(indoc!(
4833 "
4834 one two three
4835 four
4836 five
4837 # ˇ
4838 "
4839 ));
4840
4841 cx.update_editor(|editor, window, cx| {
4842 editor.newline(&Default::default(), window, cx);
4843 });
4844 cx.run_until_parked();
4845 cx.assert_editor_state(indoc!(
4846 "
4847 one two three
4848 four
4849 five
4850 #\x20
4851 #ˇ
4852 "
4853 ));
4854
4855 cx.simulate_input(" 6");
4856 cx.run_until_parked();
4857 cx.assert_editor_state(indoc!(
4858 "
4859 one two three
4860 four
4861 five
4862 #
4863 # 6ˇ
4864 "
4865 ));
4866}
4867
4868#[gpui::test]
4869async fn test_clipboard(cx: &mut TestAppContext) {
4870 init_test(cx, |_| {});
4871
4872 let mut cx = EditorTestContext::new(cx).await;
4873
4874 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
4875 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4876 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
4877
4878 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
4879 cx.set_state("two ˇfour ˇsix ˇ");
4880 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4881 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
4882
4883 // Paste again but with only two cursors. Since the number of cursors doesn't
4884 // match the number of slices in the clipboard, the entire clipboard text
4885 // is pasted at each cursor.
4886 cx.set_state("ˇtwo one✅ four three six five ˇ");
4887 cx.update_editor(|e, window, cx| {
4888 e.handle_input("( ", window, cx);
4889 e.paste(&Paste, window, cx);
4890 e.handle_input(") ", window, cx);
4891 });
4892 cx.assert_editor_state(
4893 &([
4894 "( one✅ ",
4895 "three ",
4896 "five ) ˇtwo one✅ four three six five ( one✅ ",
4897 "three ",
4898 "five ) ˇ",
4899 ]
4900 .join("\n")),
4901 );
4902
4903 // Cut with three selections, one of which is full-line.
4904 cx.set_state(indoc! {"
4905 1«2ˇ»3
4906 4ˇ567
4907 «8ˇ»9"});
4908 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
4909 cx.assert_editor_state(indoc! {"
4910 1ˇ3
4911 ˇ9"});
4912
4913 // Paste with three selections, noticing how the copied selection that was full-line
4914 // gets inserted before the second cursor.
4915 cx.set_state(indoc! {"
4916 1ˇ3
4917 9ˇ
4918 «oˇ»ne"});
4919 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4920 cx.assert_editor_state(indoc! {"
4921 12ˇ3
4922 4567
4923 9ˇ
4924 8ˇne"});
4925
4926 // Copy with a single cursor only, which writes the whole line into the clipboard.
4927 cx.set_state(indoc! {"
4928 The quick brown
4929 fox juˇmps over
4930 the lazy dog"});
4931 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4932 assert_eq!(
4933 cx.read_from_clipboard()
4934 .and_then(|item| item.text().as_deref().map(str::to_string)),
4935 Some("fox jumps over\n".to_string())
4936 );
4937
4938 // Paste with three selections, noticing how the copied full-line selection is inserted
4939 // before the empty selections but replaces the selection that is non-empty.
4940 cx.set_state(indoc! {"
4941 Tˇhe quick brown
4942 «foˇ»x jumps over
4943 tˇhe lazy dog"});
4944 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
4945 cx.assert_editor_state(indoc! {"
4946 fox jumps over
4947 Tˇhe quick brown
4948 fox jumps over
4949 ˇx jumps over
4950 fox jumps over
4951 tˇhe lazy dog"});
4952}
4953
4954#[gpui::test]
4955async fn test_copy_trim(cx: &mut TestAppContext) {
4956 init_test(cx, |_| {});
4957
4958 let mut cx = EditorTestContext::new(cx).await;
4959 cx.set_state(
4960 r#" «for selection in selections.iter() {
4961 let mut start = selection.start;
4962 let mut end = selection.end;
4963 let is_entire_line = selection.is_empty();
4964 if is_entire_line {
4965 start = Point::new(start.row, 0);ˇ»
4966 end = cmp::min(max_point, Point::new(end.row + 1, 0));
4967 }
4968 "#,
4969 );
4970 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
4971 assert_eq!(
4972 cx.read_from_clipboard()
4973 .and_then(|item| item.text().as_deref().map(str::to_string)),
4974 Some(
4975 "for selection in selections.iter() {
4976 let mut start = selection.start;
4977 let mut end = selection.end;
4978 let is_entire_line = selection.is_empty();
4979 if is_entire_line {
4980 start = Point::new(start.row, 0);"
4981 .to_string()
4982 ),
4983 "Regular copying preserves all indentation selected",
4984 );
4985 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
4986 assert_eq!(
4987 cx.read_from_clipboard()
4988 .and_then(|item| item.text().as_deref().map(str::to_string)),
4989 Some(
4990 "for selection in selections.iter() {
4991let mut start = selection.start;
4992let mut end = selection.end;
4993let is_entire_line = selection.is_empty();
4994if is_entire_line {
4995 start = Point::new(start.row, 0);"
4996 .to_string()
4997 ),
4998 "Copying with stripping should strip all leading whitespaces"
4999 );
5000
5001 cx.set_state(
5002 r#" « for selection in selections.iter() {
5003 let mut start = selection.start;
5004 let mut end = selection.end;
5005 let is_entire_line = selection.is_empty();
5006 if is_entire_line {
5007 start = Point::new(start.row, 0);ˇ»
5008 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5009 }
5010 "#,
5011 );
5012 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5013 assert_eq!(
5014 cx.read_from_clipboard()
5015 .and_then(|item| item.text().as_deref().map(str::to_string)),
5016 Some(
5017 " for selection in selections.iter() {
5018 let mut start = selection.start;
5019 let mut end = selection.end;
5020 let is_entire_line = selection.is_empty();
5021 if is_entire_line {
5022 start = Point::new(start.row, 0);"
5023 .to_string()
5024 ),
5025 "Regular copying preserves all indentation selected",
5026 );
5027 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5028 assert_eq!(
5029 cx.read_from_clipboard()
5030 .and_then(|item| item.text().as_deref().map(str::to_string)),
5031 Some(
5032 "for selection in selections.iter() {
5033let mut start = selection.start;
5034let mut end = selection.end;
5035let is_entire_line = selection.is_empty();
5036if is_entire_line {
5037 start = Point::new(start.row, 0);"
5038 .to_string()
5039 ),
5040 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5041 );
5042
5043 cx.set_state(
5044 r#" «ˇ for selection in selections.iter() {
5045 let mut start = selection.start;
5046 let mut end = selection.end;
5047 let is_entire_line = selection.is_empty();
5048 if is_entire_line {
5049 start = Point::new(start.row, 0);»
5050 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5051 }
5052 "#,
5053 );
5054 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5055 assert_eq!(
5056 cx.read_from_clipboard()
5057 .and_then(|item| item.text().as_deref().map(str::to_string)),
5058 Some(
5059 " for selection in selections.iter() {
5060 let mut start = selection.start;
5061 let mut end = selection.end;
5062 let is_entire_line = selection.is_empty();
5063 if is_entire_line {
5064 start = Point::new(start.row, 0);"
5065 .to_string()
5066 ),
5067 "Regular copying for reverse selection works the same",
5068 );
5069 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5070 assert_eq!(
5071 cx.read_from_clipboard()
5072 .and_then(|item| item.text().as_deref().map(str::to_string)),
5073 Some(
5074 "for selection in selections.iter() {
5075let mut start = selection.start;
5076let mut end = selection.end;
5077let is_entire_line = selection.is_empty();
5078if is_entire_line {
5079 start = Point::new(start.row, 0);"
5080 .to_string()
5081 ),
5082 "Copying with stripping for reverse selection works the same"
5083 );
5084
5085 cx.set_state(
5086 r#" for selection «in selections.iter() {
5087 let mut start = selection.start;
5088 let mut end = selection.end;
5089 let is_entire_line = selection.is_empty();
5090 if is_entire_line {
5091 start = Point::new(start.row, 0);ˇ»
5092 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5093 }
5094 "#,
5095 );
5096 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5097 assert_eq!(
5098 cx.read_from_clipboard()
5099 .and_then(|item| item.text().as_deref().map(str::to_string)),
5100 Some(
5101 "in selections.iter() {
5102 let mut start = selection.start;
5103 let mut end = selection.end;
5104 let is_entire_line = selection.is_empty();
5105 if is_entire_line {
5106 start = Point::new(start.row, 0);"
5107 .to_string()
5108 ),
5109 "When selecting past the indent, the copying works as usual",
5110 );
5111 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5112 assert_eq!(
5113 cx.read_from_clipboard()
5114 .and_then(|item| item.text().as_deref().map(str::to_string)),
5115 Some(
5116 "in selections.iter() {
5117 let mut start = selection.start;
5118 let mut end = selection.end;
5119 let is_entire_line = selection.is_empty();
5120 if is_entire_line {
5121 start = Point::new(start.row, 0);"
5122 .to_string()
5123 ),
5124 "When selecting past the indent, nothing is trimmed"
5125 );
5126}
5127
5128#[gpui::test]
5129async fn test_paste_multiline(cx: &mut TestAppContext) {
5130 init_test(cx, |_| {});
5131
5132 let mut cx = EditorTestContext::new(cx).await;
5133 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5134
5135 // Cut an indented block, without the leading whitespace.
5136 cx.set_state(indoc! {"
5137 const a: B = (
5138 c(),
5139 «d(
5140 e,
5141 f
5142 )ˇ»
5143 );
5144 "});
5145 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5146 cx.assert_editor_state(indoc! {"
5147 const a: B = (
5148 c(),
5149 ˇ
5150 );
5151 "});
5152
5153 // Paste it at the same position.
5154 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5155 cx.assert_editor_state(indoc! {"
5156 const a: B = (
5157 c(),
5158 d(
5159 e,
5160 f
5161 )ˇ
5162 );
5163 "});
5164
5165 // Paste it at a line with a lower indent level.
5166 cx.set_state(indoc! {"
5167 ˇ
5168 const a: B = (
5169 c(),
5170 );
5171 "});
5172 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5173 cx.assert_editor_state(indoc! {"
5174 d(
5175 e,
5176 f
5177 )ˇ
5178 const a: B = (
5179 c(),
5180 );
5181 "});
5182
5183 // Cut an indented block, with the leading whitespace.
5184 cx.set_state(indoc! {"
5185 const a: B = (
5186 c(),
5187 « d(
5188 e,
5189 f
5190 )
5191 ˇ»);
5192 "});
5193 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5194 cx.assert_editor_state(indoc! {"
5195 const a: B = (
5196 c(),
5197 ˇ);
5198 "});
5199
5200 // Paste it at the same position.
5201 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5202 cx.assert_editor_state(indoc! {"
5203 const a: B = (
5204 c(),
5205 d(
5206 e,
5207 f
5208 )
5209 ˇ);
5210 "});
5211
5212 // Paste it at a line with a higher indent level.
5213 cx.set_state(indoc! {"
5214 const a: B = (
5215 c(),
5216 d(
5217 e,
5218 fˇ
5219 )
5220 );
5221 "});
5222 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5223 cx.assert_editor_state(indoc! {"
5224 const a: B = (
5225 c(),
5226 d(
5227 e,
5228 f d(
5229 e,
5230 f
5231 )
5232 ˇ
5233 )
5234 );
5235 "});
5236
5237 // Copy an indented block, starting mid-line
5238 cx.set_state(indoc! {"
5239 const a: B = (
5240 c(),
5241 somethin«g(
5242 e,
5243 f
5244 )ˇ»
5245 );
5246 "});
5247 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5248
5249 // Paste it on a line with a lower indent level
5250 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5251 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5252 cx.assert_editor_state(indoc! {"
5253 const a: B = (
5254 c(),
5255 something(
5256 e,
5257 f
5258 )
5259 );
5260 g(
5261 e,
5262 f
5263 )ˇ"});
5264}
5265
5266#[gpui::test]
5267async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5268 init_test(cx, |_| {});
5269
5270 cx.write_to_clipboard(ClipboardItem::new_string(
5271 " d(\n e\n );\n".into(),
5272 ));
5273
5274 let mut cx = EditorTestContext::new(cx).await;
5275 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5276
5277 cx.set_state(indoc! {"
5278 fn a() {
5279 b();
5280 if c() {
5281 ˇ
5282 }
5283 }
5284 "});
5285
5286 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5287 cx.assert_editor_state(indoc! {"
5288 fn a() {
5289 b();
5290 if c() {
5291 d(
5292 e
5293 );
5294 ˇ
5295 }
5296 }
5297 "});
5298
5299 cx.set_state(indoc! {"
5300 fn a() {
5301 b();
5302 ˇ
5303 }
5304 "});
5305
5306 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5307 cx.assert_editor_state(indoc! {"
5308 fn a() {
5309 b();
5310 d(
5311 e
5312 );
5313 ˇ
5314 }
5315 "});
5316}
5317
5318#[gpui::test]
5319fn test_select_all(cx: &mut TestAppContext) {
5320 init_test(cx, |_| {});
5321
5322 let editor = cx.add_window(|window, cx| {
5323 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5324 build_editor(buffer, window, cx)
5325 });
5326 _ = editor.update(cx, |editor, window, cx| {
5327 editor.select_all(&SelectAll, window, cx);
5328 assert_eq!(
5329 editor.selections.display_ranges(cx),
5330 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5331 );
5332 });
5333}
5334
5335#[gpui::test]
5336fn test_select_line(cx: &mut TestAppContext) {
5337 init_test(cx, |_| {});
5338
5339 let editor = cx.add_window(|window, cx| {
5340 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5341 build_editor(buffer, window, cx)
5342 });
5343 _ = editor.update(cx, |editor, window, cx| {
5344 editor.change_selections(None, window, cx, |s| {
5345 s.select_display_ranges([
5346 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5347 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5348 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5349 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5350 ])
5351 });
5352 editor.select_line(&SelectLine, window, cx);
5353 assert_eq!(
5354 editor.selections.display_ranges(cx),
5355 vec![
5356 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5357 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5358 ]
5359 );
5360 });
5361
5362 _ = editor.update(cx, |editor, window, cx| {
5363 editor.select_line(&SelectLine, window, cx);
5364 assert_eq!(
5365 editor.selections.display_ranges(cx),
5366 vec![
5367 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5368 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5369 ]
5370 );
5371 });
5372
5373 _ = editor.update(cx, |editor, window, cx| {
5374 editor.select_line(&SelectLine, window, cx);
5375 assert_eq!(
5376 editor.selections.display_ranges(cx),
5377 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5378 );
5379 });
5380}
5381
5382#[gpui::test]
5383async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5384 init_test(cx, |_| {});
5385 let mut cx = EditorTestContext::new(cx).await;
5386
5387 #[track_caller]
5388 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5389 cx.set_state(initial_state);
5390 cx.update_editor(|e, window, cx| {
5391 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5392 });
5393 cx.assert_editor_state(expected_state);
5394 }
5395
5396 // Selection starts and ends at the middle of lines, left-to-right
5397 test(
5398 &mut cx,
5399 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5400 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5401 );
5402 // Same thing, right-to-left
5403 test(
5404 &mut cx,
5405 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5406 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5407 );
5408
5409 // Whole buffer, left-to-right, last line *doesn't* end with newline
5410 test(
5411 &mut cx,
5412 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5413 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5414 );
5415 // Same thing, right-to-left
5416 test(
5417 &mut cx,
5418 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5419 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5420 );
5421
5422 // Whole buffer, left-to-right, last line ends with newline
5423 test(
5424 &mut cx,
5425 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5426 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5427 );
5428 // Same thing, right-to-left
5429 test(
5430 &mut cx,
5431 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5432 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5433 );
5434
5435 // Starts at the end of a line, ends at the start of another
5436 test(
5437 &mut cx,
5438 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5439 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5440 );
5441}
5442
5443#[gpui::test]
5444async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5445 init_test(cx, |_| {});
5446
5447 let editor = cx.add_window(|window, cx| {
5448 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5449 build_editor(buffer, window, cx)
5450 });
5451
5452 // setup
5453 _ = editor.update(cx, |editor, window, cx| {
5454 editor.fold_creases(
5455 vec![
5456 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5457 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5458 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5459 ],
5460 true,
5461 window,
5462 cx,
5463 );
5464 assert_eq!(
5465 editor.display_text(cx),
5466 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5467 );
5468 });
5469
5470 _ = editor.update(cx, |editor, window, cx| {
5471 editor.change_selections(None, window, cx, |s| {
5472 s.select_display_ranges([
5473 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5474 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5475 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5476 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5477 ])
5478 });
5479 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5480 assert_eq!(
5481 editor.display_text(cx),
5482 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5483 );
5484 });
5485 EditorTestContext::for_editor(editor, cx)
5486 .await
5487 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5488
5489 _ = editor.update(cx, |editor, window, cx| {
5490 editor.change_selections(None, window, cx, |s| {
5491 s.select_display_ranges([
5492 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5493 ])
5494 });
5495 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5496 assert_eq!(
5497 editor.display_text(cx),
5498 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5499 );
5500 assert_eq!(
5501 editor.selections.display_ranges(cx),
5502 [
5503 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5504 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5505 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5506 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5507 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5508 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5509 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5510 ]
5511 );
5512 });
5513 EditorTestContext::for_editor(editor, cx)
5514 .await
5515 .assert_editor_state(
5516 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5517 );
5518}
5519
5520#[gpui::test]
5521async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5522 init_test(cx, |_| {});
5523
5524 let mut cx = EditorTestContext::new(cx).await;
5525
5526 cx.set_state(indoc!(
5527 r#"abc
5528 defˇghi
5529
5530 jk
5531 nlmo
5532 "#
5533 ));
5534
5535 cx.update_editor(|editor, window, cx| {
5536 editor.add_selection_above(&Default::default(), window, cx);
5537 });
5538
5539 cx.assert_editor_state(indoc!(
5540 r#"abcˇ
5541 defˇghi
5542
5543 jk
5544 nlmo
5545 "#
5546 ));
5547
5548 cx.update_editor(|editor, window, cx| {
5549 editor.add_selection_above(&Default::default(), window, cx);
5550 });
5551
5552 cx.assert_editor_state(indoc!(
5553 r#"abcˇ
5554 defˇghi
5555
5556 jk
5557 nlmo
5558 "#
5559 ));
5560
5561 cx.update_editor(|editor, window, cx| {
5562 editor.add_selection_below(&Default::default(), window, cx);
5563 });
5564
5565 cx.assert_editor_state(indoc!(
5566 r#"abc
5567 defˇghi
5568
5569 jk
5570 nlmo
5571 "#
5572 ));
5573
5574 cx.update_editor(|editor, window, cx| {
5575 editor.undo_selection(&Default::default(), window, cx);
5576 });
5577
5578 cx.assert_editor_state(indoc!(
5579 r#"abcˇ
5580 defˇghi
5581
5582 jk
5583 nlmo
5584 "#
5585 ));
5586
5587 cx.update_editor(|editor, window, cx| {
5588 editor.redo_selection(&Default::default(), window, cx);
5589 });
5590
5591 cx.assert_editor_state(indoc!(
5592 r#"abc
5593 defˇghi
5594
5595 jk
5596 nlmo
5597 "#
5598 ));
5599
5600 cx.update_editor(|editor, window, cx| {
5601 editor.add_selection_below(&Default::default(), window, cx);
5602 });
5603
5604 cx.assert_editor_state(indoc!(
5605 r#"abc
5606 defˇghi
5607
5608 jk
5609 nlmˇo
5610 "#
5611 ));
5612
5613 cx.update_editor(|editor, window, cx| {
5614 editor.add_selection_below(&Default::default(), window, cx);
5615 });
5616
5617 cx.assert_editor_state(indoc!(
5618 r#"abc
5619 defˇghi
5620
5621 jk
5622 nlmˇo
5623 "#
5624 ));
5625
5626 // change selections
5627 cx.set_state(indoc!(
5628 r#"abc
5629 def«ˇg»hi
5630
5631 jk
5632 nlmo
5633 "#
5634 ));
5635
5636 cx.update_editor(|editor, window, cx| {
5637 editor.add_selection_below(&Default::default(), window, cx);
5638 });
5639
5640 cx.assert_editor_state(indoc!(
5641 r#"abc
5642 def«ˇg»hi
5643
5644 jk
5645 nlm«ˇo»
5646 "#
5647 ));
5648
5649 cx.update_editor(|editor, window, cx| {
5650 editor.add_selection_below(&Default::default(), window, cx);
5651 });
5652
5653 cx.assert_editor_state(indoc!(
5654 r#"abc
5655 def«ˇg»hi
5656
5657 jk
5658 nlm«ˇo»
5659 "#
5660 ));
5661
5662 cx.update_editor(|editor, window, cx| {
5663 editor.add_selection_above(&Default::default(), window, cx);
5664 });
5665
5666 cx.assert_editor_state(indoc!(
5667 r#"abc
5668 def«ˇg»hi
5669
5670 jk
5671 nlmo
5672 "#
5673 ));
5674
5675 cx.update_editor(|editor, window, cx| {
5676 editor.add_selection_above(&Default::default(), window, cx);
5677 });
5678
5679 cx.assert_editor_state(indoc!(
5680 r#"abc
5681 def«ˇg»hi
5682
5683 jk
5684 nlmo
5685 "#
5686 ));
5687
5688 // Change selections again
5689 cx.set_state(indoc!(
5690 r#"a«bc
5691 defgˇ»hi
5692
5693 jk
5694 nlmo
5695 "#
5696 ));
5697
5698 cx.update_editor(|editor, window, cx| {
5699 editor.add_selection_below(&Default::default(), window, cx);
5700 });
5701
5702 cx.assert_editor_state(indoc!(
5703 r#"a«bcˇ»
5704 d«efgˇ»hi
5705
5706 j«kˇ»
5707 nlmo
5708 "#
5709 ));
5710
5711 cx.update_editor(|editor, window, cx| {
5712 editor.add_selection_below(&Default::default(), window, cx);
5713 });
5714 cx.assert_editor_state(indoc!(
5715 r#"a«bcˇ»
5716 d«efgˇ»hi
5717
5718 j«kˇ»
5719 n«lmoˇ»
5720 "#
5721 ));
5722 cx.update_editor(|editor, window, cx| {
5723 editor.add_selection_above(&Default::default(), window, cx);
5724 });
5725
5726 cx.assert_editor_state(indoc!(
5727 r#"a«bcˇ»
5728 d«efgˇ»hi
5729
5730 j«kˇ»
5731 nlmo
5732 "#
5733 ));
5734
5735 // Change selections again
5736 cx.set_state(indoc!(
5737 r#"abc
5738 d«ˇefghi
5739
5740 jk
5741 nlm»o
5742 "#
5743 ));
5744
5745 cx.update_editor(|editor, window, cx| {
5746 editor.add_selection_above(&Default::default(), window, cx);
5747 });
5748
5749 cx.assert_editor_state(indoc!(
5750 r#"a«ˇbc»
5751 d«ˇef»ghi
5752
5753 j«ˇk»
5754 n«ˇlm»o
5755 "#
5756 ));
5757
5758 cx.update_editor(|editor, window, cx| {
5759 editor.add_selection_below(&Default::default(), window, cx);
5760 });
5761
5762 cx.assert_editor_state(indoc!(
5763 r#"abc
5764 d«ˇef»ghi
5765
5766 j«ˇk»
5767 n«ˇlm»o
5768 "#
5769 ));
5770}
5771
5772#[gpui::test]
5773async fn test_select_next(cx: &mut TestAppContext) {
5774 init_test(cx, |_| {});
5775
5776 let mut cx = EditorTestContext::new(cx).await;
5777 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5778
5779 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5780 .unwrap();
5781 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5782
5783 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5784 .unwrap();
5785 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5786
5787 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5788 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5789
5790 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5791 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5792
5793 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5794 .unwrap();
5795 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5796
5797 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5798 .unwrap();
5799 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5800}
5801
5802#[gpui::test]
5803async fn test_select_all_matches(cx: &mut TestAppContext) {
5804 init_test(cx, |_| {});
5805
5806 let mut cx = EditorTestContext::new(cx).await;
5807
5808 // Test caret-only selections
5809 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5810 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5811 .unwrap();
5812 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5813
5814 // Test left-to-right selections
5815 cx.set_state("abc\n«abcˇ»\nabc");
5816 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5817 .unwrap();
5818 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5819
5820 // Test right-to-left selections
5821 cx.set_state("abc\n«ˇabc»\nabc");
5822 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5823 .unwrap();
5824 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5825
5826 // Test selecting whitespace with caret selection
5827 cx.set_state("abc\nˇ abc\nabc");
5828 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5829 .unwrap();
5830 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5831
5832 // Test selecting whitespace with left-to-right selection
5833 cx.set_state("abc\n«ˇ »abc\nabc");
5834 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5835 .unwrap();
5836 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
5837
5838 // Test no matches with right-to-left selection
5839 cx.set_state("abc\n« ˇ»abc\nabc");
5840 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5841 .unwrap();
5842 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
5843}
5844
5845#[gpui::test]
5846async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
5847 init_test(cx, |_| {});
5848
5849 let mut cx = EditorTestContext::new(cx).await;
5850 cx.set_state(
5851 r#"let foo = 2;
5852lˇet foo = 2;
5853let fooˇ = 2;
5854let foo = 2;
5855let foo = ˇ2;"#,
5856 );
5857
5858 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5859 .unwrap();
5860 cx.assert_editor_state(
5861 r#"let foo = 2;
5862«letˇ» foo = 2;
5863let «fooˇ» = 2;
5864let foo = 2;
5865let foo = «2ˇ»;"#,
5866 );
5867
5868 // noop for multiple selections with different contents
5869 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5870 .unwrap();
5871 cx.assert_editor_state(
5872 r#"let foo = 2;
5873«letˇ» foo = 2;
5874let «fooˇ» = 2;
5875let foo = 2;
5876let foo = «2ˇ»;"#,
5877 );
5878}
5879
5880#[gpui::test]
5881async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
5882 init_test(cx, |_| {});
5883
5884 let mut cx =
5885 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
5886
5887 cx.assert_editor_state(indoc! {"
5888 ˇbbb
5889 ccc
5890
5891 bbb
5892 ccc
5893 "});
5894 cx.dispatch_action(SelectPrevious::default());
5895 cx.assert_editor_state(indoc! {"
5896 «bbbˇ»
5897 ccc
5898
5899 bbb
5900 ccc
5901 "});
5902 cx.dispatch_action(SelectPrevious::default());
5903 cx.assert_editor_state(indoc! {"
5904 «bbbˇ»
5905 ccc
5906
5907 «bbbˇ»
5908 ccc
5909 "});
5910}
5911
5912#[gpui::test]
5913async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
5914 init_test(cx, |_| {});
5915
5916 let mut cx = EditorTestContext::new(cx).await;
5917 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5918
5919 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5920 .unwrap();
5921 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5922
5923 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5924 .unwrap();
5925 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5926
5927 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5928 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5929
5930 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5931 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
5932
5933 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5934 .unwrap();
5935 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
5936
5937 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5938 .unwrap();
5939 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndef«abcˇ»\n«abcˇ»");
5940
5941 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5942 .unwrap();
5943 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
5944}
5945
5946#[gpui::test]
5947async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
5948 init_test(cx, |_| {});
5949
5950 let mut cx = EditorTestContext::new(cx).await;
5951 cx.set_state("aˇ");
5952
5953 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5954 .unwrap();
5955 cx.assert_editor_state("«aˇ»");
5956 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5957 .unwrap();
5958 cx.assert_editor_state("«aˇ»");
5959}
5960
5961#[gpui::test]
5962async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
5963 init_test(cx, |_| {});
5964
5965 let mut cx = EditorTestContext::new(cx).await;
5966 cx.set_state(
5967 r#"let foo = 2;
5968lˇet foo = 2;
5969let fooˇ = 2;
5970let foo = 2;
5971let foo = ˇ2;"#,
5972 );
5973
5974 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5975 .unwrap();
5976 cx.assert_editor_state(
5977 r#"let foo = 2;
5978«letˇ» foo = 2;
5979let «fooˇ» = 2;
5980let foo = 2;
5981let foo = «2ˇ»;"#,
5982 );
5983
5984 // noop for multiple selections with different contents
5985 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
5986 .unwrap();
5987 cx.assert_editor_state(
5988 r#"let foo = 2;
5989«letˇ» foo = 2;
5990let «fooˇ» = 2;
5991let foo = 2;
5992let foo = «2ˇ»;"#,
5993 );
5994}
5995
5996#[gpui::test]
5997async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
5998 init_test(cx, |_| {});
5999
6000 let mut cx = EditorTestContext::new(cx).await;
6001 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6002
6003 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6004 .unwrap();
6005 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
6006
6007 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6008 .unwrap();
6009 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
6010
6011 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6012 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\nabc");
6013
6014 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6015 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndefabc\n«abcˇ»");
6016
6017 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6018 .unwrap();
6019 cx.assert_editor_state("«abcˇ»\n«ˇabc» abc\ndef«abcˇ»\n«abcˇ»");
6020
6021 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6022 .unwrap();
6023 cx.assert_editor_state("«abcˇ»\n«ˇabc» «abcˇ»\ndef«abcˇ»\n«abcˇ»");
6024}
6025
6026#[gpui::test]
6027async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6028 init_test(cx, |_| {});
6029
6030 let language = Arc::new(Language::new(
6031 LanguageConfig::default(),
6032 Some(tree_sitter_rust::LANGUAGE.into()),
6033 ));
6034
6035 let text = r#"
6036 use mod1::mod2::{mod3, mod4};
6037
6038 fn fn_1(param1: bool, param2: &str) {
6039 let var1 = "text";
6040 }
6041 "#
6042 .unindent();
6043
6044 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6045 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6046 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6047
6048 editor
6049 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6050 .await;
6051
6052 editor.update_in(cx, |editor, window, cx| {
6053 editor.change_selections(None, window, cx, |s| {
6054 s.select_display_ranges([
6055 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6056 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6057 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6058 ]);
6059 });
6060 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6061 });
6062 editor.update(cx, |editor, cx| {
6063 assert_text_with_selections(
6064 editor,
6065 indoc! {r#"
6066 use mod1::mod2::{mod3, «mod4ˇ»};
6067
6068 fn fn_1«ˇ(param1: bool, param2: &str)» {
6069 let var1 = "«ˇtext»";
6070 }
6071 "#},
6072 cx,
6073 );
6074 });
6075
6076 editor.update_in(cx, |editor, window, cx| {
6077 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6078 });
6079 editor.update(cx, |editor, cx| {
6080 assert_text_with_selections(
6081 editor,
6082 indoc! {r#"
6083 use mod1::mod2::«{mod3, mod4}ˇ»;
6084
6085 «ˇfn fn_1(param1: bool, param2: &str) {
6086 let var1 = "text";
6087 }»
6088 "#},
6089 cx,
6090 );
6091 });
6092
6093 editor.update_in(cx, |editor, window, cx| {
6094 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6095 });
6096 assert_eq!(
6097 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6098 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6099 );
6100
6101 // Trying to expand the selected syntax node one more time has no effect.
6102 editor.update_in(cx, |editor, window, cx| {
6103 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6104 });
6105 assert_eq!(
6106 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6107 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6108 );
6109
6110 editor.update_in(cx, |editor, window, cx| {
6111 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6112 });
6113 editor.update(cx, |editor, cx| {
6114 assert_text_with_selections(
6115 editor,
6116 indoc! {r#"
6117 use mod1::mod2::«{mod3, mod4}ˇ»;
6118
6119 «ˇfn fn_1(param1: bool, param2: &str) {
6120 let var1 = "text";
6121 }»
6122 "#},
6123 cx,
6124 );
6125 });
6126
6127 editor.update_in(cx, |editor, window, cx| {
6128 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6129 });
6130 editor.update(cx, |editor, cx| {
6131 assert_text_with_selections(
6132 editor,
6133 indoc! {r#"
6134 use mod1::mod2::{mod3, «mod4ˇ»};
6135
6136 fn fn_1«ˇ(param1: bool, param2: &str)» {
6137 let var1 = "«ˇtext»";
6138 }
6139 "#},
6140 cx,
6141 );
6142 });
6143
6144 editor.update_in(cx, |editor, window, cx| {
6145 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6146 });
6147 editor.update(cx, |editor, cx| {
6148 assert_text_with_selections(
6149 editor,
6150 indoc! {r#"
6151 use mod1::mod2::{mod3, mo«ˇ»d4};
6152
6153 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6154 let var1 = "te«ˇ»xt";
6155 }
6156 "#},
6157 cx,
6158 );
6159 });
6160
6161 // Trying to shrink the selected syntax node one more time has no effect.
6162 editor.update_in(cx, |editor, window, cx| {
6163 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6164 });
6165 editor.update_in(cx, |editor, _, cx| {
6166 assert_text_with_selections(
6167 editor,
6168 indoc! {r#"
6169 use mod1::mod2::{mod3, mo«ˇ»d4};
6170
6171 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6172 let var1 = "te«ˇ»xt";
6173 }
6174 "#},
6175 cx,
6176 );
6177 });
6178
6179 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6180 // a fold.
6181 editor.update_in(cx, |editor, window, cx| {
6182 editor.fold_creases(
6183 vec![
6184 Crease::simple(
6185 Point::new(0, 21)..Point::new(0, 24),
6186 FoldPlaceholder::test(),
6187 ),
6188 Crease::simple(
6189 Point::new(3, 20)..Point::new(3, 22),
6190 FoldPlaceholder::test(),
6191 ),
6192 ],
6193 true,
6194 window,
6195 cx,
6196 );
6197 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6198 });
6199 editor.update(cx, |editor, cx| {
6200 assert_text_with_selections(
6201 editor,
6202 indoc! {r#"
6203 use mod1::mod2::«{mod3, mod4}ˇ»;
6204
6205 fn fn_1«ˇ(param1: bool, param2: &str)» {
6206 «ˇlet var1 = "text";»
6207 }
6208 "#},
6209 cx,
6210 );
6211 });
6212}
6213
6214#[gpui::test]
6215async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6216 init_test(cx, |_| {});
6217
6218 let base_text = r#"
6219 impl A {
6220 // this is an uncommitted comment
6221
6222 fn b() {
6223 c();
6224 }
6225
6226 // this is another uncommitted comment
6227
6228 fn d() {
6229 // e
6230 // f
6231 }
6232 }
6233
6234 fn g() {
6235 // h
6236 }
6237 "#
6238 .unindent();
6239
6240 let text = r#"
6241 ˇimpl A {
6242
6243 fn b() {
6244 c();
6245 }
6246
6247 fn d() {
6248 // e
6249 // f
6250 }
6251 }
6252
6253 fn g() {
6254 // h
6255 }
6256 "#
6257 .unindent();
6258
6259 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6260 cx.set_state(&text);
6261 cx.set_head_text(&base_text);
6262 cx.update_editor(|editor, window, cx| {
6263 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6264 });
6265
6266 cx.assert_state_with_diff(
6267 "
6268 ˇimpl A {
6269 - // this is an uncommitted comment
6270
6271 fn b() {
6272 c();
6273 }
6274
6275 - // this is another uncommitted comment
6276 -
6277 fn d() {
6278 // e
6279 // f
6280 }
6281 }
6282
6283 fn g() {
6284 // h
6285 }
6286 "
6287 .unindent(),
6288 );
6289
6290 let expected_display_text = "
6291 impl A {
6292 // this is an uncommitted comment
6293
6294 fn b() {
6295 ⋯
6296 }
6297
6298 // this is another uncommitted comment
6299
6300 fn d() {
6301 ⋯
6302 }
6303 }
6304
6305 fn g() {
6306 ⋯
6307 }
6308 "
6309 .unindent();
6310
6311 cx.update_editor(|editor, window, cx| {
6312 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6313 assert_eq!(editor.display_text(cx), expected_display_text);
6314 });
6315}
6316
6317#[gpui::test]
6318async fn test_autoindent(cx: &mut TestAppContext) {
6319 init_test(cx, |_| {});
6320
6321 let language = Arc::new(
6322 Language::new(
6323 LanguageConfig {
6324 brackets: BracketPairConfig {
6325 pairs: vec![
6326 BracketPair {
6327 start: "{".to_string(),
6328 end: "}".to_string(),
6329 close: false,
6330 surround: false,
6331 newline: true,
6332 },
6333 BracketPair {
6334 start: "(".to_string(),
6335 end: ")".to_string(),
6336 close: false,
6337 surround: false,
6338 newline: true,
6339 },
6340 ],
6341 ..Default::default()
6342 },
6343 ..Default::default()
6344 },
6345 Some(tree_sitter_rust::LANGUAGE.into()),
6346 )
6347 .with_indents_query(
6348 r#"
6349 (_ "(" ")" @end) @indent
6350 (_ "{" "}" @end) @indent
6351 "#,
6352 )
6353 .unwrap(),
6354 );
6355
6356 let text = "fn a() {}";
6357
6358 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6359 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6360 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6361 editor
6362 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6363 .await;
6364
6365 editor.update_in(cx, |editor, window, cx| {
6366 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6367 editor.newline(&Newline, window, cx);
6368 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6369 assert_eq!(
6370 editor.selections.ranges(cx),
6371 &[
6372 Point::new(1, 4)..Point::new(1, 4),
6373 Point::new(3, 4)..Point::new(3, 4),
6374 Point::new(5, 0)..Point::new(5, 0)
6375 ]
6376 );
6377 });
6378}
6379
6380#[gpui::test]
6381async fn test_autoindent_selections(cx: &mut TestAppContext) {
6382 init_test(cx, |_| {});
6383
6384 {
6385 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6386 cx.set_state(indoc! {"
6387 impl A {
6388
6389 fn b() {}
6390
6391 «fn c() {
6392
6393 }ˇ»
6394 }
6395 "});
6396
6397 cx.update_editor(|editor, window, cx| {
6398 editor.autoindent(&Default::default(), window, cx);
6399 });
6400
6401 cx.assert_editor_state(indoc! {"
6402 impl A {
6403
6404 fn b() {}
6405
6406 «fn c() {
6407
6408 }ˇ»
6409 }
6410 "});
6411 }
6412
6413 {
6414 let mut cx = EditorTestContext::new_multibuffer(
6415 cx,
6416 [indoc! { "
6417 impl A {
6418 «
6419 // a
6420 fn b(){}
6421 »
6422 «
6423 }
6424 fn c(){}
6425 »
6426 "}],
6427 );
6428
6429 let buffer = cx.update_editor(|editor, _, cx| {
6430 let buffer = editor.buffer().update(cx, |buffer, _| {
6431 buffer.all_buffers().iter().next().unwrap().clone()
6432 });
6433 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6434 buffer
6435 });
6436
6437 cx.run_until_parked();
6438 cx.update_editor(|editor, window, cx| {
6439 editor.select_all(&Default::default(), window, cx);
6440 editor.autoindent(&Default::default(), window, cx)
6441 });
6442 cx.run_until_parked();
6443
6444 cx.update(|_, cx| {
6445 assert_eq!(
6446 buffer.read(cx).text(),
6447 indoc! { "
6448 impl A {
6449
6450 // a
6451 fn b(){}
6452
6453
6454 }
6455 fn c(){}
6456
6457 " }
6458 )
6459 });
6460 }
6461}
6462
6463#[gpui::test]
6464async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
6465 init_test(cx, |_| {});
6466
6467 let mut cx = EditorTestContext::new(cx).await;
6468
6469 let language = Arc::new(Language::new(
6470 LanguageConfig {
6471 brackets: BracketPairConfig {
6472 pairs: vec![
6473 BracketPair {
6474 start: "{".to_string(),
6475 end: "}".to_string(),
6476 close: true,
6477 surround: true,
6478 newline: true,
6479 },
6480 BracketPair {
6481 start: "(".to_string(),
6482 end: ")".to_string(),
6483 close: true,
6484 surround: true,
6485 newline: true,
6486 },
6487 BracketPair {
6488 start: "/*".to_string(),
6489 end: " */".to_string(),
6490 close: true,
6491 surround: true,
6492 newline: true,
6493 },
6494 BracketPair {
6495 start: "[".to_string(),
6496 end: "]".to_string(),
6497 close: false,
6498 surround: false,
6499 newline: true,
6500 },
6501 BracketPair {
6502 start: "\"".to_string(),
6503 end: "\"".to_string(),
6504 close: true,
6505 surround: true,
6506 newline: false,
6507 },
6508 BracketPair {
6509 start: "<".to_string(),
6510 end: ">".to_string(),
6511 close: false,
6512 surround: true,
6513 newline: true,
6514 },
6515 ],
6516 ..Default::default()
6517 },
6518 autoclose_before: "})]".to_string(),
6519 ..Default::default()
6520 },
6521 Some(tree_sitter_rust::LANGUAGE.into()),
6522 ));
6523
6524 cx.language_registry().add(language.clone());
6525 cx.update_buffer(|buffer, cx| {
6526 buffer.set_language(Some(language), cx);
6527 });
6528
6529 cx.set_state(
6530 &r#"
6531 🏀ˇ
6532 εˇ
6533 ❤️ˇ
6534 "#
6535 .unindent(),
6536 );
6537
6538 // autoclose multiple nested brackets at multiple cursors
6539 cx.update_editor(|editor, window, cx| {
6540 editor.handle_input("{", window, cx);
6541 editor.handle_input("{", window, cx);
6542 editor.handle_input("{", window, cx);
6543 });
6544 cx.assert_editor_state(
6545 &"
6546 🏀{{{ˇ}}}
6547 ε{{{ˇ}}}
6548 ❤️{{{ˇ}}}
6549 "
6550 .unindent(),
6551 );
6552
6553 // insert a different closing bracket
6554 cx.update_editor(|editor, window, cx| {
6555 editor.handle_input(")", window, cx);
6556 });
6557 cx.assert_editor_state(
6558 &"
6559 🏀{{{)ˇ}}}
6560 ε{{{)ˇ}}}
6561 ❤️{{{)ˇ}}}
6562 "
6563 .unindent(),
6564 );
6565
6566 // skip over the auto-closed brackets when typing a closing bracket
6567 cx.update_editor(|editor, window, cx| {
6568 editor.move_right(&MoveRight, window, cx);
6569 editor.handle_input("}", window, cx);
6570 editor.handle_input("}", window, cx);
6571 editor.handle_input("}", window, cx);
6572 });
6573 cx.assert_editor_state(
6574 &"
6575 🏀{{{)}}}}ˇ
6576 ε{{{)}}}}ˇ
6577 ❤️{{{)}}}}ˇ
6578 "
6579 .unindent(),
6580 );
6581
6582 // autoclose multi-character pairs
6583 cx.set_state(
6584 &"
6585 ˇ
6586 ˇ
6587 "
6588 .unindent(),
6589 );
6590 cx.update_editor(|editor, window, cx| {
6591 editor.handle_input("/", window, cx);
6592 editor.handle_input("*", window, cx);
6593 });
6594 cx.assert_editor_state(
6595 &"
6596 /*ˇ */
6597 /*ˇ */
6598 "
6599 .unindent(),
6600 );
6601
6602 // one cursor autocloses a multi-character pair, one cursor
6603 // does not autoclose.
6604 cx.set_state(
6605 &"
6606 /ˇ
6607 ˇ
6608 "
6609 .unindent(),
6610 );
6611 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
6612 cx.assert_editor_state(
6613 &"
6614 /*ˇ */
6615 *ˇ
6616 "
6617 .unindent(),
6618 );
6619
6620 // Don't autoclose if the next character isn't whitespace and isn't
6621 // listed in the language's "autoclose_before" section.
6622 cx.set_state("ˇa b");
6623 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6624 cx.assert_editor_state("{ˇa b");
6625
6626 // Don't autoclose if `close` is false for the bracket pair
6627 cx.set_state("ˇ");
6628 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
6629 cx.assert_editor_state("[ˇ");
6630
6631 // Surround with brackets if text is selected
6632 cx.set_state("«aˇ» b");
6633 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6634 cx.assert_editor_state("{«aˇ»} b");
6635
6636 // Autoclose when not immediately after a word character
6637 cx.set_state("a ˇ");
6638 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6639 cx.assert_editor_state("a \"ˇ\"");
6640
6641 // Autoclose pair where the start and end characters are the same
6642 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6643 cx.assert_editor_state("a \"\"ˇ");
6644
6645 // Don't autoclose when immediately after a word character
6646 cx.set_state("aˇ");
6647 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6648 cx.assert_editor_state("a\"ˇ");
6649
6650 // Do autoclose when after a non-word character
6651 cx.set_state("{ˇ");
6652 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
6653 cx.assert_editor_state("{\"ˇ\"");
6654
6655 // Non identical pairs autoclose regardless of preceding character
6656 cx.set_state("aˇ");
6657 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
6658 cx.assert_editor_state("a{ˇ}");
6659
6660 // Don't autoclose pair if autoclose is disabled
6661 cx.set_state("ˇ");
6662 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6663 cx.assert_editor_state("<ˇ");
6664
6665 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
6666 cx.set_state("«aˇ» b");
6667 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
6668 cx.assert_editor_state("<«aˇ»> b");
6669}
6670
6671#[gpui::test]
6672async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
6673 init_test(cx, |settings| {
6674 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
6675 });
6676
6677 let mut cx = EditorTestContext::new(cx).await;
6678
6679 let language = Arc::new(Language::new(
6680 LanguageConfig {
6681 brackets: BracketPairConfig {
6682 pairs: vec![
6683 BracketPair {
6684 start: "{".to_string(),
6685 end: "}".to_string(),
6686 close: true,
6687 surround: true,
6688 newline: true,
6689 },
6690 BracketPair {
6691 start: "(".to_string(),
6692 end: ")".to_string(),
6693 close: true,
6694 surround: true,
6695 newline: true,
6696 },
6697 BracketPair {
6698 start: "[".to_string(),
6699 end: "]".to_string(),
6700 close: false,
6701 surround: false,
6702 newline: true,
6703 },
6704 ],
6705 ..Default::default()
6706 },
6707 autoclose_before: "})]".to_string(),
6708 ..Default::default()
6709 },
6710 Some(tree_sitter_rust::LANGUAGE.into()),
6711 ));
6712
6713 cx.language_registry().add(language.clone());
6714 cx.update_buffer(|buffer, cx| {
6715 buffer.set_language(Some(language), cx);
6716 });
6717
6718 cx.set_state(
6719 &"
6720 ˇ
6721 ˇ
6722 ˇ
6723 "
6724 .unindent(),
6725 );
6726
6727 // ensure only matching closing brackets are skipped over
6728 cx.update_editor(|editor, window, cx| {
6729 editor.handle_input("}", window, cx);
6730 editor.move_left(&MoveLeft, window, cx);
6731 editor.handle_input(")", window, cx);
6732 editor.move_left(&MoveLeft, window, cx);
6733 });
6734 cx.assert_editor_state(
6735 &"
6736 ˇ)}
6737 ˇ)}
6738 ˇ)}
6739 "
6740 .unindent(),
6741 );
6742
6743 // skip-over closing brackets at multiple cursors
6744 cx.update_editor(|editor, window, cx| {
6745 editor.handle_input(")", window, cx);
6746 editor.handle_input("}", window, cx);
6747 });
6748 cx.assert_editor_state(
6749 &"
6750 )}ˇ
6751 )}ˇ
6752 )}ˇ
6753 "
6754 .unindent(),
6755 );
6756
6757 // ignore non-close brackets
6758 cx.update_editor(|editor, window, cx| {
6759 editor.handle_input("]", window, cx);
6760 editor.move_left(&MoveLeft, window, cx);
6761 editor.handle_input("]", window, cx);
6762 });
6763 cx.assert_editor_state(
6764 &"
6765 )}]ˇ]
6766 )}]ˇ]
6767 )}]ˇ]
6768 "
6769 .unindent(),
6770 );
6771}
6772
6773#[gpui::test]
6774async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
6775 init_test(cx, |_| {});
6776
6777 let mut cx = EditorTestContext::new(cx).await;
6778
6779 let html_language = Arc::new(
6780 Language::new(
6781 LanguageConfig {
6782 name: "HTML".into(),
6783 brackets: BracketPairConfig {
6784 pairs: vec![
6785 BracketPair {
6786 start: "<".into(),
6787 end: ">".into(),
6788 close: true,
6789 ..Default::default()
6790 },
6791 BracketPair {
6792 start: "{".into(),
6793 end: "}".into(),
6794 close: true,
6795 ..Default::default()
6796 },
6797 BracketPair {
6798 start: "(".into(),
6799 end: ")".into(),
6800 close: true,
6801 ..Default::default()
6802 },
6803 ],
6804 ..Default::default()
6805 },
6806 autoclose_before: "})]>".into(),
6807 ..Default::default()
6808 },
6809 Some(tree_sitter_html::LANGUAGE.into()),
6810 )
6811 .with_injection_query(
6812 r#"
6813 (script_element
6814 (raw_text) @injection.content
6815 (#set! injection.language "javascript"))
6816 "#,
6817 )
6818 .unwrap(),
6819 );
6820
6821 let javascript_language = Arc::new(Language::new(
6822 LanguageConfig {
6823 name: "JavaScript".into(),
6824 brackets: BracketPairConfig {
6825 pairs: vec![
6826 BracketPair {
6827 start: "/*".into(),
6828 end: " */".into(),
6829 close: true,
6830 ..Default::default()
6831 },
6832 BracketPair {
6833 start: "{".into(),
6834 end: "}".into(),
6835 close: true,
6836 ..Default::default()
6837 },
6838 BracketPair {
6839 start: "(".into(),
6840 end: ")".into(),
6841 close: true,
6842 ..Default::default()
6843 },
6844 ],
6845 ..Default::default()
6846 },
6847 autoclose_before: "})]>".into(),
6848 ..Default::default()
6849 },
6850 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
6851 ));
6852
6853 cx.language_registry().add(html_language.clone());
6854 cx.language_registry().add(javascript_language.clone());
6855
6856 cx.update_buffer(|buffer, cx| {
6857 buffer.set_language(Some(html_language), cx);
6858 });
6859
6860 cx.set_state(
6861 &r#"
6862 <body>ˇ
6863 <script>
6864 var x = 1;ˇ
6865 </script>
6866 </body>ˇ
6867 "#
6868 .unindent(),
6869 );
6870
6871 // Precondition: different languages are active at different locations.
6872 cx.update_editor(|editor, window, cx| {
6873 let snapshot = editor.snapshot(window, cx);
6874 let cursors = editor.selections.ranges::<usize>(cx);
6875 let languages = cursors
6876 .iter()
6877 .map(|c| snapshot.language_at(c.start).unwrap().name())
6878 .collect::<Vec<_>>();
6879 assert_eq!(
6880 languages,
6881 &["HTML".into(), "JavaScript".into(), "HTML".into()]
6882 );
6883 });
6884
6885 // Angle brackets autoclose in HTML, but not JavaScript.
6886 cx.update_editor(|editor, window, cx| {
6887 editor.handle_input("<", window, cx);
6888 editor.handle_input("a", window, cx);
6889 });
6890 cx.assert_editor_state(
6891 &r#"
6892 <body><aˇ>
6893 <script>
6894 var x = 1;<aˇ
6895 </script>
6896 </body><aˇ>
6897 "#
6898 .unindent(),
6899 );
6900
6901 // Curly braces and parens autoclose in both HTML and JavaScript.
6902 cx.update_editor(|editor, window, cx| {
6903 editor.handle_input(" b=", window, cx);
6904 editor.handle_input("{", window, cx);
6905 editor.handle_input("c", window, cx);
6906 editor.handle_input("(", window, cx);
6907 });
6908 cx.assert_editor_state(
6909 &r#"
6910 <body><a b={c(ˇ)}>
6911 <script>
6912 var x = 1;<a b={c(ˇ)}
6913 </script>
6914 </body><a b={c(ˇ)}>
6915 "#
6916 .unindent(),
6917 );
6918
6919 // Brackets that were already autoclosed are skipped.
6920 cx.update_editor(|editor, window, cx| {
6921 editor.handle_input(")", window, cx);
6922 editor.handle_input("d", window, cx);
6923 editor.handle_input("}", window, cx);
6924 });
6925 cx.assert_editor_state(
6926 &r#"
6927 <body><a b={c()d}ˇ>
6928 <script>
6929 var x = 1;<a b={c()d}ˇ
6930 </script>
6931 </body><a b={c()d}ˇ>
6932 "#
6933 .unindent(),
6934 );
6935 cx.update_editor(|editor, window, cx| {
6936 editor.handle_input(">", window, cx);
6937 });
6938 cx.assert_editor_state(
6939 &r#"
6940 <body><a b={c()d}>ˇ
6941 <script>
6942 var x = 1;<a b={c()d}>ˇ
6943 </script>
6944 </body><a b={c()d}>ˇ
6945 "#
6946 .unindent(),
6947 );
6948
6949 // Reset
6950 cx.set_state(
6951 &r#"
6952 <body>ˇ
6953 <script>
6954 var x = 1;ˇ
6955 </script>
6956 </body>ˇ
6957 "#
6958 .unindent(),
6959 );
6960
6961 cx.update_editor(|editor, window, cx| {
6962 editor.handle_input("<", window, cx);
6963 });
6964 cx.assert_editor_state(
6965 &r#"
6966 <body><ˇ>
6967 <script>
6968 var x = 1;<ˇ
6969 </script>
6970 </body><ˇ>
6971 "#
6972 .unindent(),
6973 );
6974
6975 // When backspacing, the closing angle brackets are removed.
6976 cx.update_editor(|editor, window, cx| {
6977 editor.backspace(&Backspace, window, cx);
6978 });
6979 cx.assert_editor_state(
6980 &r#"
6981 <body>ˇ
6982 <script>
6983 var x = 1;ˇ
6984 </script>
6985 </body>ˇ
6986 "#
6987 .unindent(),
6988 );
6989
6990 // Block comments autoclose in JavaScript, but not HTML.
6991 cx.update_editor(|editor, window, cx| {
6992 editor.handle_input("/", window, cx);
6993 editor.handle_input("*", window, cx);
6994 });
6995 cx.assert_editor_state(
6996 &r#"
6997 <body>/*ˇ
6998 <script>
6999 var x = 1;/*ˇ */
7000 </script>
7001 </body>/*ˇ
7002 "#
7003 .unindent(),
7004 );
7005}
7006
7007#[gpui::test]
7008async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7009 init_test(cx, |_| {});
7010
7011 let mut cx = EditorTestContext::new(cx).await;
7012
7013 let rust_language = Arc::new(
7014 Language::new(
7015 LanguageConfig {
7016 name: "Rust".into(),
7017 brackets: serde_json::from_value(json!([
7018 { "start": "{", "end": "}", "close": true, "newline": true },
7019 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7020 ]))
7021 .unwrap(),
7022 autoclose_before: "})]>".into(),
7023 ..Default::default()
7024 },
7025 Some(tree_sitter_rust::LANGUAGE.into()),
7026 )
7027 .with_override_query("(string_literal) @string")
7028 .unwrap(),
7029 );
7030
7031 cx.language_registry().add(rust_language.clone());
7032 cx.update_buffer(|buffer, cx| {
7033 buffer.set_language(Some(rust_language), cx);
7034 });
7035
7036 cx.set_state(
7037 &r#"
7038 let x = ˇ
7039 "#
7040 .unindent(),
7041 );
7042
7043 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7044 cx.update_editor(|editor, window, cx| {
7045 editor.handle_input("\"", window, cx);
7046 });
7047 cx.assert_editor_state(
7048 &r#"
7049 let x = "ˇ"
7050 "#
7051 .unindent(),
7052 );
7053
7054 // Inserting another quotation mark. The cursor moves across the existing
7055 // automatically-inserted quotation mark.
7056 cx.update_editor(|editor, window, cx| {
7057 editor.handle_input("\"", window, cx);
7058 });
7059 cx.assert_editor_state(
7060 &r#"
7061 let x = ""ˇ
7062 "#
7063 .unindent(),
7064 );
7065
7066 // Reset
7067 cx.set_state(
7068 &r#"
7069 let x = ˇ
7070 "#
7071 .unindent(),
7072 );
7073
7074 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7075 cx.update_editor(|editor, window, cx| {
7076 editor.handle_input("\"", window, cx);
7077 editor.handle_input(" ", window, cx);
7078 editor.move_left(&Default::default(), window, cx);
7079 editor.handle_input("\\", window, cx);
7080 editor.handle_input("\"", window, cx);
7081 });
7082 cx.assert_editor_state(
7083 &r#"
7084 let x = "\"ˇ "
7085 "#
7086 .unindent(),
7087 );
7088
7089 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7090 // mark. Nothing is inserted.
7091 cx.update_editor(|editor, window, cx| {
7092 editor.move_right(&Default::default(), window, cx);
7093 editor.handle_input("\"", window, cx);
7094 });
7095 cx.assert_editor_state(
7096 &r#"
7097 let x = "\" "ˇ
7098 "#
7099 .unindent(),
7100 );
7101}
7102
7103#[gpui::test]
7104async fn test_surround_with_pair(cx: &mut TestAppContext) {
7105 init_test(cx, |_| {});
7106
7107 let language = Arc::new(Language::new(
7108 LanguageConfig {
7109 brackets: BracketPairConfig {
7110 pairs: vec![
7111 BracketPair {
7112 start: "{".to_string(),
7113 end: "}".to_string(),
7114 close: true,
7115 surround: true,
7116 newline: true,
7117 },
7118 BracketPair {
7119 start: "/* ".to_string(),
7120 end: "*/".to_string(),
7121 close: true,
7122 surround: true,
7123 ..Default::default()
7124 },
7125 ],
7126 ..Default::default()
7127 },
7128 ..Default::default()
7129 },
7130 Some(tree_sitter_rust::LANGUAGE.into()),
7131 ));
7132
7133 let text = r#"
7134 a
7135 b
7136 c
7137 "#
7138 .unindent();
7139
7140 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7141 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7142 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7143 editor
7144 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7145 .await;
7146
7147 editor.update_in(cx, |editor, window, cx| {
7148 editor.change_selections(None, window, cx, |s| {
7149 s.select_display_ranges([
7150 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7151 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7152 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7153 ])
7154 });
7155
7156 editor.handle_input("{", window, cx);
7157 editor.handle_input("{", window, cx);
7158 editor.handle_input("{", window, cx);
7159 assert_eq!(
7160 editor.text(cx),
7161 "
7162 {{{a}}}
7163 {{{b}}}
7164 {{{c}}}
7165 "
7166 .unindent()
7167 );
7168 assert_eq!(
7169 editor.selections.display_ranges(cx),
7170 [
7171 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7172 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7173 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7174 ]
7175 );
7176
7177 editor.undo(&Undo, window, cx);
7178 editor.undo(&Undo, window, cx);
7179 editor.undo(&Undo, window, cx);
7180 assert_eq!(
7181 editor.text(cx),
7182 "
7183 a
7184 b
7185 c
7186 "
7187 .unindent()
7188 );
7189 assert_eq!(
7190 editor.selections.display_ranges(cx),
7191 [
7192 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7193 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7194 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7195 ]
7196 );
7197
7198 // Ensure inserting the first character of a multi-byte bracket pair
7199 // doesn't surround the selections with the bracket.
7200 editor.handle_input("/", window, cx);
7201 assert_eq!(
7202 editor.text(cx),
7203 "
7204 /
7205 /
7206 /
7207 "
7208 .unindent()
7209 );
7210 assert_eq!(
7211 editor.selections.display_ranges(cx),
7212 [
7213 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7214 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7215 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7216 ]
7217 );
7218
7219 editor.undo(&Undo, window, cx);
7220 assert_eq!(
7221 editor.text(cx),
7222 "
7223 a
7224 b
7225 c
7226 "
7227 .unindent()
7228 );
7229 assert_eq!(
7230 editor.selections.display_ranges(cx),
7231 [
7232 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7233 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7234 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7235 ]
7236 );
7237
7238 // Ensure inserting the last character of a multi-byte bracket pair
7239 // doesn't surround the selections with the bracket.
7240 editor.handle_input("*", window, cx);
7241 assert_eq!(
7242 editor.text(cx),
7243 "
7244 *
7245 *
7246 *
7247 "
7248 .unindent()
7249 );
7250 assert_eq!(
7251 editor.selections.display_ranges(cx),
7252 [
7253 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7254 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7255 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7256 ]
7257 );
7258 });
7259}
7260
7261#[gpui::test]
7262async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7263 init_test(cx, |_| {});
7264
7265 let language = Arc::new(Language::new(
7266 LanguageConfig {
7267 brackets: BracketPairConfig {
7268 pairs: vec![BracketPair {
7269 start: "{".to_string(),
7270 end: "}".to_string(),
7271 close: true,
7272 surround: true,
7273 newline: true,
7274 }],
7275 ..Default::default()
7276 },
7277 autoclose_before: "}".to_string(),
7278 ..Default::default()
7279 },
7280 Some(tree_sitter_rust::LANGUAGE.into()),
7281 ));
7282
7283 let text = r#"
7284 a
7285 b
7286 c
7287 "#
7288 .unindent();
7289
7290 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7291 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7292 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7293 editor
7294 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7295 .await;
7296
7297 editor.update_in(cx, |editor, window, cx| {
7298 editor.change_selections(None, window, cx, |s| {
7299 s.select_ranges([
7300 Point::new(0, 1)..Point::new(0, 1),
7301 Point::new(1, 1)..Point::new(1, 1),
7302 Point::new(2, 1)..Point::new(2, 1),
7303 ])
7304 });
7305
7306 editor.handle_input("{", window, cx);
7307 editor.handle_input("{", window, cx);
7308 editor.handle_input("_", window, cx);
7309 assert_eq!(
7310 editor.text(cx),
7311 "
7312 a{{_}}
7313 b{{_}}
7314 c{{_}}
7315 "
7316 .unindent()
7317 );
7318 assert_eq!(
7319 editor.selections.ranges::<Point>(cx),
7320 [
7321 Point::new(0, 4)..Point::new(0, 4),
7322 Point::new(1, 4)..Point::new(1, 4),
7323 Point::new(2, 4)..Point::new(2, 4)
7324 ]
7325 );
7326
7327 editor.backspace(&Default::default(), window, cx);
7328 editor.backspace(&Default::default(), window, cx);
7329 assert_eq!(
7330 editor.text(cx),
7331 "
7332 a{}
7333 b{}
7334 c{}
7335 "
7336 .unindent()
7337 );
7338 assert_eq!(
7339 editor.selections.ranges::<Point>(cx),
7340 [
7341 Point::new(0, 2)..Point::new(0, 2),
7342 Point::new(1, 2)..Point::new(1, 2),
7343 Point::new(2, 2)..Point::new(2, 2)
7344 ]
7345 );
7346
7347 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7348 assert_eq!(
7349 editor.text(cx),
7350 "
7351 a
7352 b
7353 c
7354 "
7355 .unindent()
7356 );
7357 assert_eq!(
7358 editor.selections.ranges::<Point>(cx),
7359 [
7360 Point::new(0, 1)..Point::new(0, 1),
7361 Point::new(1, 1)..Point::new(1, 1),
7362 Point::new(2, 1)..Point::new(2, 1)
7363 ]
7364 );
7365 });
7366}
7367
7368#[gpui::test]
7369async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7370 init_test(cx, |settings| {
7371 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7372 });
7373
7374 let mut cx = EditorTestContext::new(cx).await;
7375
7376 let language = Arc::new(Language::new(
7377 LanguageConfig {
7378 brackets: BracketPairConfig {
7379 pairs: vec![
7380 BracketPair {
7381 start: "{".to_string(),
7382 end: "}".to_string(),
7383 close: true,
7384 surround: true,
7385 newline: true,
7386 },
7387 BracketPair {
7388 start: "(".to_string(),
7389 end: ")".to_string(),
7390 close: true,
7391 surround: true,
7392 newline: true,
7393 },
7394 BracketPair {
7395 start: "[".to_string(),
7396 end: "]".to_string(),
7397 close: false,
7398 surround: true,
7399 newline: true,
7400 },
7401 ],
7402 ..Default::default()
7403 },
7404 autoclose_before: "})]".to_string(),
7405 ..Default::default()
7406 },
7407 Some(tree_sitter_rust::LANGUAGE.into()),
7408 ));
7409
7410 cx.language_registry().add(language.clone());
7411 cx.update_buffer(|buffer, cx| {
7412 buffer.set_language(Some(language), cx);
7413 });
7414
7415 cx.set_state(
7416 &"
7417 {(ˇ)}
7418 [[ˇ]]
7419 {(ˇ)}
7420 "
7421 .unindent(),
7422 );
7423
7424 cx.update_editor(|editor, window, cx| {
7425 editor.backspace(&Default::default(), window, cx);
7426 editor.backspace(&Default::default(), window, cx);
7427 });
7428
7429 cx.assert_editor_state(
7430 &"
7431 ˇ
7432 ˇ]]
7433 ˇ
7434 "
7435 .unindent(),
7436 );
7437
7438 cx.update_editor(|editor, window, cx| {
7439 editor.handle_input("{", window, cx);
7440 editor.handle_input("{", window, cx);
7441 editor.move_right(&MoveRight, window, cx);
7442 editor.move_right(&MoveRight, window, cx);
7443 editor.move_left(&MoveLeft, window, cx);
7444 editor.move_left(&MoveLeft, window, cx);
7445 editor.backspace(&Default::default(), window, cx);
7446 });
7447
7448 cx.assert_editor_state(
7449 &"
7450 {ˇ}
7451 {ˇ}]]
7452 {ˇ}
7453 "
7454 .unindent(),
7455 );
7456
7457 cx.update_editor(|editor, window, cx| {
7458 editor.backspace(&Default::default(), window, cx);
7459 });
7460
7461 cx.assert_editor_state(
7462 &"
7463 ˇ
7464 ˇ]]
7465 ˇ
7466 "
7467 .unindent(),
7468 );
7469}
7470
7471#[gpui::test]
7472async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
7473 init_test(cx, |_| {});
7474
7475 let language = Arc::new(Language::new(
7476 LanguageConfig::default(),
7477 Some(tree_sitter_rust::LANGUAGE.into()),
7478 ));
7479
7480 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
7481 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7482 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7483 editor
7484 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7485 .await;
7486
7487 editor.update_in(cx, |editor, window, cx| {
7488 editor.set_auto_replace_emoji_shortcode(true);
7489
7490 editor.handle_input("Hello ", window, cx);
7491 editor.handle_input(":wave", window, cx);
7492 assert_eq!(editor.text(cx), "Hello :wave".unindent());
7493
7494 editor.handle_input(":", window, cx);
7495 assert_eq!(editor.text(cx), "Hello 👋".unindent());
7496
7497 editor.handle_input(" :smile", window, cx);
7498 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
7499
7500 editor.handle_input(":", window, cx);
7501 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
7502
7503 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
7504 editor.handle_input(":wave", window, cx);
7505 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
7506
7507 editor.handle_input(":", window, cx);
7508 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
7509
7510 editor.handle_input(":1", window, cx);
7511 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
7512
7513 editor.handle_input(":", window, cx);
7514 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
7515
7516 // Ensure shortcode does not get replaced when it is part of a word
7517 editor.handle_input(" Test:wave", window, cx);
7518 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
7519
7520 editor.handle_input(":", window, cx);
7521 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
7522
7523 editor.set_auto_replace_emoji_shortcode(false);
7524
7525 // Ensure shortcode does not get replaced when auto replace is off
7526 editor.handle_input(" :wave", window, cx);
7527 assert_eq!(
7528 editor.text(cx),
7529 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
7530 );
7531
7532 editor.handle_input(":", window, cx);
7533 assert_eq!(
7534 editor.text(cx),
7535 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
7536 );
7537 });
7538}
7539
7540#[gpui::test]
7541async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
7542 init_test(cx, |_| {});
7543
7544 let (text, insertion_ranges) = marked_text_ranges(
7545 indoc! {"
7546 ˇ
7547 "},
7548 false,
7549 );
7550
7551 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7552 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7553
7554 _ = editor.update_in(cx, |editor, window, cx| {
7555 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
7556
7557 editor
7558 .insert_snippet(&insertion_ranges, snippet, window, cx)
7559 .unwrap();
7560
7561 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7562 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7563 assert_eq!(editor.text(cx), expected_text);
7564 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7565 }
7566
7567 assert(
7568 editor,
7569 cx,
7570 indoc! {"
7571 type «» =•
7572 "},
7573 );
7574
7575 assert!(editor.context_menu_visible(), "There should be a matches");
7576 });
7577}
7578
7579#[gpui::test]
7580async fn test_snippets(cx: &mut TestAppContext) {
7581 init_test(cx, |_| {});
7582
7583 let (text, insertion_ranges) = marked_text_ranges(
7584 indoc! {"
7585 a.ˇ b
7586 a.ˇ b
7587 a.ˇ b
7588 "},
7589 false,
7590 );
7591
7592 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
7593 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7594
7595 editor.update_in(cx, |editor, window, cx| {
7596 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
7597
7598 editor
7599 .insert_snippet(&insertion_ranges, snippet, window, cx)
7600 .unwrap();
7601
7602 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
7603 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
7604 assert_eq!(editor.text(cx), expected_text);
7605 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
7606 }
7607
7608 assert(
7609 editor,
7610 cx,
7611 indoc! {"
7612 a.f(«one», two, «three») b
7613 a.f(«one», two, «three») b
7614 a.f(«one», two, «three») b
7615 "},
7616 );
7617
7618 // Can't move earlier than the first tab stop
7619 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
7620 assert(
7621 editor,
7622 cx,
7623 indoc! {"
7624 a.f(«one», two, «three») b
7625 a.f(«one», two, «three») b
7626 a.f(«one», two, «three») b
7627 "},
7628 );
7629
7630 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7631 assert(
7632 editor,
7633 cx,
7634 indoc! {"
7635 a.f(one, «two», three) b
7636 a.f(one, «two», three) b
7637 a.f(one, «two», three) b
7638 "},
7639 );
7640
7641 editor.move_to_prev_snippet_tabstop(window, cx);
7642 assert(
7643 editor,
7644 cx,
7645 indoc! {"
7646 a.f(«one», two, «three») b
7647 a.f(«one», two, «three») b
7648 a.f(«one», two, «three») b
7649 "},
7650 );
7651
7652 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7653 assert(
7654 editor,
7655 cx,
7656 indoc! {"
7657 a.f(one, «two», three) b
7658 a.f(one, «two», three) b
7659 a.f(one, «two», three) b
7660 "},
7661 );
7662 assert!(editor.move_to_next_snippet_tabstop(window, cx));
7663 assert(
7664 editor,
7665 cx,
7666 indoc! {"
7667 a.f(one, two, three)ˇ b
7668 a.f(one, two, three)ˇ b
7669 a.f(one, two, three)ˇ b
7670 "},
7671 );
7672
7673 // As soon as the last tab stop is reached, snippet state is gone
7674 editor.move_to_prev_snippet_tabstop(window, cx);
7675 assert(
7676 editor,
7677 cx,
7678 indoc! {"
7679 a.f(one, two, three)ˇ b
7680 a.f(one, two, three)ˇ b
7681 a.f(one, two, three)ˇ b
7682 "},
7683 );
7684 });
7685}
7686
7687#[gpui::test]
7688async fn test_document_format_during_save(cx: &mut TestAppContext) {
7689 init_test(cx, |_| {});
7690
7691 let fs = FakeFs::new(cx.executor());
7692 fs.insert_file(path!("/file.rs"), Default::default()).await;
7693
7694 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
7695
7696 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7697 language_registry.add(rust_lang());
7698 let mut fake_servers = language_registry.register_fake_lsp(
7699 "Rust",
7700 FakeLspAdapter {
7701 capabilities: lsp::ServerCapabilities {
7702 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7703 ..Default::default()
7704 },
7705 ..Default::default()
7706 },
7707 );
7708
7709 let buffer = project
7710 .update(cx, |project, cx| {
7711 project.open_local_buffer(path!("/file.rs"), cx)
7712 })
7713 .await
7714 .unwrap();
7715
7716 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7717 let (editor, cx) = cx.add_window_view(|window, cx| {
7718 build_editor_with_project(project.clone(), buffer, window, cx)
7719 });
7720 editor.update_in(cx, |editor, window, cx| {
7721 editor.set_text("one\ntwo\nthree\n", window, cx)
7722 });
7723 assert!(cx.read(|cx| editor.is_dirty(cx)));
7724
7725 cx.executor().start_waiting();
7726 let fake_server = fake_servers.next().await.unwrap();
7727
7728 let save = editor
7729 .update_in(cx, |editor, window, cx| {
7730 editor.save(true, project.clone(), window, cx)
7731 })
7732 .unwrap();
7733 fake_server
7734 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7735 assert_eq!(
7736 params.text_document.uri,
7737 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7738 );
7739 assert_eq!(params.options.tab_size, 4);
7740 Ok(Some(vec![lsp::TextEdit::new(
7741 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
7742 ", ".to_string(),
7743 )]))
7744 })
7745 .next()
7746 .await;
7747 cx.executor().start_waiting();
7748 save.await;
7749
7750 assert_eq!(
7751 editor.update(cx, |editor, cx| editor.text(cx)),
7752 "one, two\nthree\n"
7753 );
7754 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7755
7756 editor.update_in(cx, |editor, window, cx| {
7757 editor.set_text("one\ntwo\nthree\n", window, cx)
7758 });
7759 assert!(cx.read(|cx| editor.is_dirty(cx)));
7760
7761 // Ensure we can still save even if formatting hangs.
7762 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
7763 move |params, _| async move {
7764 assert_eq!(
7765 params.text_document.uri,
7766 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7767 );
7768 futures::future::pending::<()>().await;
7769 unreachable!()
7770 },
7771 );
7772 let save = editor
7773 .update_in(cx, |editor, window, cx| {
7774 editor.save(true, project.clone(), window, cx)
7775 })
7776 .unwrap();
7777 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
7778 cx.executor().start_waiting();
7779 save.await;
7780 assert_eq!(
7781 editor.update(cx, |editor, cx| editor.text(cx)),
7782 "one\ntwo\nthree\n"
7783 );
7784 assert!(!cx.read(|cx| editor.is_dirty(cx)));
7785
7786 // For non-dirty buffer, no formatting request should be sent
7787 let save = editor
7788 .update_in(cx, |editor, window, cx| {
7789 editor.save(true, project.clone(), window, cx)
7790 })
7791 .unwrap();
7792 let _pending_format_request = fake_server
7793 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
7794 panic!("Should not be invoked on non-dirty buffer");
7795 })
7796 .next();
7797 cx.executor().start_waiting();
7798 save.await;
7799
7800 // Set rust language override and assert overridden tabsize is sent to language server
7801 update_test_language_settings(cx, |settings| {
7802 settings.languages.insert(
7803 "Rust".into(),
7804 LanguageSettingsContent {
7805 tab_size: NonZeroU32::new(8),
7806 ..Default::default()
7807 },
7808 );
7809 });
7810
7811 editor.update_in(cx, |editor, window, cx| {
7812 editor.set_text("somehting_new\n", window, cx)
7813 });
7814 assert!(cx.read(|cx| editor.is_dirty(cx)));
7815 let save = editor
7816 .update_in(cx, |editor, window, cx| {
7817 editor.save(true, project.clone(), window, cx)
7818 })
7819 .unwrap();
7820 fake_server
7821 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
7822 assert_eq!(
7823 params.text_document.uri,
7824 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
7825 );
7826 assert_eq!(params.options.tab_size, 8);
7827 Ok(Some(vec![]))
7828 })
7829 .next()
7830 .await;
7831 cx.executor().start_waiting();
7832 save.await;
7833}
7834
7835#[gpui::test]
7836async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
7837 init_test(cx, |_| {});
7838
7839 let cols = 4;
7840 let rows = 10;
7841 let sample_text_1 = sample_text(rows, cols, 'a');
7842 assert_eq!(
7843 sample_text_1,
7844 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
7845 );
7846 let sample_text_2 = sample_text(rows, cols, 'l');
7847 assert_eq!(
7848 sample_text_2,
7849 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
7850 );
7851 let sample_text_3 = sample_text(rows, cols, 'v');
7852 assert_eq!(
7853 sample_text_3,
7854 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
7855 );
7856
7857 let fs = FakeFs::new(cx.executor());
7858 fs.insert_tree(
7859 path!("/a"),
7860 json!({
7861 "main.rs": sample_text_1,
7862 "other.rs": sample_text_2,
7863 "lib.rs": sample_text_3,
7864 }),
7865 )
7866 .await;
7867
7868 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
7869 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
7870 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
7871
7872 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
7873 language_registry.add(rust_lang());
7874 let mut fake_servers = language_registry.register_fake_lsp(
7875 "Rust",
7876 FakeLspAdapter {
7877 capabilities: lsp::ServerCapabilities {
7878 document_formatting_provider: Some(lsp::OneOf::Left(true)),
7879 ..Default::default()
7880 },
7881 ..Default::default()
7882 },
7883 );
7884
7885 let worktree = project.update(cx, |project, cx| {
7886 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
7887 assert_eq!(worktrees.len(), 1);
7888 worktrees.pop().unwrap()
7889 });
7890 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
7891
7892 let buffer_1 = project
7893 .update(cx, |project, cx| {
7894 project.open_buffer((worktree_id, "main.rs"), cx)
7895 })
7896 .await
7897 .unwrap();
7898 let buffer_2 = project
7899 .update(cx, |project, cx| {
7900 project.open_buffer((worktree_id, "other.rs"), cx)
7901 })
7902 .await
7903 .unwrap();
7904 let buffer_3 = project
7905 .update(cx, |project, cx| {
7906 project.open_buffer((worktree_id, "lib.rs"), cx)
7907 })
7908 .await
7909 .unwrap();
7910
7911 let multi_buffer = cx.new(|cx| {
7912 let mut multi_buffer = MultiBuffer::new(ReadWrite);
7913 multi_buffer.push_excerpts(
7914 buffer_1.clone(),
7915 [
7916 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7917 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7918 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7919 ],
7920 cx,
7921 );
7922 multi_buffer.push_excerpts(
7923 buffer_2.clone(),
7924 [
7925 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7926 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7927 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7928 ],
7929 cx,
7930 );
7931 multi_buffer.push_excerpts(
7932 buffer_3.clone(),
7933 [
7934 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
7935 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
7936 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
7937 ],
7938 cx,
7939 );
7940 multi_buffer
7941 });
7942 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
7943 Editor::new(
7944 EditorMode::Full,
7945 multi_buffer,
7946 Some(project.clone()),
7947 window,
7948 cx,
7949 )
7950 });
7951
7952 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7953 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7954 s.select_ranges(Some(1..2))
7955 });
7956 editor.insert("|one|two|three|", window, cx);
7957 });
7958 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7959 multi_buffer_editor.update_in(cx, |editor, window, cx| {
7960 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
7961 s.select_ranges(Some(60..70))
7962 });
7963 editor.insert("|four|five|six|", window, cx);
7964 });
7965 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
7966
7967 // First two buffers should be edited, but not the third one.
7968 assert_eq!(
7969 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
7970 "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}",
7971 );
7972 buffer_1.update(cx, |buffer, _| {
7973 assert!(buffer.is_dirty());
7974 assert_eq!(
7975 buffer.text(),
7976 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
7977 )
7978 });
7979 buffer_2.update(cx, |buffer, _| {
7980 assert!(buffer.is_dirty());
7981 assert_eq!(
7982 buffer.text(),
7983 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
7984 )
7985 });
7986 buffer_3.update(cx, |buffer, _| {
7987 assert!(!buffer.is_dirty());
7988 assert_eq!(buffer.text(), sample_text_3,)
7989 });
7990 cx.executor().run_until_parked();
7991
7992 cx.executor().start_waiting();
7993 let save = multi_buffer_editor
7994 .update_in(cx, |editor, window, cx| {
7995 editor.save(true, project.clone(), window, cx)
7996 })
7997 .unwrap();
7998
7999 let fake_server = fake_servers.next().await.unwrap();
8000 fake_server
8001 .server
8002 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8003 Ok(Some(vec![lsp::TextEdit::new(
8004 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8005 format!("[{} formatted]", params.text_document.uri),
8006 )]))
8007 })
8008 .detach();
8009 save.await;
8010
8011 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8012 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8013 assert_eq!(
8014 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8015 uri!(
8016 "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}"
8017 ),
8018 );
8019 buffer_1.update(cx, |buffer, _| {
8020 assert!(!buffer.is_dirty());
8021 assert_eq!(
8022 buffer.text(),
8023 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8024 )
8025 });
8026 buffer_2.update(cx, |buffer, _| {
8027 assert!(!buffer.is_dirty());
8028 assert_eq!(
8029 buffer.text(),
8030 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8031 )
8032 });
8033 buffer_3.update(cx, |buffer, _| {
8034 assert!(!buffer.is_dirty());
8035 assert_eq!(buffer.text(), sample_text_3,)
8036 });
8037}
8038
8039#[gpui::test]
8040async fn test_range_format_during_save(cx: &mut TestAppContext) {
8041 init_test(cx, |_| {});
8042
8043 let fs = FakeFs::new(cx.executor());
8044 fs.insert_file(path!("/file.rs"), Default::default()).await;
8045
8046 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8047
8048 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8049 language_registry.add(rust_lang());
8050 let mut fake_servers = language_registry.register_fake_lsp(
8051 "Rust",
8052 FakeLspAdapter {
8053 capabilities: lsp::ServerCapabilities {
8054 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8055 ..Default::default()
8056 },
8057 ..Default::default()
8058 },
8059 );
8060
8061 let buffer = project
8062 .update(cx, |project, cx| {
8063 project.open_local_buffer(path!("/file.rs"), cx)
8064 })
8065 .await
8066 .unwrap();
8067
8068 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8069 let (editor, cx) = cx.add_window_view(|window, cx| {
8070 build_editor_with_project(project.clone(), buffer, window, cx)
8071 });
8072 editor.update_in(cx, |editor, window, cx| {
8073 editor.set_text("one\ntwo\nthree\n", window, cx)
8074 });
8075 assert!(cx.read(|cx| editor.is_dirty(cx)));
8076
8077 cx.executor().start_waiting();
8078 let fake_server = fake_servers.next().await.unwrap();
8079
8080 let save = editor
8081 .update_in(cx, |editor, window, cx| {
8082 editor.save(true, project.clone(), window, cx)
8083 })
8084 .unwrap();
8085 fake_server
8086 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8087 assert_eq!(
8088 params.text_document.uri,
8089 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8090 );
8091 assert_eq!(params.options.tab_size, 4);
8092 Ok(Some(vec![lsp::TextEdit::new(
8093 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8094 ", ".to_string(),
8095 )]))
8096 })
8097 .next()
8098 .await;
8099 cx.executor().start_waiting();
8100 save.await;
8101 assert_eq!(
8102 editor.update(cx, |editor, cx| editor.text(cx)),
8103 "one, two\nthree\n"
8104 );
8105 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8106
8107 editor.update_in(cx, |editor, window, cx| {
8108 editor.set_text("one\ntwo\nthree\n", window, cx)
8109 });
8110 assert!(cx.read(|cx| editor.is_dirty(cx)));
8111
8112 // Ensure we can still save even if formatting hangs.
8113 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8114 move |params, _| async move {
8115 assert_eq!(
8116 params.text_document.uri,
8117 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8118 );
8119 futures::future::pending::<()>().await;
8120 unreachable!()
8121 },
8122 );
8123 let save = editor
8124 .update_in(cx, |editor, window, cx| {
8125 editor.save(true, project.clone(), window, cx)
8126 })
8127 .unwrap();
8128 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8129 cx.executor().start_waiting();
8130 save.await;
8131 assert_eq!(
8132 editor.update(cx, |editor, cx| editor.text(cx)),
8133 "one\ntwo\nthree\n"
8134 );
8135 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8136
8137 // For non-dirty buffer, no formatting request should be sent
8138 let save = editor
8139 .update_in(cx, |editor, window, cx| {
8140 editor.save(true, project.clone(), window, cx)
8141 })
8142 .unwrap();
8143 let _pending_format_request = fake_server
8144 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8145 panic!("Should not be invoked on non-dirty buffer");
8146 })
8147 .next();
8148 cx.executor().start_waiting();
8149 save.await;
8150
8151 // Set Rust language override and assert overridden tabsize is sent to language server
8152 update_test_language_settings(cx, |settings| {
8153 settings.languages.insert(
8154 "Rust".into(),
8155 LanguageSettingsContent {
8156 tab_size: NonZeroU32::new(8),
8157 ..Default::default()
8158 },
8159 );
8160 });
8161
8162 editor.update_in(cx, |editor, window, cx| {
8163 editor.set_text("somehting_new\n", window, cx)
8164 });
8165 assert!(cx.read(|cx| editor.is_dirty(cx)));
8166 let save = editor
8167 .update_in(cx, |editor, window, cx| {
8168 editor.save(true, project.clone(), window, cx)
8169 })
8170 .unwrap();
8171 fake_server
8172 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8173 assert_eq!(
8174 params.text_document.uri,
8175 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8176 );
8177 assert_eq!(params.options.tab_size, 8);
8178 Ok(Some(vec![]))
8179 })
8180 .next()
8181 .await;
8182 cx.executor().start_waiting();
8183 save.await;
8184}
8185
8186#[gpui::test]
8187async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8188 init_test(cx, |settings| {
8189 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8190 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8191 ))
8192 });
8193
8194 let fs = FakeFs::new(cx.executor());
8195 fs.insert_file(path!("/file.rs"), Default::default()).await;
8196
8197 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8198
8199 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8200 language_registry.add(Arc::new(Language::new(
8201 LanguageConfig {
8202 name: "Rust".into(),
8203 matcher: LanguageMatcher {
8204 path_suffixes: vec!["rs".to_string()],
8205 ..Default::default()
8206 },
8207 ..LanguageConfig::default()
8208 },
8209 Some(tree_sitter_rust::LANGUAGE.into()),
8210 )));
8211 update_test_language_settings(cx, |settings| {
8212 // Enable Prettier formatting for the same buffer, and ensure
8213 // LSP is called instead of Prettier.
8214 settings.defaults.prettier = Some(PrettierSettings {
8215 allowed: true,
8216 ..PrettierSettings::default()
8217 });
8218 });
8219 let mut fake_servers = language_registry.register_fake_lsp(
8220 "Rust",
8221 FakeLspAdapter {
8222 capabilities: lsp::ServerCapabilities {
8223 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8224 ..Default::default()
8225 },
8226 ..Default::default()
8227 },
8228 );
8229
8230 let buffer = project
8231 .update(cx, |project, cx| {
8232 project.open_local_buffer(path!("/file.rs"), cx)
8233 })
8234 .await
8235 .unwrap();
8236
8237 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8238 let (editor, cx) = cx.add_window_view(|window, cx| {
8239 build_editor_with_project(project.clone(), buffer, window, cx)
8240 });
8241 editor.update_in(cx, |editor, window, cx| {
8242 editor.set_text("one\ntwo\nthree\n", window, cx)
8243 });
8244
8245 cx.executor().start_waiting();
8246 let fake_server = fake_servers.next().await.unwrap();
8247
8248 let format = editor
8249 .update_in(cx, |editor, window, cx| {
8250 editor.perform_format(
8251 project.clone(),
8252 FormatTrigger::Manual,
8253 FormatTarget::Buffers,
8254 window,
8255 cx,
8256 )
8257 })
8258 .unwrap();
8259 fake_server
8260 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8261 assert_eq!(
8262 params.text_document.uri,
8263 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8264 );
8265 assert_eq!(params.options.tab_size, 4);
8266 Ok(Some(vec![lsp::TextEdit::new(
8267 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8268 ", ".to_string(),
8269 )]))
8270 })
8271 .next()
8272 .await;
8273 cx.executor().start_waiting();
8274 format.await;
8275 assert_eq!(
8276 editor.update(cx, |editor, cx| editor.text(cx)),
8277 "one, two\nthree\n"
8278 );
8279
8280 editor.update_in(cx, |editor, window, cx| {
8281 editor.set_text("one\ntwo\nthree\n", window, cx)
8282 });
8283 // Ensure we don't lock if formatting hangs.
8284 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8285 move |params, _| async move {
8286 assert_eq!(
8287 params.text_document.uri,
8288 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8289 );
8290 futures::future::pending::<()>().await;
8291 unreachable!()
8292 },
8293 );
8294 let format = editor
8295 .update_in(cx, |editor, window, cx| {
8296 editor.perform_format(
8297 project,
8298 FormatTrigger::Manual,
8299 FormatTarget::Buffers,
8300 window,
8301 cx,
8302 )
8303 })
8304 .unwrap();
8305 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8306 cx.executor().start_waiting();
8307 format.await;
8308 assert_eq!(
8309 editor.update(cx, |editor, cx| editor.text(cx)),
8310 "one\ntwo\nthree\n"
8311 );
8312}
8313
8314#[gpui::test]
8315async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
8316 init_test(cx, |settings| {
8317 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8318 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8319 ))
8320 });
8321
8322 let fs = FakeFs::new(cx.executor());
8323 fs.insert_file(path!("/file.ts"), Default::default()).await;
8324
8325 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8326
8327 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8328 language_registry.add(Arc::new(Language::new(
8329 LanguageConfig {
8330 name: "TypeScript".into(),
8331 matcher: LanguageMatcher {
8332 path_suffixes: vec!["ts".to_string()],
8333 ..Default::default()
8334 },
8335 ..LanguageConfig::default()
8336 },
8337 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
8338 )));
8339 update_test_language_settings(cx, |settings| {
8340 settings.defaults.prettier = Some(PrettierSettings {
8341 allowed: true,
8342 ..PrettierSettings::default()
8343 });
8344 });
8345 let mut fake_servers = language_registry.register_fake_lsp(
8346 "TypeScript",
8347 FakeLspAdapter {
8348 capabilities: lsp::ServerCapabilities {
8349 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8350 ..Default::default()
8351 },
8352 ..Default::default()
8353 },
8354 );
8355
8356 let buffer = project
8357 .update(cx, |project, cx| {
8358 project.open_local_buffer(path!("/file.ts"), cx)
8359 })
8360 .await
8361 .unwrap();
8362
8363 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8364 let (editor, cx) = cx.add_window_view(|window, cx| {
8365 build_editor_with_project(project.clone(), buffer, window, cx)
8366 });
8367 editor.update_in(cx, |editor, window, cx| {
8368 editor.set_text(
8369 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8370 window,
8371 cx,
8372 )
8373 });
8374
8375 cx.executor().start_waiting();
8376 let fake_server = fake_servers.next().await.unwrap();
8377
8378 let format = editor
8379 .update_in(cx, |editor, window, cx| {
8380 editor.perform_code_action_kind(
8381 project.clone(),
8382 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8383 window,
8384 cx,
8385 )
8386 })
8387 .unwrap();
8388 fake_server
8389 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
8390 assert_eq!(
8391 params.text_document.uri,
8392 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8393 );
8394 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
8395 lsp::CodeAction {
8396 title: "Organize Imports".to_string(),
8397 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
8398 edit: Some(lsp::WorkspaceEdit {
8399 changes: Some(
8400 [(
8401 params.text_document.uri.clone(),
8402 vec![lsp::TextEdit::new(
8403 lsp::Range::new(
8404 lsp::Position::new(1, 0),
8405 lsp::Position::new(2, 0),
8406 ),
8407 "".to_string(),
8408 )],
8409 )]
8410 .into_iter()
8411 .collect(),
8412 ),
8413 ..Default::default()
8414 }),
8415 ..Default::default()
8416 },
8417 )]))
8418 })
8419 .next()
8420 .await;
8421 cx.executor().start_waiting();
8422 format.await;
8423 assert_eq!(
8424 editor.update(cx, |editor, cx| editor.text(cx)),
8425 "import { a } from 'module';\n\nconst x = a;\n"
8426 );
8427
8428 editor.update_in(cx, |editor, window, cx| {
8429 editor.set_text(
8430 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
8431 window,
8432 cx,
8433 )
8434 });
8435 // Ensure we don't lock if code action hangs.
8436 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8437 move |params, _| async move {
8438 assert_eq!(
8439 params.text_document.uri,
8440 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
8441 );
8442 futures::future::pending::<()>().await;
8443 unreachable!()
8444 },
8445 );
8446 let format = editor
8447 .update_in(cx, |editor, window, cx| {
8448 editor.perform_code_action_kind(
8449 project,
8450 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
8451 window,
8452 cx,
8453 )
8454 })
8455 .unwrap();
8456 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
8457 cx.executor().start_waiting();
8458 format.await;
8459 assert_eq!(
8460 editor.update(cx, |editor, cx| editor.text(cx)),
8461 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
8462 );
8463}
8464
8465#[gpui::test]
8466async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
8467 init_test(cx, |_| {});
8468
8469 let mut cx = EditorLspTestContext::new_rust(
8470 lsp::ServerCapabilities {
8471 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8472 ..Default::default()
8473 },
8474 cx,
8475 )
8476 .await;
8477
8478 cx.set_state(indoc! {"
8479 one.twoˇ
8480 "});
8481
8482 // The format request takes a long time. When it completes, it inserts
8483 // a newline and an indent before the `.`
8484 cx.lsp
8485 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
8486 let executor = cx.background_executor().clone();
8487 async move {
8488 executor.timer(Duration::from_millis(100)).await;
8489 Ok(Some(vec![lsp::TextEdit {
8490 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
8491 new_text: "\n ".into(),
8492 }]))
8493 }
8494 });
8495
8496 // Submit a format request.
8497 let format_1 = cx
8498 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8499 .unwrap();
8500 cx.executor().run_until_parked();
8501
8502 // Submit a second format request.
8503 let format_2 = cx
8504 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8505 .unwrap();
8506 cx.executor().run_until_parked();
8507
8508 // Wait for both format requests to complete
8509 cx.executor().advance_clock(Duration::from_millis(200));
8510 cx.executor().start_waiting();
8511 format_1.await.unwrap();
8512 cx.executor().start_waiting();
8513 format_2.await.unwrap();
8514
8515 // The formatting edits only happens once.
8516 cx.assert_editor_state(indoc! {"
8517 one
8518 .twoˇ
8519 "});
8520}
8521
8522#[gpui::test]
8523async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
8524 init_test(cx, |settings| {
8525 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
8526 });
8527
8528 let mut cx = EditorLspTestContext::new_rust(
8529 lsp::ServerCapabilities {
8530 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8531 ..Default::default()
8532 },
8533 cx,
8534 )
8535 .await;
8536
8537 // Set up a buffer white some trailing whitespace and no trailing newline.
8538 cx.set_state(
8539 &[
8540 "one ", //
8541 "twoˇ", //
8542 "three ", //
8543 "four", //
8544 ]
8545 .join("\n"),
8546 );
8547
8548 // Submit a format request.
8549 let format = cx
8550 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
8551 .unwrap();
8552
8553 // Record which buffer changes have been sent to the language server
8554 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
8555 cx.lsp
8556 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
8557 let buffer_changes = buffer_changes.clone();
8558 move |params, _| {
8559 buffer_changes.lock().extend(
8560 params
8561 .content_changes
8562 .into_iter()
8563 .map(|e| (e.range.unwrap(), e.text)),
8564 );
8565 }
8566 });
8567
8568 // Handle formatting requests to the language server.
8569 cx.lsp
8570 .set_request_handler::<lsp::request::Formatting, _, _>({
8571 let buffer_changes = buffer_changes.clone();
8572 move |_, _| {
8573 // When formatting is requested, trailing whitespace has already been stripped,
8574 // and the trailing newline has already been added.
8575 assert_eq!(
8576 &buffer_changes.lock()[1..],
8577 &[
8578 (
8579 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
8580 "".into()
8581 ),
8582 (
8583 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
8584 "".into()
8585 ),
8586 (
8587 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
8588 "\n".into()
8589 ),
8590 ]
8591 );
8592
8593 // Insert blank lines between each line of the buffer.
8594 async move {
8595 Ok(Some(vec![
8596 lsp::TextEdit {
8597 range: lsp::Range::new(
8598 lsp::Position::new(1, 0),
8599 lsp::Position::new(1, 0),
8600 ),
8601 new_text: "\n".into(),
8602 },
8603 lsp::TextEdit {
8604 range: lsp::Range::new(
8605 lsp::Position::new(2, 0),
8606 lsp::Position::new(2, 0),
8607 ),
8608 new_text: "\n".into(),
8609 },
8610 ]))
8611 }
8612 }
8613 });
8614
8615 // After formatting the buffer, the trailing whitespace is stripped,
8616 // a newline is appended, and the edits provided by the language server
8617 // have been applied.
8618 format.await.unwrap();
8619 cx.assert_editor_state(
8620 &[
8621 "one", //
8622 "", //
8623 "twoˇ", //
8624 "", //
8625 "three", //
8626 "four", //
8627 "", //
8628 ]
8629 .join("\n"),
8630 );
8631
8632 // Undoing the formatting undoes the trailing whitespace removal, the
8633 // trailing newline, and the LSP edits.
8634 cx.update_buffer(|buffer, cx| buffer.undo(cx));
8635 cx.assert_editor_state(
8636 &[
8637 "one ", //
8638 "twoˇ", //
8639 "three ", //
8640 "four", //
8641 ]
8642 .join("\n"),
8643 );
8644}
8645
8646#[gpui::test]
8647async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
8648 cx: &mut TestAppContext,
8649) {
8650 init_test(cx, |_| {});
8651
8652 cx.update(|cx| {
8653 cx.update_global::<SettingsStore, _>(|settings, cx| {
8654 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8655 settings.auto_signature_help = Some(true);
8656 });
8657 });
8658 });
8659
8660 let mut cx = EditorLspTestContext::new_rust(
8661 lsp::ServerCapabilities {
8662 signature_help_provider: Some(lsp::SignatureHelpOptions {
8663 ..Default::default()
8664 }),
8665 ..Default::default()
8666 },
8667 cx,
8668 )
8669 .await;
8670
8671 let language = Language::new(
8672 LanguageConfig {
8673 name: "Rust".into(),
8674 brackets: BracketPairConfig {
8675 pairs: vec![
8676 BracketPair {
8677 start: "{".to_string(),
8678 end: "}".to_string(),
8679 close: true,
8680 surround: true,
8681 newline: true,
8682 },
8683 BracketPair {
8684 start: "(".to_string(),
8685 end: ")".to_string(),
8686 close: true,
8687 surround: true,
8688 newline: true,
8689 },
8690 BracketPair {
8691 start: "/*".to_string(),
8692 end: " */".to_string(),
8693 close: true,
8694 surround: true,
8695 newline: true,
8696 },
8697 BracketPair {
8698 start: "[".to_string(),
8699 end: "]".to_string(),
8700 close: false,
8701 surround: false,
8702 newline: true,
8703 },
8704 BracketPair {
8705 start: "\"".to_string(),
8706 end: "\"".to_string(),
8707 close: true,
8708 surround: true,
8709 newline: false,
8710 },
8711 BracketPair {
8712 start: "<".to_string(),
8713 end: ">".to_string(),
8714 close: false,
8715 surround: true,
8716 newline: true,
8717 },
8718 ],
8719 ..Default::default()
8720 },
8721 autoclose_before: "})]".to_string(),
8722 ..Default::default()
8723 },
8724 Some(tree_sitter_rust::LANGUAGE.into()),
8725 );
8726 let language = Arc::new(language);
8727
8728 cx.language_registry().add(language.clone());
8729 cx.update_buffer(|buffer, cx| {
8730 buffer.set_language(Some(language), cx);
8731 });
8732
8733 cx.set_state(
8734 &r#"
8735 fn main() {
8736 sampleˇ
8737 }
8738 "#
8739 .unindent(),
8740 );
8741
8742 cx.update_editor(|editor, window, cx| {
8743 editor.handle_input("(", window, cx);
8744 });
8745 cx.assert_editor_state(
8746 &"
8747 fn main() {
8748 sample(ˇ)
8749 }
8750 "
8751 .unindent(),
8752 );
8753
8754 let mocked_response = lsp::SignatureHelp {
8755 signatures: vec![lsp::SignatureInformation {
8756 label: "fn sample(param1: u8, param2: u8)".to_string(),
8757 documentation: None,
8758 parameters: Some(vec![
8759 lsp::ParameterInformation {
8760 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8761 documentation: None,
8762 },
8763 lsp::ParameterInformation {
8764 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8765 documentation: None,
8766 },
8767 ]),
8768 active_parameter: None,
8769 }],
8770 active_signature: Some(0),
8771 active_parameter: Some(0),
8772 };
8773 handle_signature_help_request(&mut cx, mocked_response).await;
8774
8775 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8776 .await;
8777
8778 cx.editor(|editor, _, _| {
8779 let signature_help_state = editor.signature_help_state.popover().cloned();
8780 assert_eq!(
8781 signature_help_state.unwrap().label,
8782 "param1: u8, param2: u8"
8783 );
8784 });
8785}
8786
8787#[gpui::test]
8788async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
8789 init_test(cx, |_| {});
8790
8791 cx.update(|cx| {
8792 cx.update_global::<SettingsStore, _>(|settings, cx| {
8793 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8794 settings.auto_signature_help = Some(false);
8795 settings.show_signature_help_after_edits = Some(false);
8796 });
8797 });
8798 });
8799
8800 let mut cx = EditorLspTestContext::new_rust(
8801 lsp::ServerCapabilities {
8802 signature_help_provider: Some(lsp::SignatureHelpOptions {
8803 ..Default::default()
8804 }),
8805 ..Default::default()
8806 },
8807 cx,
8808 )
8809 .await;
8810
8811 let language = Language::new(
8812 LanguageConfig {
8813 name: "Rust".into(),
8814 brackets: BracketPairConfig {
8815 pairs: vec![
8816 BracketPair {
8817 start: "{".to_string(),
8818 end: "}".to_string(),
8819 close: true,
8820 surround: true,
8821 newline: true,
8822 },
8823 BracketPair {
8824 start: "(".to_string(),
8825 end: ")".to_string(),
8826 close: true,
8827 surround: true,
8828 newline: true,
8829 },
8830 BracketPair {
8831 start: "/*".to_string(),
8832 end: " */".to_string(),
8833 close: true,
8834 surround: true,
8835 newline: true,
8836 },
8837 BracketPair {
8838 start: "[".to_string(),
8839 end: "]".to_string(),
8840 close: false,
8841 surround: false,
8842 newline: true,
8843 },
8844 BracketPair {
8845 start: "\"".to_string(),
8846 end: "\"".to_string(),
8847 close: true,
8848 surround: true,
8849 newline: false,
8850 },
8851 BracketPair {
8852 start: "<".to_string(),
8853 end: ">".to_string(),
8854 close: false,
8855 surround: true,
8856 newline: true,
8857 },
8858 ],
8859 ..Default::default()
8860 },
8861 autoclose_before: "})]".to_string(),
8862 ..Default::default()
8863 },
8864 Some(tree_sitter_rust::LANGUAGE.into()),
8865 );
8866 let language = Arc::new(language);
8867
8868 cx.language_registry().add(language.clone());
8869 cx.update_buffer(|buffer, cx| {
8870 buffer.set_language(Some(language), cx);
8871 });
8872
8873 // Ensure that signature_help is not called when no signature help is enabled.
8874 cx.set_state(
8875 &r#"
8876 fn main() {
8877 sampleˇ
8878 }
8879 "#
8880 .unindent(),
8881 );
8882 cx.update_editor(|editor, window, cx| {
8883 editor.handle_input("(", window, cx);
8884 });
8885 cx.assert_editor_state(
8886 &"
8887 fn main() {
8888 sample(ˇ)
8889 }
8890 "
8891 .unindent(),
8892 );
8893 cx.editor(|editor, _, _| {
8894 assert!(editor.signature_help_state.task().is_none());
8895 });
8896
8897 let mocked_response = lsp::SignatureHelp {
8898 signatures: vec![lsp::SignatureInformation {
8899 label: "fn sample(param1: u8, param2: u8)".to_string(),
8900 documentation: None,
8901 parameters: Some(vec![
8902 lsp::ParameterInformation {
8903 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
8904 documentation: None,
8905 },
8906 lsp::ParameterInformation {
8907 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
8908 documentation: None,
8909 },
8910 ]),
8911 active_parameter: None,
8912 }],
8913 active_signature: Some(0),
8914 active_parameter: Some(0),
8915 };
8916
8917 // Ensure that signature_help is called when enabled afte edits
8918 cx.update(|_, cx| {
8919 cx.update_global::<SettingsStore, _>(|settings, cx| {
8920 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8921 settings.auto_signature_help = Some(false);
8922 settings.show_signature_help_after_edits = Some(true);
8923 });
8924 });
8925 });
8926 cx.set_state(
8927 &r#"
8928 fn main() {
8929 sampleˇ
8930 }
8931 "#
8932 .unindent(),
8933 );
8934 cx.update_editor(|editor, window, cx| {
8935 editor.handle_input("(", window, cx);
8936 });
8937 cx.assert_editor_state(
8938 &"
8939 fn main() {
8940 sample(ˇ)
8941 }
8942 "
8943 .unindent(),
8944 );
8945 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
8946 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8947 .await;
8948 cx.update_editor(|editor, _, _| {
8949 let signature_help_state = editor.signature_help_state.popover().cloned();
8950 assert!(signature_help_state.is_some());
8951 assert_eq!(
8952 signature_help_state.unwrap().label,
8953 "param1: u8, param2: u8"
8954 );
8955 editor.signature_help_state = SignatureHelpState::default();
8956 });
8957
8958 // Ensure that signature_help is called when auto signature help override is enabled
8959 cx.update(|_, cx| {
8960 cx.update_global::<SettingsStore, _>(|settings, cx| {
8961 settings.update_user_settings::<EditorSettings>(cx, |settings| {
8962 settings.auto_signature_help = Some(true);
8963 settings.show_signature_help_after_edits = Some(false);
8964 });
8965 });
8966 });
8967 cx.set_state(
8968 &r#"
8969 fn main() {
8970 sampleˇ
8971 }
8972 "#
8973 .unindent(),
8974 );
8975 cx.update_editor(|editor, window, cx| {
8976 editor.handle_input("(", window, cx);
8977 });
8978 cx.assert_editor_state(
8979 &"
8980 fn main() {
8981 sample(ˇ)
8982 }
8983 "
8984 .unindent(),
8985 );
8986 handle_signature_help_request(&mut cx, mocked_response).await;
8987 cx.condition(|editor, _| editor.signature_help_state.is_shown())
8988 .await;
8989 cx.editor(|editor, _, _| {
8990 let signature_help_state = editor.signature_help_state.popover().cloned();
8991 assert!(signature_help_state.is_some());
8992 assert_eq!(
8993 signature_help_state.unwrap().label,
8994 "param1: u8, param2: u8"
8995 );
8996 });
8997}
8998
8999#[gpui::test]
9000async fn test_signature_help(cx: &mut TestAppContext) {
9001 init_test(cx, |_| {});
9002 cx.update(|cx| {
9003 cx.update_global::<SettingsStore, _>(|settings, cx| {
9004 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9005 settings.auto_signature_help = Some(true);
9006 });
9007 });
9008 });
9009
9010 let mut cx = EditorLspTestContext::new_rust(
9011 lsp::ServerCapabilities {
9012 signature_help_provider: Some(lsp::SignatureHelpOptions {
9013 ..Default::default()
9014 }),
9015 ..Default::default()
9016 },
9017 cx,
9018 )
9019 .await;
9020
9021 // A test that directly calls `show_signature_help`
9022 cx.update_editor(|editor, window, cx| {
9023 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9024 });
9025
9026 let mocked_response = lsp::SignatureHelp {
9027 signatures: vec![lsp::SignatureInformation {
9028 label: "fn sample(param1: u8, param2: u8)".to_string(),
9029 documentation: None,
9030 parameters: Some(vec![
9031 lsp::ParameterInformation {
9032 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9033 documentation: None,
9034 },
9035 lsp::ParameterInformation {
9036 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9037 documentation: None,
9038 },
9039 ]),
9040 active_parameter: None,
9041 }],
9042 active_signature: Some(0),
9043 active_parameter: Some(0),
9044 };
9045 handle_signature_help_request(&mut cx, mocked_response).await;
9046
9047 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9048 .await;
9049
9050 cx.editor(|editor, _, _| {
9051 let signature_help_state = editor.signature_help_state.popover().cloned();
9052 assert!(signature_help_state.is_some());
9053 assert_eq!(
9054 signature_help_state.unwrap().label,
9055 "param1: u8, param2: u8"
9056 );
9057 });
9058
9059 // When exiting outside from inside the brackets, `signature_help` is closed.
9060 cx.set_state(indoc! {"
9061 fn main() {
9062 sample(ˇ);
9063 }
9064
9065 fn sample(param1: u8, param2: u8) {}
9066 "});
9067
9068 cx.update_editor(|editor, window, cx| {
9069 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9070 });
9071
9072 let mocked_response = lsp::SignatureHelp {
9073 signatures: Vec::new(),
9074 active_signature: None,
9075 active_parameter: None,
9076 };
9077 handle_signature_help_request(&mut cx, mocked_response).await;
9078
9079 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9080 .await;
9081
9082 cx.editor(|editor, _, _| {
9083 assert!(!editor.signature_help_state.is_shown());
9084 });
9085
9086 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9087 cx.set_state(indoc! {"
9088 fn main() {
9089 sample(ˇ);
9090 }
9091
9092 fn sample(param1: u8, param2: u8) {}
9093 "});
9094
9095 let mocked_response = lsp::SignatureHelp {
9096 signatures: vec![lsp::SignatureInformation {
9097 label: "fn sample(param1: u8, param2: u8)".to_string(),
9098 documentation: None,
9099 parameters: Some(vec![
9100 lsp::ParameterInformation {
9101 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9102 documentation: None,
9103 },
9104 lsp::ParameterInformation {
9105 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9106 documentation: None,
9107 },
9108 ]),
9109 active_parameter: None,
9110 }],
9111 active_signature: Some(0),
9112 active_parameter: Some(0),
9113 };
9114 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9115 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9116 .await;
9117 cx.editor(|editor, _, _| {
9118 assert!(editor.signature_help_state.is_shown());
9119 });
9120
9121 // Restore the popover with more parameter input
9122 cx.set_state(indoc! {"
9123 fn main() {
9124 sample(param1, param2ˇ);
9125 }
9126
9127 fn sample(param1: u8, param2: u8) {}
9128 "});
9129
9130 let mocked_response = lsp::SignatureHelp {
9131 signatures: vec![lsp::SignatureInformation {
9132 label: "fn sample(param1: u8, param2: u8)".to_string(),
9133 documentation: None,
9134 parameters: Some(vec![
9135 lsp::ParameterInformation {
9136 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9137 documentation: None,
9138 },
9139 lsp::ParameterInformation {
9140 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9141 documentation: None,
9142 },
9143 ]),
9144 active_parameter: None,
9145 }],
9146 active_signature: Some(0),
9147 active_parameter: Some(1),
9148 };
9149 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9150 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9151 .await;
9152
9153 // When selecting a range, the popover is gone.
9154 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9155 cx.update_editor(|editor, window, cx| {
9156 editor.change_selections(None, window, cx, |s| {
9157 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9158 })
9159 });
9160 cx.assert_editor_state(indoc! {"
9161 fn main() {
9162 sample(param1, «ˇparam2»);
9163 }
9164
9165 fn sample(param1: u8, param2: u8) {}
9166 "});
9167 cx.editor(|editor, _, _| {
9168 assert!(!editor.signature_help_state.is_shown());
9169 });
9170
9171 // When unselecting again, the popover is back if within the brackets.
9172 cx.update_editor(|editor, window, cx| {
9173 editor.change_selections(None, window, cx, |s| {
9174 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9175 })
9176 });
9177 cx.assert_editor_state(indoc! {"
9178 fn main() {
9179 sample(param1, ˇparam2);
9180 }
9181
9182 fn sample(param1: u8, param2: u8) {}
9183 "});
9184 handle_signature_help_request(&mut cx, mocked_response).await;
9185 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9186 .await;
9187 cx.editor(|editor, _, _| {
9188 assert!(editor.signature_help_state.is_shown());
9189 });
9190
9191 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
9192 cx.update_editor(|editor, window, cx| {
9193 editor.change_selections(None, window, cx, |s| {
9194 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
9195 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9196 })
9197 });
9198 cx.assert_editor_state(indoc! {"
9199 fn main() {
9200 sample(param1, ˇparam2);
9201 }
9202
9203 fn sample(param1: u8, param2: u8) {}
9204 "});
9205
9206 let mocked_response = lsp::SignatureHelp {
9207 signatures: vec![lsp::SignatureInformation {
9208 label: "fn sample(param1: u8, param2: u8)".to_string(),
9209 documentation: None,
9210 parameters: Some(vec![
9211 lsp::ParameterInformation {
9212 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9213 documentation: None,
9214 },
9215 lsp::ParameterInformation {
9216 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9217 documentation: None,
9218 },
9219 ]),
9220 active_parameter: None,
9221 }],
9222 active_signature: Some(0),
9223 active_parameter: Some(1),
9224 };
9225 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9226 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9227 .await;
9228 cx.update_editor(|editor, _, cx| {
9229 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
9230 });
9231 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9232 .await;
9233 cx.update_editor(|editor, window, cx| {
9234 editor.change_selections(None, window, cx, |s| {
9235 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9236 })
9237 });
9238 cx.assert_editor_state(indoc! {"
9239 fn main() {
9240 sample(param1, «ˇparam2»);
9241 }
9242
9243 fn sample(param1: u8, param2: u8) {}
9244 "});
9245 cx.update_editor(|editor, window, cx| {
9246 editor.change_selections(None, window, cx, |s| {
9247 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9248 })
9249 });
9250 cx.assert_editor_state(indoc! {"
9251 fn main() {
9252 sample(param1, ˇparam2);
9253 }
9254
9255 fn sample(param1: u8, param2: u8) {}
9256 "});
9257 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
9258 .await;
9259}
9260
9261#[gpui::test]
9262async fn test_completion_mode(cx: &mut TestAppContext) {
9263 init_test(cx, |_| {});
9264 let mut cx = EditorLspTestContext::new_rust(
9265 lsp::ServerCapabilities {
9266 completion_provider: Some(lsp::CompletionOptions {
9267 resolve_provider: Some(true),
9268 ..Default::default()
9269 }),
9270 ..Default::default()
9271 },
9272 cx,
9273 )
9274 .await;
9275
9276 struct Run {
9277 run_description: &'static str,
9278 initial_state: String,
9279 buffer_marked_text: String,
9280 completion_text: &'static str,
9281 expected_with_insert_mode: String,
9282 expected_with_replace_mode: String,
9283 expected_with_replace_subsequence_mode: String,
9284 expected_with_replace_suffix_mode: String,
9285 }
9286
9287 let runs = [
9288 Run {
9289 run_description: "Start of word matches completion text",
9290 initial_state: "before ediˇ after".into(),
9291 buffer_marked_text: "before <edi|> after".into(),
9292 completion_text: "editor",
9293 expected_with_insert_mode: "before editorˇ after".into(),
9294 expected_with_replace_mode: "before editorˇ after".into(),
9295 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9296 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9297 },
9298 Run {
9299 run_description: "Accept same text at the middle of the word",
9300 initial_state: "before ediˇtor after".into(),
9301 buffer_marked_text: "before <edi|tor> after".into(),
9302 completion_text: "editor",
9303 expected_with_insert_mode: "before editorˇtor after".into(),
9304 expected_with_replace_mode: "before ediˇtor after".into(),
9305 expected_with_replace_subsequence_mode: "before ediˇtor after".into(),
9306 expected_with_replace_suffix_mode: "before ediˇtor after".into(),
9307 },
9308 Run {
9309 run_description: "End of word matches completion text -- cursor at end",
9310 initial_state: "before torˇ after".into(),
9311 buffer_marked_text: "before <tor|> after".into(),
9312 completion_text: "editor",
9313 expected_with_insert_mode: "before editorˇ after".into(),
9314 expected_with_replace_mode: "before editorˇ after".into(),
9315 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9316 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9317 },
9318 Run {
9319 run_description: "End of word matches completion text -- cursor at start",
9320 initial_state: "before ˇtor after".into(),
9321 buffer_marked_text: "before <|tor> after".into(),
9322 completion_text: "editor",
9323 expected_with_insert_mode: "before editorˇtor after".into(),
9324 expected_with_replace_mode: "before editorˇ after".into(),
9325 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
9326 expected_with_replace_suffix_mode: "before editorˇ after".into(),
9327 },
9328 Run {
9329 run_description: "Prepend text containing whitespace",
9330 initial_state: "pˇfield: bool".into(),
9331 buffer_marked_text: "<p|field>: bool".into(),
9332 completion_text: "pub ",
9333 expected_with_insert_mode: "pub ˇfield: bool".into(),
9334 expected_with_replace_mode: "pub ˇ: bool".into(),
9335 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
9336 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
9337 },
9338 Run {
9339 run_description: "Add element to start of list",
9340 initial_state: "[element_ˇelement_2]".into(),
9341 buffer_marked_text: "[<element_|element_2>]".into(),
9342 completion_text: "element_1",
9343 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
9344 expected_with_replace_mode: "[element_1ˇ]".into(),
9345 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
9346 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
9347 },
9348 Run {
9349 run_description: "Add element to start of list -- first and second elements are equal",
9350 initial_state: "[elˇelement]".into(),
9351 buffer_marked_text: "[<el|element>]".into(),
9352 completion_text: "element",
9353 expected_with_insert_mode: "[elementˇelement]".into(),
9354 expected_with_replace_mode: "[elˇement]".into(),
9355 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
9356 expected_with_replace_suffix_mode: "[elˇement]".into(),
9357 },
9358 Run {
9359 run_description: "Ends with matching suffix",
9360 initial_state: "SubˇError".into(),
9361 buffer_marked_text: "<Sub|Error>".into(),
9362 completion_text: "SubscriptionError",
9363 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
9364 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9365 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9366 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
9367 },
9368 Run {
9369 run_description: "Suffix is a subsequence -- contiguous",
9370 initial_state: "SubˇErr".into(),
9371 buffer_marked_text: "<Sub|Err>".into(),
9372 completion_text: "SubscriptionError",
9373 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
9374 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9375 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9376 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
9377 },
9378 Run {
9379 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
9380 initial_state: "Suˇscrirr".into(),
9381 buffer_marked_text: "<Su|scrirr>".into(),
9382 completion_text: "SubscriptionError",
9383 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
9384 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
9385 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
9386 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
9387 },
9388 Run {
9389 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
9390 initial_state: "foo(indˇix)".into(),
9391 buffer_marked_text: "foo(<ind|ix>)".into(),
9392 completion_text: "node_index",
9393 expected_with_insert_mode: "foo(node_indexˇix)".into(),
9394 expected_with_replace_mode: "foo(node_indexˇ)".into(),
9395 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
9396 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
9397 },
9398 ];
9399
9400 for run in runs {
9401 let run_variations = [
9402 (LspInsertMode::Insert, run.expected_with_insert_mode),
9403 (LspInsertMode::Replace, run.expected_with_replace_mode),
9404 (
9405 LspInsertMode::ReplaceSubsequence,
9406 run.expected_with_replace_subsequence_mode,
9407 ),
9408 (
9409 LspInsertMode::ReplaceSuffix,
9410 run.expected_with_replace_suffix_mode,
9411 ),
9412 ];
9413
9414 for (lsp_insert_mode, expected_text) in run_variations {
9415 eprintln!(
9416 "run = {:?}, mode = {lsp_insert_mode:.?}",
9417 run.run_description,
9418 );
9419
9420 update_test_language_settings(&mut cx, |settings| {
9421 settings.defaults.completions = Some(CompletionSettings {
9422 lsp_insert_mode,
9423 words: WordsCompletionMode::Disabled,
9424 lsp: true,
9425 lsp_fetch_timeout_ms: 0,
9426 });
9427 });
9428
9429 cx.set_state(&run.initial_state);
9430 cx.update_editor(|editor, window, cx| {
9431 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9432 });
9433
9434 let counter = Arc::new(AtomicUsize::new(0));
9435 handle_completion_request_with_insert_and_replace(
9436 &mut cx,
9437 &run.buffer_marked_text,
9438 vec![run.completion_text],
9439 counter.clone(),
9440 )
9441 .await;
9442 cx.condition(|editor, _| editor.context_menu_visible())
9443 .await;
9444 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9445
9446 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9447 editor
9448 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9449 .unwrap()
9450 });
9451 cx.assert_editor_state(&expected_text);
9452 handle_resolve_completion_request(&mut cx, None).await;
9453 apply_additional_edits.await.unwrap();
9454 }
9455 }
9456}
9457
9458#[gpui::test]
9459async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
9460 init_test(cx, |_| {});
9461 let mut cx = EditorLspTestContext::new_rust(
9462 lsp::ServerCapabilities {
9463 completion_provider: Some(lsp::CompletionOptions {
9464 resolve_provider: Some(true),
9465 ..Default::default()
9466 }),
9467 ..Default::default()
9468 },
9469 cx,
9470 )
9471 .await;
9472
9473 let initial_state = "SubˇError";
9474 let buffer_marked_text = "<Sub|Error>";
9475 let completion_text = "SubscriptionError";
9476 let expected_with_insert_mode = "SubscriptionErrorˇError";
9477 let expected_with_replace_mode = "SubscriptionErrorˇ";
9478
9479 update_test_language_settings(&mut cx, |settings| {
9480 settings.defaults.completions = Some(CompletionSettings {
9481 words: WordsCompletionMode::Disabled,
9482 // set the opposite here to ensure that the action is overriding the default behavior
9483 lsp_insert_mode: LspInsertMode::Insert,
9484 lsp: true,
9485 lsp_fetch_timeout_ms: 0,
9486 });
9487 });
9488
9489 cx.set_state(initial_state);
9490 cx.update_editor(|editor, window, cx| {
9491 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9492 });
9493
9494 let counter = Arc::new(AtomicUsize::new(0));
9495 handle_completion_request_with_insert_and_replace(
9496 &mut cx,
9497 &buffer_marked_text,
9498 vec![completion_text],
9499 counter.clone(),
9500 )
9501 .await;
9502 cx.condition(|editor, _| editor.context_menu_visible())
9503 .await;
9504 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9505
9506 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9507 editor
9508 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
9509 .unwrap()
9510 });
9511 cx.assert_editor_state(&expected_with_replace_mode);
9512 handle_resolve_completion_request(&mut cx, None).await;
9513 apply_additional_edits.await.unwrap();
9514
9515 update_test_language_settings(&mut cx, |settings| {
9516 settings.defaults.completions = Some(CompletionSettings {
9517 words: WordsCompletionMode::Disabled,
9518 // set the opposite here to ensure that the action is overriding the default behavior
9519 lsp_insert_mode: LspInsertMode::Replace,
9520 lsp: true,
9521 lsp_fetch_timeout_ms: 0,
9522 });
9523 });
9524
9525 cx.set_state(initial_state);
9526 cx.update_editor(|editor, window, cx| {
9527 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9528 });
9529 handle_completion_request_with_insert_and_replace(
9530 &mut cx,
9531 &buffer_marked_text,
9532 vec![completion_text],
9533 counter.clone(),
9534 )
9535 .await;
9536 cx.condition(|editor, _| editor.context_menu_visible())
9537 .await;
9538 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9539
9540 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9541 editor
9542 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
9543 .unwrap()
9544 });
9545 cx.assert_editor_state(&expected_with_insert_mode);
9546 handle_resolve_completion_request(&mut cx, None).await;
9547 apply_additional_edits.await.unwrap();
9548}
9549
9550#[gpui::test]
9551async fn test_completion(cx: &mut TestAppContext) {
9552 init_test(cx, |_| {});
9553
9554 let mut cx = EditorLspTestContext::new_rust(
9555 lsp::ServerCapabilities {
9556 completion_provider: Some(lsp::CompletionOptions {
9557 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9558 resolve_provider: Some(true),
9559 ..Default::default()
9560 }),
9561 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9562 ..Default::default()
9563 },
9564 cx,
9565 )
9566 .await;
9567 let counter = Arc::new(AtomicUsize::new(0));
9568
9569 cx.set_state(indoc! {"
9570 oneˇ
9571 two
9572 three
9573 "});
9574 cx.simulate_keystroke(".");
9575 handle_completion_request(
9576 &mut cx,
9577 indoc! {"
9578 one.|<>
9579 two
9580 three
9581 "},
9582 vec!["first_completion", "second_completion"],
9583 counter.clone(),
9584 )
9585 .await;
9586 cx.condition(|editor, _| editor.context_menu_visible())
9587 .await;
9588 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
9589
9590 let _handler = handle_signature_help_request(
9591 &mut cx,
9592 lsp::SignatureHelp {
9593 signatures: vec![lsp::SignatureInformation {
9594 label: "test signature".to_string(),
9595 documentation: None,
9596 parameters: Some(vec![lsp::ParameterInformation {
9597 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
9598 documentation: None,
9599 }]),
9600 active_parameter: None,
9601 }],
9602 active_signature: None,
9603 active_parameter: None,
9604 },
9605 );
9606 cx.update_editor(|editor, window, cx| {
9607 assert!(
9608 !editor.signature_help_state.is_shown(),
9609 "No signature help was called for"
9610 );
9611 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9612 });
9613 cx.run_until_parked();
9614 cx.update_editor(|editor, _, _| {
9615 assert!(
9616 !editor.signature_help_state.is_shown(),
9617 "No signature help should be shown when completions menu is open"
9618 );
9619 });
9620
9621 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9622 editor.context_menu_next(&Default::default(), window, cx);
9623 editor
9624 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9625 .unwrap()
9626 });
9627 cx.assert_editor_state(indoc! {"
9628 one.second_completionˇ
9629 two
9630 three
9631 "});
9632
9633 handle_resolve_completion_request(
9634 &mut cx,
9635 Some(vec![
9636 (
9637 //This overlaps with the primary completion edit which is
9638 //misbehavior from the LSP spec, test that we filter it out
9639 indoc! {"
9640 one.second_ˇcompletion
9641 two
9642 threeˇ
9643 "},
9644 "overlapping additional edit",
9645 ),
9646 (
9647 indoc! {"
9648 one.second_completion
9649 two
9650 threeˇ
9651 "},
9652 "\nadditional edit",
9653 ),
9654 ]),
9655 )
9656 .await;
9657 apply_additional_edits.await.unwrap();
9658 cx.assert_editor_state(indoc! {"
9659 one.second_completionˇ
9660 two
9661 three
9662 additional edit
9663 "});
9664
9665 cx.set_state(indoc! {"
9666 one.second_completion
9667 twoˇ
9668 threeˇ
9669 additional edit
9670 "});
9671 cx.simulate_keystroke(" ");
9672 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9673 cx.simulate_keystroke("s");
9674 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9675
9676 cx.assert_editor_state(indoc! {"
9677 one.second_completion
9678 two sˇ
9679 three sˇ
9680 additional edit
9681 "});
9682 handle_completion_request(
9683 &mut cx,
9684 indoc! {"
9685 one.second_completion
9686 two s
9687 three <s|>
9688 additional edit
9689 "},
9690 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9691 counter.clone(),
9692 )
9693 .await;
9694 cx.condition(|editor, _| editor.context_menu_visible())
9695 .await;
9696 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
9697
9698 cx.simulate_keystroke("i");
9699
9700 handle_completion_request(
9701 &mut cx,
9702 indoc! {"
9703 one.second_completion
9704 two si
9705 three <si|>
9706 additional edit
9707 "},
9708 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
9709 counter.clone(),
9710 )
9711 .await;
9712 cx.condition(|editor, _| editor.context_menu_visible())
9713 .await;
9714 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
9715
9716 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9717 editor
9718 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9719 .unwrap()
9720 });
9721 cx.assert_editor_state(indoc! {"
9722 one.second_completion
9723 two sixth_completionˇ
9724 three sixth_completionˇ
9725 additional edit
9726 "});
9727
9728 apply_additional_edits.await.unwrap();
9729
9730 update_test_language_settings(&mut cx, |settings| {
9731 settings.defaults.show_completions_on_input = Some(false);
9732 });
9733 cx.set_state("editorˇ");
9734 cx.simulate_keystroke(".");
9735 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9736 cx.simulate_keystrokes("c l o");
9737 cx.assert_editor_state("editor.cloˇ");
9738 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
9739 cx.update_editor(|editor, window, cx| {
9740 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
9741 });
9742 handle_completion_request(
9743 &mut cx,
9744 "editor.<clo|>",
9745 vec!["close", "clobber"],
9746 counter.clone(),
9747 )
9748 .await;
9749 cx.condition(|editor, _| editor.context_menu_visible())
9750 .await;
9751 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
9752
9753 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
9754 editor
9755 .confirm_completion(&ConfirmCompletion::default(), window, cx)
9756 .unwrap()
9757 });
9758 cx.assert_editor_state("editor.closeˇ");
9759 handle_resolve_completion_request(&mut cx, None).await;
9760 apply_additional_edits.await.unwrap();
9761}
9762
9763#[gpui::test]
9764async fn test_word_completion(cx: &mut TestAppContext) {
9765 let lsp_fetch_timeout_ms = 10;
9766 init_test(cx, |language_settings| {
9767 language_settings.defaults.completions = Some(CompletionSettings {
9768 words: WordsCompletionMode::Fallback,
9769 lsp: true,
9770 lsp_fetch_timeout_ms: 10,
9771 lsp_insert_mode: LspInsertMode::Insert,
9772 });
9773 });
9774
9775 let mut cx = EditorLspTestContext::new_rust(
9776 lsp::ServerCapabilities {
9777 completion_provider: Some(lsp::CompletionOptions {
9778 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9779 ..lsp::CompletionOptions::default()
9780 }),
9781 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9782 ..lsp::ServerCapabilities::default()
9783 },
9784 cx,
9785 )
9786 .await;
9787
9788 let throttle_completions = Arc::new(AtomicBool::new(false));
9789
9790 let lsp_throttle_completions = throttle_completions.clone();
9791 let _completion_requests_handler =
9792 cx.lsp
9793 .server
9794 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
9795 let lsp_throttle_completions = lsp_throttle_completions.clone();
9796 let cx = cx.clone();
9797 async move {
9798 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
9799 cx.background_executor()
9800 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
9801 .await;
9802 }
9803 Ok(Some(lsp::CompletionResponse::Array(vec![
9804 lsp::CompletionItem {
9805 label: "first".into(),
9806 ..lsp::CompletionItem::default()
9807 },
9808 lsp::CompletionItem {
9809 label: "last".into(),
9810 ..lsp::CompletionItem::default()
9811 },
9812 ])))
9813 }
9814 });
9815
9816 cx.set_state(indoc! {"
9817 oneˇ
9818 two
9819 three
9820 "});
9821 cx.simulate_keystroke(".");
9822 cx.executor().run_until_parked();
9823 cx.condition(|editor, _| editor.context_menu_visible())
9824 .await;
9825 cx.update_editor(|editor, window, cx| {
9826 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9827 {
9828 assert_eq!(
9829 completion_menu_entries(&menu),
9830 &["first", "last"],
9831 "When LSP server is fast to reply, no fallback word completions are used"
9832 );
9833 } else {
9834 panic!("expected completion menu to be open");
9835 }
9836 editor.cancel(&Cancel, window, cx);
9837 });
9838 cx.executor().run_until_parked();
9839 cx.condition(|editor, _| !editor.context_menu_visible())
9840 .await;
9841
9842 throttle_completions.store(true, atomic::Ordering::Release);
9843 cx.simulate_keystroke(".");
9844 cx.executor()
9845 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
9846 cx.executor().run_until_parked();
9847 cx.condition(|editor, _| editor.context_menu_visible())
9848 .await;
9849 cx.update_editor(|editor, _, _| {
9850 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9851 {
9852 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
9853 "When LSP server is slow, document words can be shown instead, if configured accordingly");
9854 } else {
9855 panic!("expected completion menu to be open");
9856 }
9857 });
9858}
9859
9860#[gpui::test]
9861async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
9862 init_test(cx, |language_settings| {
9863 language_settings.defaults.completions = Some(CompletionSettings {
9864 words: WordsCompletionMode::Enabled,
9865 lsp: true,
9866 lsp_fetch_timeout_ms: 0,
9867 lsp_insert_mode: LspInsertMode::Insert,
9868 });
9869 });
9870
9871 let mut cx = EditorLspTestContext::new_rust(
9872 lsp::ServerCapabilities {
9873 completion_provider: Some(lsp::CompletionOptions {
9874 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9875 ..lsp::CompletionOptions::default()
9876 }),
9877 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9878 ..lsp::ServerCapabilities::default()
9879 },
9880 cx,
9881 )
9882 .await;
9883
9884 let _completion_requests_handler =
9885 cx.lsp
9886 .server
9887 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9888 Ok(Some(lsp::CompletionResponse::Array(vec![
9889 lsp::CompletionItem {
9890 label: "first".into(),
9891 ..lsp::CompletionItem::default()
9892 },
9893 lsp::CompletionItem {
9894 label: "last".into(),
9895 ..lsp::CompletionItem::default()
9896 },
9897 ])))
9898 });
9899
9900 cx.set_state(indoc! {"ˇ
9901 first
9902 last
9903 second
9904 "});
9905 cx.simulate_keystroke(".");
9906 cx.executor().run_until_parked();
9907 cx.condition(|editor, _| editor.context_menu_visible())
9908 .await;
9909 cx.update_editor(|editor, _, _| {
9910 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9911 {
9912 assert_eq!(
9913 completion_menu_entries(&menu),
9914 &["first", "last", "second"],
9915 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
9916 );
9917 } else {
9918 panic!("expected completion menu to be open");
9919 }
9920 });
9921}
9922
9923#[gpui::test]
9924async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
9925 init_test(cx, |language_settings| {
9926 language_settings.defaults.completions = Some(CompletionSettings {
9927 words: WordsCompletionMode::Disabled,
9928 lsp: true,
9929 lsp_fetch_timeout_ms: 0,
9930 lsp_insert_mode: LspInsertMode::Insert,
9931 });
9932 });
9933
9934 let mut cx = EditorLspTestContext::new_rust(
9935 lsp::ServerCapabilities {
9936 completion_provider: Some(lsp::CompletionOptions {
9937 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
9938 ..lsp::CompletionOptions::default()
9939 }),
9940 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
9941 ..lsp::ServerCapabilities::default()
9942 },
9943 cx,
9944 )
9945 .await;
9946
9947 let _completion_requests_handler =
9948 cx.lsp
9949 .server
9950 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
9951 panic!("LSP completions should not be queried when dealing with word completions")
9952 });
9953
9954 cx.set_state(indoc! {"ˇ
9955 first
9956 last
9957 second
9958 "});
9959 cx.update_editor(|editor, window, cx| {
9960 editor.show_word_completions(&ShowWordCompletions, window, cx);
9961 });
9962 cx.executor().run_until_parked();
9963 cx.condition(|editor, _| editor.context_menu_visible())
9964 .await;
9965 cx.update_editor(|editor, _, _| {
9966 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9967 {
9968 assert_eq!(
9969 completion_menu_entries(&menu),
9970 &["first", "last", "second"],
9971 "`ShowWordCompletions` action should show word completions"
9972 );
9973 } else {
9974 panic!("expected completion menu to be open");
9975 }
9976 });
9977
9978 cx.simulate_keystroke("l");
9979 cx.executor().run_until_parked();
9980 cx.condition(|editor, _| editor.context_menu_visible())
9981 .await;
9982 cx.update_editor(|editor, _, _| {
9983 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
9984 {
9985 assert_eq!(
9986 completion_menu_entries(&menu),
9987 &["last"],
9988 "After showing word completions, further editing should filter them and not query the LSP"
9989 );
9990 } else {
9991 panic!("expected completion menu to be open");
9992 }
9993 });
9994}
9995
9996#[gpui::test]
9997async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
9998 init_test(cx, |language_settings| {
9999 language_settings.defaults.completions = Some(CompletionSettings {
10000 words: WordsCompletionMode::Fallback,
10001 lsp: false,
10002 lsp_fetch_timeout_ms: 0,
10003 lsp_insert_mode: LspInsertMode::Insert,
10004 });
10005 });
10006
10007 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
10008
10009 cx.set_state(indoc! {"ˇ
10010 0_usize
10011 let
10012 33
10013 4.5f32
10014 "});
10015 cx.update_editor(|editor, window, cx| {
10016 editor.show_completions(&ShowCompletions::default(), window, cx);
10017 });
10018 cx.executor().run_until_parked();
10019 cx.condition(|editor, _| editor.context_menu_visible())
10020 .await;
10021 cx.update_editor(|editor, window, cx| {
10022 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10023 {
10024 assert_eq!(
10025 completion_menu_entries(&menu),
10026 &["let"],
10027 "With no digits in the completion query, no digits should be in the word completions"
10028 );
10029 } else {
10030 panic!("expected completion menu to be open");
10031 }
10032 editor.cancel(&Cancel, window, cx);
10033 });
10034
10035 cx.set_state(indoc! {"3ˇ
10036 0_usize
10037 let
10038 3
10039 33.35f32
10040 "});
10041 cx.update_editor(|editor, window, cx| {
10042 editor.show_completions(&ShowCompletions::default(), window, cx);
10043 });
10044 cx.executor().run_until_parked();
10045 cx.condition(|editor, _| editor.context_menu_visible())
10046 .await;
10047 cx.update_editor(|editor, _, _| {
10048 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10049 {
10050 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
10051 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
10052 } else {
10053 panic!("expected completion menu to be open");
10054 }
10055 });
10056}
10057
10058fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
10059 let position = || lsp::Position {
10060 line: params.text_document_position.position.line,
10061 character: params.text_document_position.position.character,
10062 };
10063 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10064 range: lsp::Range {
10065 start: position(),
10066 end: position(),
10067 },
10068 new_text: text.to_string(),
10069 }))
10070}
10071
10072#[gpui::test]
10073async fn test_multiline_completion(cx: &mut TestAppContext) {
10074 init_test(cx, |_| {});
10075
10076 let fs = FakeFs::new(cx.executor());
10077 fs.insert_tree(
10078 path!("/a"),
10079 json!({
10080 "main.ts": "a",
10081 }),
10082 )
10083 .await;
10084
10085 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10086 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10087 let typescript_language = Arc::new(Language::new(
10088 LanguageConfig {
10089 name: "TypeScript".into(),
10090 matcher: LanguageMatcher {
10091 path_suffixes: vec!["ts".to_string()],
10092 ..LanguageMatcher::default()
10093 },
10094 line_comments: vec!["// ".into()],
10095 ..LanguageConfig::default()
10096 },
10097 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
10098 ));
10099 language_registry.add(typescript_language.clone());
10100 let mut fake_servers = language_registry.register_fake_lsp(
10101 "TypeScript",
10102 FakeLspAdapter {
10103 capabilities: lsp::ServerCapabilities {
10104 completion_provider: Some(lsp::CompletionOptions {
10105 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10106 ..lsp::CompletionOptions::default()
10107 }),
10108 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10109 ..lsp::ServerCapabilities::default()
10110 },
10111 // Emulate vtsls label generation
10112 label_for_completion: Some(Box::new(|item, _| {
10113 let text = if let Some(description) = item
10114 .label_details
10115 .as_ref()
10116 .and_then(|label_details| label_details.description.as_ref())
10117 {
10118 format!("{} {}", item.label, description)
10119 } else if let Some(detail) = &item.detail {
10120 format!("{} {}", item.label, detail)
10121 } else {
10122 item.label.clone()
10123 };
10124 let len = text.len();
10125 Some(language::CodeLabel {
10126 text,
10127 runs: Vec::new(),
10128 filter_range: 0..len,
10129 })
10130 })),
10131 ..FakeLspAdapter::default()
10132 },
10133 );
10134 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10135 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10136 let worktree_id = workspace
10137 .update(cx, |workspace, _window, cx| {
10138 workspace.project().update(cx, |project, cx| {
10139 project.worktrees(cx).next().unwrap().read(cx).id()
10140 })
10141 })
10142 .unwrap();
10143 let _buffer = project
10144 .update(cx, |project, cx| {
10145 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
10146 })
10147 .await
10148 .unwrap();
10149 let editor = workspace
10150 .update(cx, |workspace, window, cx| {
10151 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
10152 })
10153 .unwrap()
10154 .await
10155 .unwrap()
10156 .downcast::<Editor>()
10157 .unwrap();
10158 let fake_server = fake_servers.next().await.unwrap();
10159
10160 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
10161 let multiline_label_2 = "a\nb\nc\n";
10162 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
10163 let multiline_description = "d\ne\nf\n";
10164 let multiline_detail_2 = "g\nh\ni\n";
10165
10166 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
10167 move |params, _| async move {
10168 Ok(Some(lsp::CompletionResponse::Array(vec![
10169 lsp::CompletionItem {
10170 label: multiline_label.to_string(),
10171 text_edit: gen_text_edit(¶ms, "new_text_1"),
10172 ..lsp::CompletionItem::default()
10173 },
10174 lsp::CompletionItem {
10175 label: "single line label 1".to_string(),
10176 detail: Some(multiline_detail.to_string()),
10177 text_edit: gen_text_edit(¶ms, "new_text_2"),
10178 ..lsp::CompletionItem::default()
10179 },
10180 lsp::CompletionItem {
10181 label: "single line label 2".to_string(),
10182 label_details: Some(lsp::CompletionItemLabelDetails {
10183 description: Some(multiline_description.to_string()),
10184 detail: None,
10185 }),
10186 text_edit: gen_text_edit(¶ms, "new_text_2"),
10187 ..lsp::CompletionItem::default()
10188 },
10189 lsp::CompletionItem {
10190 label: multiline_label_2.to_string(),
10191 detail: Some(multiline_detail_2.to_string()),
10192 text_edit: gen_text_edit(¶ms, "new_text_3"),
10193 ..lsp::CompletionItem::default()
10194 },
10195 lsp::CompletionItem {
10196 label: "Label with many spaces and \t but without newlines".to_string(),
10197 detail: Some(
10198 "Details with many spaces and \t but without newlines".to_string(),
10199 ),
10200 text_edit: gen_text_edit(¶ms, "new_text_4"),
10201 ..lsp::CompletionItem::default()
10202 },
10203 ])))
10204 },
10205 );
10206
10207 editor.update_in(cx, |editor, window, cx| {
10208 cx.focus_self(window);
10209 editor.move_to_end(&MoveToEnd, window, cx);
10210 editor.handle_input(".", window, cx);
10211 });
10212 cx.run_until_parked();
10213 completion_handle.next().await.unwrap();
10214
10215 editor.update(cx, |editor, _| {
10216 assert!(editor.context_menu_visible());
10217 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10218 {
10219 let completion_labels = menu
10220 .completions
10221 .borrow()
10222 .iter()
10223 .map(|c| c.label.text.clone())
10224 .collect::<Vec<_>>();
10225 assert_eq!(
10226 completion_labels,
10227 &[
10228 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
10229 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
10230 "single line label 2 d e f ",
10231 "a b c g h i ",
10232 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
10233 ],
10234 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
10235 );
10236
10237 for completion in menu
10238 .completions
10239 .borrow()
10240 .iter() {
10241 assert_eq!(
10242 completion.label.filter_range,
10243 0..completion.label.text.len(),
10244 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
10245 );
10246 }
10247 } else {
10248 panic!("expected completion menu to be open");
10249 }
10250 });
10251}
10252
10253#[gpui::test]
10254async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
10255 init_test(cx, |_| {});
10256 let mut cx = EditorLspTestContext::new_rust(
10257 lsp::ServerCapabilities {
10258 completion_provider: Some(lsp::CompletionOptions {
10259 trigger_characters: Some(vec![".".to_string()]),
10260 ..Default::default()
10261 }),
10262 ..Default::default()
10263 },
10264 cx,
10265 )
10266 .await;
10267 cx.lsp
10268 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10269 Ok(Some(lsp::CompletionResponse::Array(vec![
10270 lsp::CompletionItem {
10271 label: "first".into(),
10272 ..Default::default()
10273 },
10274 lsp::CompletionItem {
10275 label: "last".into(),
10276 ..Default::default()
10277 },
10278 ])))
10279 });
10280 cx.set_state("variableˇ");
10281 cx.simulate_keystroke(".");
10282 cx.executor().run_until_parked();
10283
10284 cx.update_editor(|editor, _, _| {
10285 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10286 {
10287 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
10288 } else {
10289 panic!("expected completion menu to be open");
10290 }
10291 });
10292
10293 cx.update_editor(|editor, window, cx| {
10294 editor.move_page_down(&MovePageDown::default(), window, cx);
10295 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10296 {
10297 assert!(
10298 menu.selected_item == 1,
10299 "expected PageDown to select the last item from the context menu"
10300 );
10301 } else {
10302 panic!("expected completion menu to stay open after PageDown");
10303 }
10304 });
10305
10306 cx.update_editor(|editor, window, cx| {
10307 editor.move_page_up(&MovePageUp::default(), window, cx);
10308 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10309 {
10310 assert!(
10311 menu.selected_item == 0,
10312 "expected PageUp to select the first item from the context menu"
10313 );
10314 } else {
10315 panic!("expected completion menu to stay open after PageUp");
10316 }
10317 });
10318}
10319
10320#[gpui::test]
10321async fn test_completion_sort(cx: &mut TestAppContext) {
10322 init_test(cx, |_| {});
10323 let mut cx = EditorLspTestContext::new_rust(
10324 lsp::ServerCapabilities {
10325 completion_provider: Some(lsp::CompletionOptions {
10326 trigger_characters: Some(vec![".".to_string()]),
10327 ..Default::default()
10328 }),
10329 ..Default::default()
10330 },
10331 cx,
10332 )
10333 .await;
10334 cx.lsp
10335 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10336 Ok(Some(lsp::CompletionResponse::Array(vec![
10337 lsp::CompletionItem {
10338 label: "Range".into(),
10339 sort_text: Some("a".into()),
10340 ..Default::default()
10341 },
10342 lsp::CompletionItem {
10343 label: "r".into(),
10344 sort_text: Some("b".into()),
10345 ..Default::default()
10346 },
10347 lsp::CompletionItem {
10348 label: "ret".into(),
10349 sort_text: Some("c".into()),
10350 ..Default::default()
10351 },
10352 lsp::CompletionItem {
10353 label: "return".into(),
10354 sort_text: Some("d".into()),
10355 ..Default::default()
10356 },
10357 lsp::CompletionItem {
10358 label: "slice".into(),
10359 sort_text: Some("d".into()),
10360 ..Default::default()
10361 },
10362 ])))
10363 });
10364 cx.set_state("rˇ");
10365 cx.executor().run_until_parked();
10366 cx.update_editor(|editor, window, cx| {
10367 editor.show_completions(
10368 &ShowCompletions {
10369 trigger: Some("r".into()),
10370 },
10371 window,
10372 cx,
10373 );
10374 });
10375 cx.executor().run_until_parked();
10376
10377 cx.update_editor(|editor, _, _| {
10378 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
10379 {
10380 assert_eq!(
10381 completion_menu_entries(&menu),
10382 &["r", "ret", "Range", "return"]
10383 );
10384 } else {
10385 panic!("expected completion menu to be open");
10386 }
10387 });
10388}
10389
10390#[gpui::test]
10391async fn test_as_is_completions(cx: &mut TestAppContext) {
10392 init_test(cx, |_| {});
10393 let mut cx = EditorLspTestContext::new_rust(
10394 lsp::ServerCapabilities {
10395 completion_provider: Some(lsp::CompletionOptions {
10396 ..Default::default()
10397 }),
10398 ..Default::default()
10399 },
10400 cx,
10401 )
10402 .await;
10403 cx.lsp
10404 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10405 Ok(Some(lsp::CompletionResponse::Array(vec![
10406 lsp::CompletionItem {
10407 label: "unsafe".into(),
10408 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10409 range: lsp::Range {
10410 start: lsp::Position {
10411 line: 1,
10412 character: 2,
10413 },
10414 end: lsp::Position {
10415 line: 1,
10416 character: 3,
10417 },
10418 },
10419 new_text: "unsafe".to_string(),
10420 })),
10421 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
10422 ..Default::default()
10423 },
10424 ])))
10425 });
10426 cx.set_state("fn a() {}\n nˇ");
10427 cx.executor().run_until_parked();
10428 cx.update_editor(|editor, window, cx| {
10429 editor.show_completions(
10430 &ShowCompletions {
10431 trigger: Some("\n".into()),
10432 },
10433 window,
10434 cx,
10435 );
10436 });
10437 cx.executor().run_until_parked();
10438
10439 cx.update_editor(|editor, window, cx| {
10440 editor.confirm_completion(&Default::default(), window, cx)
10441 });
10442 cx.executor().run_until_parked();
10443 cx.assert_editor_state("fn a() {}\n unsafeˇ");
10444}
10445
10446#[gpui::test]
10447async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
10448 init_test(cx, |_| {});
10449
10450 let mut cx = EditorLspTestContext::new_rust(
10451 lsp::ServerCapabilities {
10452 completion_provider: Some(lsp::CompletionOptions {
10453 trigger_characters: Some(vec![".".to_string()]),
10454 resolve_provider: Some(true),
10455 ..Default::default()
10456 }),
10457 ..Default::default()
10458 },
10459 cx,
10460 )
10461 .await;
10462
10463 cx.set_state("fn main() { let a = 2ˇ; }");
10464 cx.simulate_keystroke(".");
10465 let completion_item = lsp::CompletionItem {
10466 label: "Some".into(),
10467 kind: Some(lsp::CompletionItemKind::SNIPPET),
10468 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
10469 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
10470 kind: lsp::MarkupKind::Markdown,
10471 value: "```rust\nSome(2)\n```".to_string(),
10472 })),
10473 deprecated: Some(false),
10474 sort_text: Some("Some".to_string()),
10475 filter_text: Some("Some".to_string()),
10476 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
10477 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
10478 range: lsp::Range {
10479 start: lsp::Position {
10480 line: 0,
10481 character: 22,
10482 },
10483 end: lsp::Position {
10484 line: 0,
10485 character: 22,
10486 },
10487 },
10488 new_text: "Some(2)".to_string(),
10489 })),
10490 additional_text_edits: Some(vec![lsp::TextEdit {
10491 range: lsp::Range {
10492 start: lsp::Position {
10493 line: 0,
10494 character: 20,
10495 },
10496 end: lsp::Position {
10497 line: 0,
10498 character: 22,
10499 },
10500 },
10501 new_text: "".to_string(),
10502 }]),
10503 ..Default::default()
10504 };
10505
10506 let closure_completion_item = completion_item.clone();
10507 let counter = Arc::new(AtomicUsize::new(0));
10508 let counter_clone = counter.clone();
10509 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
10510 let task_completion_item = closure_completion_item.clone();
10511 counter_clone.fetch_add(1, atomic::Ordering::Release);
10512 async move {
10513 Ok(Some(lsp::CompletionResponse::Array(vec![
10514 task_completion_item,
10515 ])))
10516 }
10517 });
10518
10519 cx.condition(|editor, _| editor.context_menu_visible())
10520 .await;
10521 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
10522 assert!(request.next().await.is_some());
10523 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10524
10525 cx.simulate_keystrokes("S o m");
10526 cx.condition(|editor, _| editor.context_menu_visible())
10527 .await;
10528 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
10529 assert!(request.next().await.is_some());
10530 assert!(request.next().await.is_some());
10531 assert!(request.next().await.is_some());
10532 request.close();
10533 assert!(request.next().await.is_none());
10534 assert_eq!(
10535 counter.load(atomic::Ordering::Acquire),
10536 4,
10537 "With the completions menu open, only one LSP request should happen per input"
10538 );
10539}
10540
10541#[gpui::test]
10542async fn test_toggle_comment(cx: &mut TestAppContext) {
10543 init_test(cx, |_| {});
10544 let mut cx = EditorTestContext::new(cx).await;
10545 let language = Arc::new(Language::new(
10546 LanguageConfig {
10547 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10548 ..Default::default()
10549 },
10550 Some(tree_sitter_rust::LANGUAGE.into()),
10551 ));
10552 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10553
10554 // If multiple selections intersect a line, the line is only toggled once.
10555 cx.set_state(indoc! {"
10556 fn a() {
10557 «//b();
10558 ˇ»// «c();
10559 //ˇ» d();
10560 }
10561 "});
10562
10563 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10564
10565 cx.assert_editor_state(indoc! {"
10566 fn a() {
10567 «b();
10568 c();
10569 ˇ» d();
10570 }
10571 "});
10572
10573 // The comment prefix is inserted at the same column for every line in a
10574 // selection.
10575 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10576
10577 cx.assert_editor_state(indoc! {"
10578 fn a() {
10579 // «b();
10580 // c();
10581 ˇ»// d();
10582 }
10583 "});
10584
10585 // If a selection ends at the beginning of a line, that line is not toggled.
10586 cx.set_selections_state(indoc! {"
10587 fn a() {
10588 // b();
10589 «// c();
10590 ˇ» // d();
10591 }
10592 "});
10593
10594 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10595
10596 cx.assert_editor_state(indoc! {"
10597 fn a() {
10598 // b();
10599 «c();
10600 ˇ» // d();
10601 }
10602 "});
10603
10604 // If a selection span a single line and is empty, the line is toggled.
10605 cx.set_state(indoc! {"
10606 fn a() {
10607 a();
10608 b();
10609 ˇ
10610 }
10611 "});
10612
10613 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10614
10615 cx.assert_editor_state(indoc! {"
10616 fn a() {
10617 a();
10618 b();
10619 //•ˇ
10620 }
10621 "});
10622
10623 // If a selection span multiple lines, empty lines are not toggled.
10624 cx.set_state(indoc! {"
10625 fn a() {
10626 «a();
10627
10628 c();ˇ»
10629 }
10630 "});
10631
10632 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10633
10634 cx.assert_editor_state(indoc! {"
10635 fn a() {
10636 // «a();
10637
10638 // c();ˇ»
10639 }
10640 "});
10641
10642 // If a selection includes multiple comment prefixes, all lines are uncommented.
10643 cx.set_state(indoc! {"
10644 fn a() {
10645 «// a();
10646 /// b();
10647 //! c();ˇ»
10648 }
10649 "});
10650
10651 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
10652
10653 cx.assert_editor_state(indoc! {"
10654 fn a() {
10655 «a();
10656 b();
10657 c();ˇ»
10658 }
10659 "});
10660}
10661
10662#[gpui::test]
10663async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
10664 init_test(cx, |_| {});
10665 let mut cx = EditorTestContext::new(cx).await;
10666 let language = Arc::new(Language::new(
10667 LanguageConfig {
10668 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
10669 ..Default::default()
10670 },
10671 Some(tree_sitter_rust::LANGUAGE.into()),
10672 ));
10673 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
10674
10675 let toggle_comments = &ToggleComments {
10676 advance_downwards: false,
10677 ignore_indent: true,
10678 };
10679
10680 // If multiple selections intersect a line, the line is only toggled once.
10681 cx.set_state(indoc! {"
10682 fn a() {
10683 // «b();
10684 // c();
10685 // ˇ» d();
10686 }
10687 "});
10688
10689 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10690
10691 cx.assert_editor_state(indoc! {"
10692 fn a() {
10693 «b();
10694 c();
10695 ˇ» d();
10696 }
10697 "});
10698
10699 // The comment prefix is inserted at the beginning of each line
10700 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10701
10702 cx.assert_editor_state(indoc! {"
10703 fn a() {
10704 // «b();
10705 // c();
10706 // ˇ» d();
10707 }
10708 "});
10709
10710 // If a selection ends at the beginning of a line, that line is not toggled.
10711 cx.set_selections_state(indoc! {"
10712 fn a() {
10713 // b();
10714 // «c();
10715 ˇ»// d();
10716 }
10717 "});
10718
10719 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10720
10721 cx.assert_editor_state(indoc! {"
10722 fn a() {
10723 // b();
10724 «c();
10725 ˇ»// d();
10726 }
10727 "});
10728
10729 // If a selection span a single line and is empty, the line is toggled.
10730 cx.set_state(indoc! {"
10731 fn a() {
10732 a();
10733 b();
10734 ˇ
10735 }
10736 "});
10737
10738 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10739
10740 cx.assert_editor_state(indoc! {"
10741 fn a() {
10742 a();
10743 b();
10744 //ˇ
10745 }
10746 "});
10747
10748 // If a selection span multiple lines, empty lines are not toggled.
10749 cx.set_state(indoc! {"
10750 fn a() {
10751 «a();
10752
10753 c();ˇ»
10754 }
10755 "});
10756
10757 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10758
10759 cx.assert_editor_state(indoc! {"
10760 fn a() {
10761 // «a();
10762
10763 // c();ˇ»
10764 }
10765 "});
10766
10767 // If a selection includes multiple comment prefixes, all lines are uncommented.
10768 cx.set_state(indoc! {"
10769 fn a() {
10770 // «a();
10771 /// b();
10772 //! c();ˇ»
10773 }
10774 "});
10775
10776 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
10777
10778 cx.assert_editor_state(indoc! {"
10779 fn a() {
10780 «a();
10781 b();
10782 c();ˇ»
10783 }
10784 "});
10785}
10786
10787#[gpui::test]
10788async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
10789 init_test(cx, |_| {});
10790
10791 let language = Arc::new(Language::new(
10792 LanguageConfig {
10793 line_comments: vec!["// ".into()],
10794 ..Default::default()
10795 },
10796 Some(tree_sitter_rust::LANGUAGE.into()),
10797 ));
10798
10799 let mut cx = EditorTestContext::new(cx).await;
10800
10801 cx.language_registry().add(language.clone());
10802 cx.update_buffer(|buffer, cx| {
10803 buffer.set_language(Some(language), cx);
10804 });
10805
10806 let toggle_comments = &ToggleComments {
10807 advance_downwards: true,
10808 ignore_indent: false,
10809 };
10810
10811 // Single cursor on one line -> advance
10812 // Cursor moves horizontally 3 characters as well on non-blank line
10813 cx.set_state(indoc!(
10814 "fn a() {
10815 ˇdog();
10816 cat();
10817 }"
10818 ));
10819 cx.update_editor(|editor, window, cx| {
10820 editor.toggle_comments(toggle_comments, window, cx);
10821 });
10822 cx.assert_editor_state(indoc!(
10823 "fn a() {
10824 // dog();
10825 catˇ();
10826 }"
10827 ));
10828
10829 // Single selection on one line -> don't advance
10830 cx.set_state(indoc!(
10831 "fn a() {
10832 «dog()ˇ»;
10833 cat();
10834 }"
10835 ));
10836 cx.update_editor(|editor, window, cx| {
10837 editor.toggle_comments(toggle_comments, window, cx);
10838 });
10839 cx.assert_editor_state(indoc!(
10840 "fn a() {
10841 // «dog()ˇ»;
10842 cat();
10843 }"
10844 ));
10845
10846 // Multiple cursors on one line -> advance
10847 cx.set_state(indoc!(
10848 "fn a() {
10849 ˇdˇog();
10850 cat();
10851 }"
10852 ));
10853 cx.update_editor(|editor, window, cx| {
10854 editor.toggle_comments(toggle_comments, window, cx);
10855 });
10856 cx.assert_editor_state(indoc!(
10857 "fn a() {
10858 // dog();
10859 catˇ(ˇ);
10860 }"
10861 ));
10862
10863 // Multiple cursors on one line, with selection -> don't advance
10864 cx.set_state(indoc!(
10865 "fn a() {
10866 ˇdˇog«()ˇ»;
10867 cat();
10868 }"
10869 ));
10870 cx.update_editor(|editor, window, cx| {
10871 editor.toggle_comments(toggle_comments, window, cx);
10872 });
10873 cx.assert_editor_state(indoc!(
10874 "fn a() {
10875 // ˇdˇog«()ˇ»;
10876 cat();
10877 }"
10878 ));
10879
10880 // Single cursor on one line -> advance
10881 // Cursor moves to column 0 on blank line
10882 cx.set_state(indoc!(
10883 "fn a() {
10884 ˇdog();
10885
10886 cat();
10887 }"
10888 ));
10889 cx.update_editor(|editor, window, cx| {
10890 editor.toggle_comments(toggle_comments, window, cx);
10891 });
10892 cx.assert_editor_state(indoc!(
10893 "fn a() {
10894 // dog();
10895 ˇ
10896 cat();
10897 }"
10898 ));
10899
10900 // Single cursor on one line -> advance
10901 // Cursor starts and ends at column 0
10902 cx.set_state(indoc!(
10903 "fn a() {
10904 ˇ dog();
10905 cat();
10906 }"
10907 ));
10908 cx.update_editor(|editor, window, cx| {
10909 editor.toggle_comments(toggle_comments, window, cx);
10910 });
10911 cx.assert_editor_state(indoc!(
10912 "fn a() {
10913 // dog();
10914 ˇ cat();
10915 }"
10916 ));
10917}
10918
10919#[gpui::test]
10920async fn test_toggle_block_comment(cx: &mut TestAppContext) {
10921 init_test(cx, |_| {});
10922
10923 let mut cx = EditorTestContext::new(cx).await;
10924
10925 let html_language = Arc::new(
10926 Language::new(
10927 LanguageConfig {
10928 name: "HTML".into(),
10929 block_comment: Some(("<!-- ".into(), " -->".into())),
10930 ..Default::default()
10931 },
10932 Some(tree_sitter_html::LANGUAGE.into()),
10933 )
10934 .with_injection_query(
10935 r#"
10936 (script_element
10937 (raw_text) @injection.content
10938 (#set! injection.language "javascript"))
10939 "#,
10940 )
10941 .unwrap(),
10942 );
10943
10944 let javascript_language = Arc::new(Language::new(
10945 LanguageConfig {
10946 name: "JavaScript".into(),
10947 line_comments: vec!["// ".into()],
10948 ..Default::default()
10949 },
10950 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10951 ));
10952
10953 cx.language_registry().add(html_language.clone());
10954 cx.language_registry().add(javascript_language.clone());
10955 cx.update_buffer(|buffer, cx| {
10956 buffer.set_language(Some(html_language), cx);
10957 });
10958
10959 // Toggle comments for empty selections
10960 cx.set_state(
10961 &r#"
10962 <p>A</p>ˇ
10963 <p>B</p>ˇ
10964 <p>C</p>ˇ
10965 "#
10966 .unindent(),
10967 );
10968 cx.update_editor(|editor, window, cx| {
10969 editor.toggle_comments(&ToggleComments::default(), window, cx)
10970 });
10971 cx.assert_editor_state(
10972 &r#"
10973 <!-- <p>A</p>ˇ -->
10974 <!-- <p>B</p>ˇ -->
10975 <!-- <p>C</p>ˇ -->
10976 "#
10977 .unindent(),
10978 );
10979 cx.update_editor(|editor, window, cx| {
10980 editor.toggle_comments(&ToggleComments::default(), window, cx)
10981 });
10982 cx.assert_editor_state(
10983 &r#"
10984 <p>A</p>ˇ
10985 <p>B</p>ˇ
10986 <p>C</p>ˇ
10987 "#
10988 .unindent(),
10989 );
10990
10991 // Toggle comments for mixture of empty and non-empty selections, where
10992 // multiple selections occupy a given line.
10993 cx.set_state(
10994 &r#"
10995 <p>A«</p>
10996 <p>ˇ»B</p>ˇ
10997 <p>C«</p>
10998 <p>ˇ»D</p>ˇ
10999 "#
11000 .unindent(),
11001 );
11002
11003 cx.update_editor(|editor, window, cx| {
11004 editor.toggle_comments(&ToggleComments::default(), window, cx)
11005 });
11006 cx.assert_editor_state(
11007 &r#"
11008 <!-- <p>A«</p>
11009 <p>ˇ»B</p>ˇ -->
11010 <!-- <p>C«</p>
11011 <p>ˇ»D</p>ˇ -->
11012 "#
11013 .unindent(),
11014 );
11015 cx.update_editor(|editor, window, cx| {
11016 editor.toggle_comments(&ToggleComments::default(), window, cx)
11017 });
11018 cx.assert_editor_state(
11019 &r#"
11020 <p>A«</p>
11021 <p>ˇ»B</p>ˇ
11022 <p>C«</p>
11023 <p>ˇ»D</p>ˇ
11024 "#
11025 .unindent(),
11026 );
11027
11028 // Toggle comments when different languages are active for different
11029 // selections.
11030 cx.set_state(
11031 &r#"
11032 ˇ<script>
11033 ˇvar x = new Y();
11034 ˇ</script>
11035 "#
11036 .unindent(),
11037 );
11038 cx.executor().run_until_parked();
11039 cx.update_editor(|editor, window, cx| {
11040 editor.toggle_comments(&ToggleComments::default(), window, cx)
11041 });
11042 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
11043 // Uncommenting and commenting from this position brings in even more wrong artifacts.
11044 cx.assert_editor_state(
11045 &r#"
11046 <!-- ˇ<script> -->
11047 // ˇvar x = new Y();
11048 <!-- ˇ</script> -->
11049 "#
11050 .unindent(),
11051 );
11052}
11053
11054#[gpui::test]
11055fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
11056 init_test(cx, |_| {});
11057
11058 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11059 let multibuffer = cx.new(|cx| {
11060 let mut multibuffer = MultiBuffer::new(ReadWrite);
11061 multibuffer.push_excerpts(
11062 buffer.clone(),
11063 [
11064 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
11065 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
11066 ],
11067 cx,
11068 );
11069 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
11070 multibuffer
11071 });
11072
11073 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11074 editor.update_in(cx, |editor, window, cx| {
11075 assert_eq!(editor.text(cx), "aaaa\nbbbb");
11076 editor.change_selections(None, window, cx, |s| {
11077 s.select_ranges([
11078 Point::new(0, 0)..Point::new(0, 0),
11079 Point::new(1, 0)..Point::new(1, 0),
11080 ])
11081 });
11082
11083 editor.handle_input("X", window, cx);
11084 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
11085 assert_eq!(
11086 editor.selections.ranges(cx),
11087 [
11088 Point::new(0, 1)..Point::new(0, 1),
11089 Point::new(1, 1)..Point::new(1, 1),
11090 ]
11091 );
11092
11093 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
11094 editor.change_selections(None, window, cx, |s| {
11095 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
11096 });
11097 editor.backspace(&Default::default(), window, cx);
11098 assert_eq!(editor.text(cx), "Xa\nbbb");
11099 assert_eq!(
11100 editor.selections.ranges(cx),
11101 [Point::new(1, 0)..Point::new(1, 0)]
11102 );
11103
11104 editor.change_selections(None, window, cx, |s| {
11105 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
11106 });
11107 editor.backspace(&Default::default(), window, cx);
11108 assert_eq!(editor.text(cx), "X\nbb");
11109 assert_eq!(
11110 editor.selections.ranges(cx),
11111 [Point::new(0, 1)..Point::new(0, 1)]
11112 );
11113 });
11114}
11115
11116#[gpui::test]
11117fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
11118 init_test(cx, |_| {});
11119
11120 let markers = vec![('[', ']').into(), ('(', ')').into()];
11121 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
11122 indoc! {"
11123 [aaaa
11124 (bbbb]
11125 cccc)",
11126 },
11127 markers.clone(),
11128 );
11129 let excerpt_ranges = markers.into_iter().map(|marker| {
11130 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
11131 ExcerptRange::new(context.clone())
11132 });
11133 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
11134 let multibuffer = cx.new(|cx| {
11135 let mut multibuffer = MultiBuffer::new(ReadWrite);
11136 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
11137 multibuffer
11138 });
11139
11140 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
11141 editor.update_in(cx, |editor, window, cx| {
11142 let (expected_text, selection_ranges) = marked_text_ranges(
11143 indoc! {"
11144 aaaa
11145 bˇbbb
11146 bˇbbˇb
11147 cccc"
11148 },
11149 true,
11150 );
11151 assert_eq!(editor.text(cx), expected_text);
11152 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
11153
11154 editor.handle_input("X", window, cx);
11155
11156 let (expected_text, expected_selections) = marked_text_ranges(
11157 indoc! {"
11158 aaaa
11159 bXˇbbXb
11160 bXˇbbXˇb
11161 cccc"
11162 },
11163 false,
11164 );
11165 assert_eq!(editor.text(cx), expected_text);
11166 assert_eq!(editor.selections.ranges(cx), expected_selections);
11167
11168 editor.newline(&Newline, window, cx);
11169 let (expected_text, expected_selections) = marked_text_ranges(
11170 indoc! {"
11171 aaaa
11172 bX
11173 ˇbbX
11174 b
11175 bX
11176 ˇbbX
11177 ˇb
11178 cccc"
11179 },
11180 false,
11181 );
11182 assert_eq!(editor.text(cx), expected_text);
11183 assert_eq!(editor.selections.ranges(cx), expected_selections);
11184 });
11185}
11186
11187#[gpui::test]
11188fn test_refresh_selections(cx: &mut TestAppContext) {
11189 init_test(cx, |_| {});
11190
11191 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11192 let mut excerpt1_id = None;
11193 let multibuffer = cx.new(|cx| {
11194 let mut multibuffer = MultiBuffer::new(ReadWrite);
11195 excerpt1_id = multibuffer
11196 .push_excerpts(
11197 buffer.clone(),
11198 [
11199 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11200 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11201 ],
11202 cx,
11203 )
11204 .into_iter()
11205 .next();
11206 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11207 multibuffer
11208 });
11209
11210 let editor = cx.add_window(|window, cx| {
11211 let mut editor = build_editor(multibuffer.clone(), window, cx);
11212 let snapshot = editor.snapshot(window, cx);
11213 editor.change_selections(None, window, cx, |s| {
11214 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
11215 });
11216 editor.begin_selection(
11217 Point::new(2, 1).to_display_point(&snapshot),
11218 true,
11219 1,
11220 window,
11221 cx,
11222 );
11223 assert_eq!(
11224 editor.selections.ranges(cx),
11225 [
11226 Point::new(1, 3)..Point::new(1, 3),
11227 Point::new(2, 1)..Point::new(2, 1),
11228 ]
11229 );
11230 editor
11231 });
11232
11233 // Refreshing selections is a no-op when excerpts haven't changed.
11234 _ = editor.update(cx, |editor, window, cx| {
11235 editor.change_selections(None, window, cx, |s| s.refresh());
11236 assert_eq!(
11237 editor.selections.ranges(cx),
11238 [
11239 Point::new(1, 3)..Point::new(1, 3),
11240 Point::new(2, 1)..Point::new(2, 1),
11241 ]
11242 );
11243 });
11244
11245 multibuffer.update(cx, |multibuffer, cx| {
11246 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11247 });
11248 _ = editor.update(cx, |editor, window, cx| {
11249 // Removing an excerpt causes the first selection to become degenerate.
11250 assert_eq!(
11251 editor.selections.ranges(cx),
11252 [
11253 Point::new(0, 0)..Point::new(0, 0),
11254 Point::new(0, 1)..Point::new(0, 1)
11255 ]
11256 );
11257
11258 // Refreshing selections will relocate the first selection to the original buffer
11259 // location.
11260 editor.change_selections(None, window, cx, |s| s.refresh());
11261 assert_eq!(
11262 editor.selections.ranges(cx),
11263 [
11264 Point::new(0, 1)..Point::new(0, 1),
11265 Point::new(0, 3)..Point::new(0, 3)
11266 ]
11267 );
11268 assert!(editor.selections.pending_anchor().is_some());
11269 });
11270}
11271
11272#[gpui::test]
11273fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
11274 init_test(cx, |_| {});
11275
11276 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
11277 let mut excerpt1_id = None;
11278 let multibuffer = cx.new(|cx| {
11279 let mut multibuffer = MultiBuffer::new(ReadWrite);
11280 excerpt1_id = multibuffer
11281 .push_excerpts(
11282 buffer.clone(),
11283 [
11284 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
11285 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
11286 ],
11287 cx,
11288 )
11289 .into_iter()
11290 .next();
11291 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
11292 multibuffer
11293 });
11294
11295 let editor = cx.add_window(|window, cx| {
11296 let mut editor = build_editor(multibuffer.clone(), window, cx);
11297 let snapshot = editor.snapshot(window, cx);
11298 editor.begin_selection(
11299 Point::new(1, 3).to_display_point(&snapshot),
11300 false,
11301 1,
11302 window,
11303 cx,
11304 );
11305 assert_eq!(
11306 editor.selections.ranges(cx),
11307 [Point::new(1, 3)..Point::new(1, 3)]
11308 );
11309 editor
11310 });
11311
11312 multibuffer.update(cx, |multibuffer, cx| {
11313 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
11314 });
11315 _ = editor.update(cx, |editor, window, cx| {
11316 assert_eq!(
11317 editor.selections.ranges(cx),
11318 [Point::new(0, 0)..Point::new(0, 0)]
11319 );
11320
11321 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
11322 editor.change_selections(None, window, cx, |s| s.refresh());
11323 assert_eq!(
11324 editor.selections.ranges(cx),
11325 [Point::new(0, 3)..Point::new(0, 3)]
11326 );
11327 assert!(editor.selections.pending_anchor().is_some());
11328 });
11329}
11330
11331#[gpui::test]
11332async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
11333 init_test(cx, |_| {});
11334
11335 let language = Arc::new(
11336 Language::new(
11337 LanguageConfig {
11338 brackets: BracketPairConfig {
11339 pairs: vec![
11340 BracketPair {
11341 start: "{".to_string(),
11342 end: "}".to_string(),
11343 close: true,
11344 surround: true,
11345 newline: true,
11346 },
11347 BracketPair {
11348 start: "/* ".to_string(),
11349 end: " */".to_string(),
11350 close: true,
11351 surround: true,
11352 newline: true,
11353 },
11354 ],
11355 ..Default::default()
11356 },
11357 ..Default::default()
11358 },
11359 Some(tree_sitter_rust::LANGUAGE.into()),
11360 )
11361 .with_indents_query("")
11362 .unwrap(),
11363 );
11364
11365 let text = concat!(
11366 "{ }\n", //
11367 " x\n", //
11368 " /* */\n", //
11369 "x\n", //
11370 "{{} }\n", //
11371 );
11372
11373 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
11374 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11375 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11376 editor
11377 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
11378 .await;
11379
11380 editor.update_in(cx, |editor, window, cx| {
11381 editor.change_selections(None, window, cx, |s| {
11382 s.select_display_ranges([
11383 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
11384 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
11385 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
11386 ])
11387 });
11388 editor.newline(&Newline, window, cx);
11389
11390 assert_eq!(
11391 editor.buffer().read(cx).read(cx).text(),
11392 concat!(
11393 "{ \n", // Suppress rustfmt
11394 "\n", //
11395 "}\n", //
11396 " x\n", //
11397 " /* \n", //
11398 " \n", //
11399 " */\n", //
11400 "x\n", //
11401 "{{} \n", //
11402 "}\n", //
11403 )
11404 );
11405 });
11406}
11407
11408#[gpui::test]
11409fn test_highlighted_ranges(cx: &mut TestAppContext) {
11410 init_test(cx, |_| {});
11411
11412 let editor = cx.add_window(|window, cx| {
11413 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
11414 build_editor(buffer.clone(), window, cx)
11415 });
11416
11417 _ = editor.update(cx, |editor, window, cx| {
11418 struct Type1;
11419 struct Type2;
11420
11421 let buffer = editor.buffer.read(cx).snapshot(cx);
11422
11423 let anchor_range =
11424 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
11425
11426 editor.highlight_background::<Type1>(
11427 &[
11428 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
11429 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
11430 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
11431 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
11432 ],
11433 |_| Hsla::red(),
11434 cx,
11435 );
11436 editor.highlight_background::<Type2>(
11437 &[
11438 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
11439 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
11440 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
11441 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
11442 ],
11443 |_| Hsla::green(),
11444 cx,
11445 );
11446
11447 let snapshot = editor.snapshot(window, cx);
11448 let mut highlighted_ranges = editor.background_highlights_in_range(
11449 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
11450 &snapshot,
11451 cx.theme().colors(),
11452 );
11453 // Enforce a consistent ordering based on color without relying on the ordering of the
11454 // highlight's `TypeId` which is non-executor.
11455 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
11456 assert_eq!(
11457 highlighted_ranges,
11458 &[
11459 (
11460 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
11461 Hsla::red(),
11462 ),
11463 (
11464 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11465 Hsla::red(),
11466 ),
11467 (
11468 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
11469 Hsla::green(),
11470 ),
11471 (
11472 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
11473 Hsla::green(),
11474 ),
11475 ]
11476 );
11477 assert_eq!(
11478 editor.background_highlights_in_range(
11479 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
11480 &snapshot,
11481 cx.theme().colors(),
11482 ),
11483 &[(
11484 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
11485 Hsla::red(),
11486 )]
11487 );
11488 });
11489}
11490
11491#[gpui::test]
11492async fn test_following(cx: &mut TestAppContext) {
11493 init_test(cx, |_| {});
11494
11495 let fs = FakeFs::new(cx.executor());
11496 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11497
11498 let buffer = project.update(cx, |project, cx| {
11499 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
11500 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
11501 });
11502 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
11503 let follower = cx.update(|cx| {
11504 cx.open_window(
11505 WindowOptions {
11506 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
11507 gpui::Point::new(px(0.), px(0.)),
11508 gpui::Point::new(px(10.), px(80.)),
11509 ))),
11510 ..Default::default()
11511 },
11512 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
11513 )
11514 .unwrap()
11515 });
11516
11517 let is_still_following = Rc::new(RefCell::new(true));
11518 let follower_edit_event_count = Rc::new(RefCell::new(0));
11519 let pending_update = Rc::new(RefCell::new(None));
11520 let leader_entity = leader.root(cx).unwrap();
11521 let follower_entity = follower.root(cx).unwrap();
11522 _ = follower.update(cx, {
11523 let update = pending_update.clone();
11524 let is_still_following = is_still_following.clone();
11525 let follower_edit_event_count = follower_edit_event_count.clone();
11526 |_, window, cx| {
11527 cx.subscribe_in(
11528 &leader_entity,
11529 window,
11530 move |_, leader, event, window, cx| {
11531 leader.read(cx).add_event_to_update_proto(
11532 event,
11533 &mut update.borrow_mut(),
11534 window,
11535 cx,
11536 );
11537 },
11538 )
11539 .detach();
11540
11541 cx.subscribe_in(
11542 &follower_entity,
11543 window,
11544 move |_, _, event: &EditorEvent, _window, _cx| {
11545 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
11546 *is_still_following.borrow_mut() = false;
11547 }
11548
11549 if let EditorEvent::BufferEdited = event {
11550 *follower_edit_event_count.borrow_mut() += 1;
11551 }
11552 },
11553 )
11554 .detach();
11555 }
11556 });
11557
11558 // Update the selections only
11559 _ = leader.update(cx, |leader, window, cx| {
11560 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11561 });
11562 follower
11563 .update(cx, |follower, window, cx| {
11564 follower.apply_update_proto(
11565 &project,
11566 pending_update.borrow_mut().take().unwrap(),
11567 window,
11568 cx,
11569 )
11570 })
11571 .unwrap()
11572 .await
11573 .unwrap();
11574 _ = follower.update(cx, |follower, _, cx| {
11575 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
11576 });
11577 assert!(*is_still_following.borrow());
11578 assert_eq!(*follower_edit_event_count.borrow(), 0);
11579
11580 // Update the scroll position only
11581 _ = leader.update(cx, |leader, window, cx| {
11582 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11583 });
11584 follower
11585 .update(cx, |follower, window, cx| {
11586 follower.apply_update_proto(
11587 &project,
11588 pending_update.borrow_mut().take().unwrap(),
11589 window,
11590 cx,
11591 )
11592 })
11593 .unwrap()
11594 .await
11595 .unwrap();
11596 assert_eq!(
11597 follower
11598 .update(cx, |follower, _, cx| follower.scroll_position(cx))
11599 .unwrap(),
11600 gpui::Point::new(1.5, 3.5)
11601 );
11602 assert!(*is_still_following.borrow());
11603 assert_eq!(*follower_edit_event_count.borrow(), 0);
11604
11605 // Update the selections and scroll position. The follower's scroll position is updated
11606 // via autoscroll, not via the leader's exact scroll position.
11607 _ = leader.update(cx, |leader, window, cx| {
11608 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
11609 leader.request_autoscroll(Autoscroll::newest(), cx);
11610 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
11611 });
11612 follower
11613 .update(cx, |follower, window, cx| {
11614 follower.apply_update_proto(
11615 &project,
11616 pending_update.borrow_mut().take().unwrap(),
11617 window,
11618 cx,
11619 )
11620 })
11621 .unwrap()
11622 .await
11623 .unwrap();
11624 _ = follower.update(cx, |follower, _, cx| {
11625 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
11626 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
11627 });
11628 assert!(*is_still_following.borrow());
11629
11630 // Creating a pending selection that precedes another selection
11631 _ = leader.update(cx, |leader, window, cx| {
11632 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
11633 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
11634 });
11635 follower
11636 .update(cx, |follower, window, cx| {
11637 follower.apply_update_proto(
11638 &project,
11639 pending_update.borrow_mut().take().unwrap(),
11640 window,
11641 cx,
11642 )
11643 })
11644 .unwrap()
11645 .await
11646 .unwrap();
11647 _ = follower.update(cx, |follower, _, cx| {
11648 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
11649 });
11650 assert!(*is_still_following.borrow());
11651
11652 // Extend the pending selection so that it surrounds another selection
11653 _ = leader.update(cx, |leader, window, cx| {
11654 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
11655 });
11656 follower
11657 .update(cx, |follower, window, cx| {
11658 follower.apply_update_proto(
11659 &project,
11660 pending_update.borrow_mut().take().unwrap(),
11661 window,
11662 cx,
11663 )
11664 })
11665 .unwrap()
11666 .await
11667 .unwrap();
11668 _ = follower.update(cx, |follower, _, cx| {
11669 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
11670 });
11671
11672 // Scrolling locally breaks the follow
11673 _ = follower.update(cx, |follower, window, cx| {
11674 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
11675 follower.set_scroll_anchor(
11676 ScrollAnchor {
11677 anchor: top_anchor,
11678 offset: gpui::Point::new(0.0, 0.5),
11679 },
11680 window,
11681 cx,
11682 );
11683 });
11684 assert!(!(*is_still_following.borrow()));
11685}
11686
11687#[gpui::test]
11688async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
11689 init_test(cx, |_| {});
11690
11691 let fs = FakeFs::new(cx.executor());
11692 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
11693 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11694 let pane = workspace
11695 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11696 .unwrap();
11697
11698 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11699
11700 let leader = pane.update_in(cx, |_, window, cx| {
11701 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
11702 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
11703 });
11704
11705 // Start following the editor when it has no excerpts.
11706 let mut state_message =
11707 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11708 let workspace_entity = workspace.root(cx).unwrap();
11709 let follower_1 = cx
11710 .update_window(*workspace.deref(), |_, window, cx| {
11711 Editor::from_state_proto(
11712 workspace_entity,
11713 ViewId {
11714 creator: Default::default(),
11715 id: 0,
11716 },
11717 &mut state_message,
11718 window,
11719 cx,
11720 )
11721 })
11722 .unwrap()
11723 .unwrap()
11724 .await
11725 .unwrap();
11726
11727 let update_message = Rc::new(RefCell::new(None));
11728 follower_1.update_in(cx, {
11729 let update = update_message.clone();
11730 |_, window, cx| {
11731 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
11732 leader.read(cx).add_event_to_update_proto(
11733 event,
11734 &mut update.borrow_mut(),
11735 window,
11736 cx,
11737 );
11738 })
11739 .detach();
11740 }
11741 });
11742
11743 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
11744 (
11745 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
11746 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
11747 )
11748 });
11749
11750 // Insert some excerpts.
11751 leader.update(cx, |leader, cx| {
11752 leader.buffer.update(cx, |multibuffer, cx| {
11753 let excerpt_ids = multibuffer.push_excerpts(
11754 buffer_1.clone(),
11755 [
11756 ExcerptRange::new(1..6),
11757 ExcerptRange::new(12..15),
11758 ExcerptRange::new(0..3),
11759 ],
11760 cx,
11761 );
11762 multibuffer.insert_excerpts_after(
11763 excerpt_ids[0],
11764 buffer_2.clone(),
11765 [ExcerptRange::new(8..12), ExcerptRange::new(0..6)],
11766 cx,
11767 );
11768 });
11769 });
11770
11771 // Apply the update of adding the excerpts.
11772 follower_1
11773 .update_in(cx, |follower, window, cx| {
11774 follower.apply_update_proto(
11775 &project,
11776 update_message.borrow().clone().unwrap(),
11777 window,
11778 cx,
11779 )
11780 })
11781 .await
11782 .unwrap();
11783 assert_eq!(
11784 follower_1.update(cx, |editor, cx| editor.text(cx)),
11785 leader.update(cx, |editor, cx| editor.text(cx))
11786 );
11787 update_message.borrow_mut().take();
11788
11789 // Start following separately after it already has excerpts.
11790 let mut state_message =
11791 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
11792 let workspace_entity = workspace.root(cx).unwrap();
11793 let follower_2 = cx
11794 .update_window(*workspace.deref(), |_, window, cx| {
11795 Editor::from_state_proto(
11796 workspace_entity,
11797 ViewId {
11798 creator: Default::default(),
11799 id: 0,
11800 },
11801 &mut state_message,
11802 window,
11803 cx,
11804 )
11805 })
11806 .unwrap()
11807 .unwrap()
11808 .await
11809 .unwrap();
11810 assert_eq!(
11811 follower_2.update(cx, |editor, cx| editor.text(cx)),
11812 leader.update(cx, |editor, cx| editor.text(cx))
11813 );
11814
11815 // Remove some excerpts.
11816 leader.update(cx, |leader, cx| {
11817 leader.buffer.update(cx, |multibuffer, cx| {
11818 let excerpt_ids = multibuffer.excerpt_ids();
11819 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
11820 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
11821 });
11822 });
11823
11824 // Apply the update of removing the excerpts.
11825 follower_1
11826 .update_in(cx, |follower, window, cx| {
11827 follower.apply_update_proto(
11828 &project,
11829 update_message.borrow().clone().unwrap(),
11830 window,
11831 cx,
11832 )
11833 })
11834 .await
11835 .unwrap();
11836 follower_2
11837 .update_in(cx, |follower, window, cx| {
11838 follower.apply_update_proto(
11839 &project,
11840 update_message.borrow().clone().unwrap(),
11841 window,
11842 cx,
11843 )
11844 })
11845 .await
11846 .unwrap();
11847 update_message.borrow_mut().take();
11848 assert_eq!(
11849 follower_1.update(cx, |editor, cx| editor.text(cx)),
11850 leader.update(cx, |editor, cx| editor.text(cx))
11851 );
11852}
11853
11854#[gpui::test]
11855async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
11856 init_test(cx, |_| {});
11857
11858 let mut cx = EditorTestContext::new(cx).await;
11859 let lsp_store =
11860 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11861
11862 cx.set_state(indoc! {"
11863 ˇfn func(abc def: i32) -> u32 {
11864 }
11865 "});
11866
11867 cx.update(|_, cx| {
11868 lsp_store.update(cx, |lsp_store, cx| {
11869 lsp_store
11870 .update_diagnostics(
11871 LanguageServerId(0),
11872 lsp::PublishDiagnosticsParams {
11873 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11874 version: None,
11875 diagnostics: vec![
11876 lsp::Diagnostic {
11877 range: lsp::Range::new(
11878 lsp::Position::new(0, 11),
11879 lsp::Position::new(0, 12),
11880 ),
11881 severity: Some(lsp::DiagnosticSeverity::ERROR),
11882 ..Default::default()
11883 },
11884 lsp::Diagnostic {
11885 range: lsp::Range::new(
11886 lsp::Position::new(0, 12),
11887 lsp::Position::new(0, 15),
11888 ),
11889 severity: Some(lsp::DiagnosticSeverity::ERROR),
11890 ..Default::default()
11891 },
11892 lsp::Diagnostic {
11893 range: lsp::Range::new(
11894 lsp::Position::new(0, 25),
11895 lsp::Position::new(0, 28),
11896 ),
11897 severity: Some(lsp::DiagnosticSeverity::ERROR),
11898 ..Default::default()
11899 },
11900 ],
11901 },
11902 &[],
11903 cx,
11904 )
11905 .unwrap()
11906 });
11907 });
11908
11909 executor.run_until_parked();
11910
11911 cx.update_editor(|editor, window, cx| {
11912 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11913 });
11914
11915 cx.assert_editor_state(indoc! {"
11916 fn func(abc def: i32) -> ˇu32 {
11917 }
11918 "});
11919
11920 cx.update_editor(|editor, window, cx| {
11921 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11922 });
11923
11924 cx.assert_editor_state(indoc! {"
11925 fn func(abc ˇdef: i32) -> u32 {
11926 }
11927 "});
11928
11929 cx.update_editor(|editor, window, cx| {
11930 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11931 });
11932
11933 cx.assert_editor_state(indoc! {"
11934 fn func(abcˇ def: i32) -> u32 {
11935 }
11936 "});
11937
11938 cx.update_editor(|editor, window, cx| {
11939 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
11940 });
11941
11942 cx.assert_editor_state(indoc! {"
11943 fn func(abc def: i32) -> ˇu32 {
11944 }
11945 "});
11946}
11947
11948#[gpui::test]
11949async fn cycle_through_same_place_diagnostics(
11950 executor: BackgroundExecutor,
11951 cx: &mut TestAppContext,
11952) {
11953 init_test(cx, |_| {});
11954
11955 let mut cx = EditorTestContext::new(cx).await;
11956 let lsp_store =
11957 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
11958
11959 cx.set_state(indoc! {"
11960 ˇfn func(abc def: i32) -> u32 {
11961 }
11962 "});
11963
11964 cx.update(|_, cx| {
11965 lsp_store.update(cx, |lsp_store, cx| {
11966 lsp_store
11967 .update_diagnostics(
11968 LanguageServerId(0),
11969 lsp::PublishDiagnosticsParams {
11970 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
11971 version: None,
11972 diagnostics: vec![
11973 lsp::Diagnostic {
11974 range: lsp::Range::new(
11975 lsp::Position::new(0, 11),
11976 lsp::Position::new(0, 12),
11977 ),
11978 severity: Some(lsp::DiagnosticSeverity::ERROR),
11979 ..Default::default()
11980 },
11981 lsp::Diagnostic {
11982 range: lsp::Range::new(
11983 lsp::Position::new(0, 12),
11984 lsp::Position::new(0, 15),
11985 ),
11986 severity: Some(lsp::DiagnosticSeverity::ERROR),
11987 ..Default::default()
11988 },
11989 lsp::Diagnostic {
11990 range: lsp::Range::new(
11991 lsp::Position::new(0, 12),
11992 lsp::Position::new(0, 15),
11993 ),
11994 severity: Some(lsp::DiagnosticSeverity::ERROR),
11995 ..Default::default()
11996 },
11997 lsp::Diagnostic {
11998 range: lsp::Range::new(
11999 lsp::Position::new(0, 25),
12000 lsp::Position::new(0, 28),
12001 ),
12002 severity: Some(lsp::DiagnosticSeverity::ERROR),
12003 ..Default::default()
12004 },
12005 ],
12006 },
12007 &[],
12008 cx,
12009 )
12010 .unwrap()
12011 });
12012 });
12013 executor.run_until_parked();
12014
12015 //// Backward
12016
12017 // Fourth diagnostic
12018 cx.update_editor(|editor, window, cx| {
12019 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12020 });
12021 cx.assert_editor_state(indoc! {"
12022 fn func(abc def: i32) -> ˇu32 {
12023 }
12024 "});
12025
12026 // Third diagnostic
12027 cx.update_editor(|editor, window, cx| {
12028 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12029 });
12030 cx.assert_editor_state(indoc! {"
12031 fn func(abc ˇdef: i32) -> u32 {
12032 }
12033 "});
12034
12035 // Second diagnostic, same place
12036 cx.update_editor(|editor, window, cx| {
12037 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12038 });
12039 cx.assert_editor_state(indoc! {"
12040 fn func(abc ˇdef: i32) -> u32 {
12041 }
12042 "});
12043
12044 // First diagnostic
12045 cx.update_editor(|editor, window, cx| {
12046 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12047 });
12048 cx.assert_editor_state(indoc! {"
12049 fn func(abcˇ def: i32) -> u32 {
12050 }
12051 "});
12052
12053 // Wrapped over, fourth diagnostic
12054 cx.update_editor(|editor, window, cx| {
12055 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
12056 });
12057 cx.assert_editor_state(indoc! {"
12058 fn func(abc def: i32) -> ˇu32 {
12059 }
12060 "});
12061
12062 cx.update_editor(|editor, window, cx| {
12063 editor.move_to_beginning(&MoveToBeginning, window, cx);
12064 });
12065 cx.assert_editor_state(indoc! {"
12066 ˇfn func(abc def: i32) -> u32 {
12067 }
12068 "});
12069
12070 //// Forward
12071
12072 // First diagnostic
12073 cx.update_editor(|editor, window, cx| {
12074 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12075 });
12076 cx.assert_editor_state(indoc! {"
12077 fn func(abcˇ def: i32) -> u32 {
12078 }
12079 "});
12080
12081 // Second diagnostic
12082 cx.update_editor(|editor, window, cx| {
12083 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12084 });
12085 cx.assert_editor_state(indoc! {"
12086 fn func(abc ˇdef: i32) -> u32 {
12087 }
12088 "});
12089
12090 // Third diagnostic, same place
12091 cx.update_editor(|editor, window, cx| {
12092 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12093 });
12094 cx.assert_editor_state(indoc! {"
12095 fn func(abc ˇdef: i32) -> u32 {
12096 }
12097 "});
12098
12099 // Fourth diagnostic
12100 cx.update_editor(|editor, window, cx| {
12101 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12102 });
12103 cx.assert_editor_state(indoc! {"
12104 fn func(abc def: i32) -> ˇu32 {
12105 }
12106 "});
12107
12108 // Wrapped around, first diagnostic
12109 cx.update_editor(|editor, window, cx| {
12110 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12111 });
12112 cx.assert_editor_state(indoc! {"
12113 fn func(abcˇ def: i32) -> u32 {
12114 }
12115 "});
12116}
12117
12118#[gpui::test]
12119async fn active_diagnostics_dismiss_after_invalidation(
12120 executor: BackgroundExecutor,
12121 cx: &mut TestAppContext,
12122) {
12123 init_test(cx, |_| {});
12124
12125 let mut cx = EditorTestContext::new(cx).await;
12126 let lsp_store =
12127 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12128
12129 cx.set_state(indoc! {"
12130 ˇfn func(abc def: i32) -> u32 {
12131 }
12132 "});
12133
12134 let message = "Something's wrong!";
12135 cx.update(|_, cx| {
12136 lsp_store.update(cx, |lsp_store, cx| {
12137 lsp_store
12138 .update_diagnostics(
12139 LanguageServerId(0),
12140 lsp::PublishDiagnosticsParams {
12141 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12142 version: None,
12143 diagnostics: vec![lsp::Diagnostic {
12144 range: lsp::Range::new(
12145 lsp::Position::new(0, 11),
12146 lsp::Position::new(0, 12),
12147 ),
12148 severity: Some(lsp::DiagnosticSeverity::ERROR),
12149 message: message.to_string(),
12150 ..Default::default()
12151 }],
12152 },
12153 &[],
12154 cx,
12155 )
12156 .unwrap()
12157 });
12158 });
12159 executor.run_until_parked();
12160
12161 cx.update_editor(|editor, window, cx| {
12162 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12163 assert_eq!(
12164 editor
12165 .active_diagnostics
12166 .as_ref()
12167 .map(|diagnostics_group| diagnostics_group.primary_message.as_str()),
12168 Some(message),
12169 "Should have a diagnostics group activated"
12170 );
12171 });
12172 cx.assert_editor_state(indoc! {"
12173 fn func(abcˇ def: i32) -> u32 {
12174 }
12175 "});
12176
12177 cx.update(|_, cx| {
12178 lsp_store.update(cx, |lsp_store, cx| {
12179 lsp_store
12180 .update_diagnostics(
12181 LanguageServerId(0),
12182 lsp::PublishDiagnosticsParams {
12183 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12184 version: None,
12185 diagnostics: Vec::new(),
12186 },
12187 &[],
12188 cx,
12189 )
12190 .unwrap()
12191 });
12192 });
12193 executor.run_until_parked();
12194 cx.update_editor(|editor, _, _| {
12195 assert_eq!(
12196 editor.active_diagnostics, None,
12197 "After no diagnostics set to the editor, no diagnostics should be active"
12198 );
12199 });
12200 cx.assert_editor_state(indoc! {"
12201 fn func(abcˇ def: i32) -> u32 {
12202 }
12203 "});
12204
12205 cx.update_editor(|editor, window, cx| {
12206 editor.go_to_diagnostic(&GoToDiagnostic, window, cx);
12207 assert_eq!(
12208 editor.active_diagnostics, None,
12209 "Should be no diagnostics to go to and activate"
12210 );
12211 });
12212 cx.assert_editor_state(indoc! {"
12213 fn func(abcˇ def: i32) -> u32 {
12214 }
12215 "});
12216}
12217
12218#[gpui::test]
12219async fn test_diagnostics_with_links(cx: &mut TestAppContext) {
12220 init_test(cx, |_| {});
12221
12222 let mut cx = EditorTestContext::new(cx).await;
12223
12224 cx.set_state(indoc! {"
12225 fn func(abˇc def: i32) -> u32 {
12226 }
12227 "});
12228 let lsp_store =
12229 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12230
12231 cx.update(|_, cx| {
12232 lsp_store.update(cx, |lsp_store, cx| {
12233 lsp_store.update_diagnostics(
12234 LanguageServerId(0),
12235 lsp::PublishDiagnosticsParams {
12236 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12237 version: None,
12238 diagnostics: vec![lsp::Diagnostic {
12239 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 12)),
12240 severity: Some(lsp::DiagnosticSeverity::ERROR),
12241 message: "we've had problems with <https://link.one>, and <https://link.two> is broken".to_string(),
12242 ..Default::default()
12243 }],
12244 },
12245 &[],
12246 cx,
12247 )
12248 })
12249 }).unwrap();
12250 cx.run_until_parked();
12251 cx.update_editor(|editor, window, cx| {
12252 hover_popover::hover(editor, &Default::default(), window, cx)
12253 });
12254 cx.run_until_parked();
12255 cx.update_editor(|editor, _, _| assert!(editor.hover_state.diagnostic_popover.is_some()))
12256}
12257
12258#[gpui::test]
12259async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12260 init_test(cx, |_| {});
12261
12262 let mut cx = EditorTestContext::new(cx).await;
12263
12264 let diff_base = r#"
12265 use some::mod;
12266
12267 const A: u32 = 42;
12268
12269 fn main() {
12270 println!("hello");
12271
12272 println!("world");
12273 }
12274 "#
12275 .unindent();
12276
12277 // Edits are modified, removed, modified, added
12278 cx.set_state(
12279 &r#"
12280 use some::modified;
12281
12282 ˇ
12283 fn main() {
12284 println!("hello there");
12285
12286 println!("around the");
12287 println!("world");
12288 }
12289 "#
12290 .unindent(),
12291 );
12292
12293 cx.set_head_text(&diff_base);
12294 executor.run_until_parked();
12295
12296 cx.update_editor(|editor, window, cx| {
12297 //Wrap around the bottom of the buffer
12298 for _ in 0..3 {
12299 editor.go_to_next_hunk(&GoToHunk, window, cx);
12300 }
12301 });
12302
12303 cx.assert_editor_state(
12304 &r#"
12305 ˇuse some::modified;
12306
12307
12308 fn main() {
12309 println!("hello there");
12310
12311 println!("around the");
12312 println!("world");
12313 }
12314 "#
12315 .unindent(),
12316 );
12317
12318 cx.update_editor(|editor, window, cx| {
12319 //Wrap around the top of the buffer
12320 for _ in 0..2 {
12321 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12322 }
12323 });
12324
12325 cx.assert_editor_state(
12326 &r#"
12327 use some::modified;
12328
12329
12330 fn main() {
12331 ˇ println!("hello there");
12332
12333 println!("around the");
12334 println!("world");
12335 }
12336 "#
12337 .unindent(),
12338 );
12339
12340 cx.update_editor(|editor, window, cx| {
12341 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12342 });
12343
12344 cx.assert_editor_state(
12345 &r#"
12346 use some::modified;
12347
12348 ˇ
12349 fn main() {
12350 println!("hello there");
12351
12352 println!("around the");
12353 println!("world");
12354 }
12355 "#
12356 .unindent(),
12357 );
12358
12359 cx.update_editor(|editor, window, cx| {
12360 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12361 });
12362
12363 cx.assert_editor_state(
12364 &r#"
12365 ˇuse some::modified;
12366
12367
12368 fn main() {
12369 println!("hello there");
12370
12371 println!("around the");
12372 println!("world");
12373 }
12374 "#
12375 .unindent(),
12376 );
12377
12378 cx.update_editor(|editor, window, cx| {
12379 for _ in 0..2 {
12380 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
12381 }
12382 });
12383
12384 cx.assert_editor_state(
12385 &r#"
12386 use some::modified;
12387
12388
12389 fn main() {
12390 ˇ println!("hello there");
12391
12392 println!("around the");
12393 println!("world");
12394 }
12395 "#
12396 .unindent(),
12397 );
12398
12399 cx.update_editor(|editor, window, cx| {
12400 editor.fold(&Fold, window, cx);
12401 });
12402
12403 cx.update_editor(|editor, window, cx| {
12404 editor.go_to_next_hunk(&GoToHunk, window, cx);
12405 });
12406
12407 cx.assert_editor_state(
12408 &r#"
12409 ˇuse some::modified;
12410
12411
12412 fn main() {
12413 println!("hello there");
12414
12415 println!("around the");
12416 println!("world");
12417 }
12418 "#
12419 .unindent(),
12420 );
12421}
12422
12423#[test]
12424fn test_split_words() {
12425 fn split(text: &str) -> Vec<&str> {
12426 split_words(text).collect()
12427 }
12428
12429 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
12430 assert_eq!(split("hello_world"), &["hello_", "world"]);
12431 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
12432 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
12433 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
12434 assert_eq!(split("helloworld"), &["helloworld"]);
12435
12436 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
12437}
12438
12439#[gpui::test]
12440async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
12441 init_test(cx, |_| {});
12442
12443 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
12444 let mut assert = |before, after| {
12445 let _state_context = cx.set_state(before);
12446 cx.run_until_parked();
12447 cx.update_editor(|editor, window, cx| {
12448 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
12449 });
12450 cx.run_until_parked();
12451 cx.assert_editor_state(after);
12452 };
12453
12454 // Outside bracket jumps to outside of matching bracket
12455 assert("console.logˇ(var);", "console.log(var)ˇ;");
12456 assert("console.log(var)ˇ;", "console.logˇ(var);");
12457
12458 // Inside bracket jumps to inside of matching bracket
12459 assert("console.log(ˇvar);", "console.log(varˇ);");
12460 assert("console.log(varˇ);", "console.log(ˇvar);");
12461
12462 // When outside a bracket and inside, favor jumping to the inside bracket
12463 assert(
12464 "console.log('foo', [1, 2, 3]ˇ);",
12465 "console.log(ˇ'foo', [1, 2, 3]);",
12466 );
12467 assert(
12468 "console.log(ˇ'foo', [1, 2, 3]);",
12469 "console.log('foo', [1, 2, 3]ˇ);",
12470 );
12471
12472 // Bias forward if two options are equally likely
12473 assert(
12474 "let result = curried_fun()ˇ();",
12475 "let result = curried_fun()()ˇ;",
12476 );
12477
12478 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
12479 assert(
12480 indoc! {"
12481 function test() {
12482 console.log('test')ˇ
12483 }"},
12484 indoc! {"
12485 function test() {
12486 console.logˇ('test')
12487 }"},
12488 );
12489}
12490
12491#[gpui::test]
12492async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
12493 init_test(cx, |_| {});
12494
12495 let fs = FakeFs::new(cx.executor());
12496 fs.insert_tree(
12497 path!("/a"),
12498 json!({
12499 "main.rs": "fn main() { let a = 5; }",
12500 "other.rs": "// Test file",
12501 }),
12502 )
12503 .await;
12504 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12505
12506 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12507 language_registry.add(Arc::new(Language::new(
12508 LanguageConfig {
12509 name: "Rust".into(),
12510 matcher: LanguageMatcher {
12511 path_suffixes: vec!["rs".to_string()],
12512 ..Default::default()
12513 },
12514 brackets: BracketPairConfig {
12515 pairs: vec![BracketPair {
12516 start: "{".to_string(),
12517 end: "}".to_string(),
12518 close: true,
12519 surround: true,
12520 newline: true,
12521 }],
12522 disabled_scopes_by_bracket_ix: Vec::new(),
12523 },
12524 ..Default::default()
12525 },
12526 Some(tree_sitter_rust::LANGUAGE.into()),
12527 )));
12528 let mut fake_servers = language_registry.register_fake_lsp(
12529 "Rust",
12530 FakeLspAdapter {
12531 capabilities: lsp::ServerCapabilities {
12532 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
12533 first_trigger_character: "{".to_string(),
12534 more_trigger_character: None,
12535 }),
12536 ..Default::default()
12537 },
12538 ..Default::default()
12539 },
12540 );
12541
12542 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12543
12544 let cx = &mut VisualTestContext::from_window(*workspace, cx);
12545
12546 let worktree_id = workspace
12547 .update(cx, |workspace, _, cx| {
12548 workspace.project().update(cx, |project, cx| {
12549 project.worktrees(cx).next().unwrap().read(cx).id()
12550 })
12551 })
12552 .unwrap();
12553
12554 let buffer = project
12555 .update(cx, |project, cx| {
12556 project.open_local_buffer(path!("/a/main.rs"), cx)
12557 })
12558 .await
12559 .unwrap();
12560 let editor_handle = workspace
12561 .update(cx, |workspace, window, cx| {
12562 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
12563 })
12564 .unwrap()
12565 .await
12566 .unwrap()
12567 .downcast::<Editor>()
12568 .unwrap();
12569
12570 cx.executor().start_waiting();
12571 let fake_server = fake_servers.next().await.unwrap();
12572
12573 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
12574 |params, _| async move {
12575 assert_eq!(
12576 params.text_document_position.text_document.uri,
12577 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
12578 );
12579 assert_eq!(
12580 params.text_document_position.position,
12581 lsp::Position::new(0, 21),
12582 );
12583
12584 Ok(Some(vec![lsp::TextEdit {
12585 new_text: "]".to_string(),
12586 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12587 }]))
12588 },
12589 );
12590
12591 editor_handle.update_in(cx, |editor, window, cx| {
12592 window.focus(&editor.focus_handle(cx));
12593 editor.change_selections(None, window, cx, |s| {
12594 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
12595 });
12596 editor.handle_input("{", window, cx);
12597 });
12598
12599 cx.executor().run_until_parked();
12600
12601 buffer.update(cx, |buffer, _| {
12602 assert_eq!(
12603 buffer.text(),
12604 "fn main() { let a = {5}; }",
12605 "No extra braces from on type formatting should appear in the buffer"
12606 )
12607 });
12608}
12609
12610#[gpui::test]
12611async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
12612 init_test(cx, |_| {});
12613
12614 let fs = FakeFs::new(cx.executor());
12615 fs.insert_tree(
12616 path!("/a"),
12617 json!({
12618 "main.rs": "fn main() { let a = 5; }",
12619 "other.rs": "// Test file",
12620 }),
12621 )
12622 .await;
12623
12624 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
12625
12626 let server_restarts = Arc::new(AtomicUsize::new(0));
12627 let closure_restarts = Arc::clone(&server_restarts);
12628 let language_server_name = "test language server";
12629 let language_name: LanguageName = "Rust".into();
12630
12631 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12632 language_registry.add(Arc::new(Language::new(
12633 LanguageConfig {
12634 name: language_name.clone(),
12635 matcher: LanguageMatcher {
12636 path_suffixes: vec!["rs".to_string()],
12637 ..Default::default()
12638 },
12639 ..Default::default()
12640 },
12641 Some(tree_sitter_rust::LANGUAGE.into()),
12642 )));
12643 let mut fake_servers = language_registry.register_fake_lsp(
12644 "Rust",
12645 FakeLspAdapter {
12646 name: language_server_name,
12647 initialization_options: Some(json!({
12648 "testOptionValue": true
12649 })),
12650 initializer: Some(Box::new(move |fake_server| {
12651 let task_restarts = Arc::clone(&closure_restarts);
12652 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
12653 task_restarts.fetch_add(1, atomic::Ordering::Release);
12654 futures::future::ready(Ok(()))
12655 });
12656 })),
12657 ..Default::default()
12658 },
12659 );
12660
12661 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12662 let _buffer = project
12663 .update(cx, |project, cx| {
12664 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
12665 })
12666 .await
12667 .unwrap();
12668 let _fake_server = fake_servers.next().await.unwrap();
12669 update_test_language_settings(cx, |language_settings| {
12670 language_settings.languages.insert(
12671 language_name.clone(),
12672 LanguageSettingsContent {
12673 tab_size: NonZeroU32::new(8),
12674 ..Default::default()
12675 },
12676 );
12677 });
12678 cx.executor().run_until_parked();
12679 assert_eq!(
12680 server_restarts.load(atomic::Ordering::Acquire),
12681 0,
12682 "Should not restart LSP server on an unrelated change"
12683 );
12684
12685 update_test_project_settings(cx, |project_settings| {
12686 project_settings.lsp.insert(
12687 "Some other server name".into(),
12688 LspSettings {
12689 binary: None,
12690 settings: None,
12691 initialization_options: Some(json!({
12692 "some other init value": false
12693 })),
12694 enable_lsp_tasks: false,
12695 },
12696 );
12697 });
12698 cx.executor().run_until_parked();
12699 assert_eq!(
12700 server_restarts.load(atomic::Ordering::Acquire),
12701 0,
12702 "Should not restart LSP server on an unrelated LSP settings change"
12703 );
12704
12705 update_test_project_settings(cx, |project_settings| {
12706 project_settings.lsp.insert(
12707 language_server_name.into(),
12708 LspSettings {
12709 binary: None,
12710 settings: None,
12711 initialization_options: Some(json!({
12712 "anotherInitValue": false
12713 })),
12714 enable_lsp_tasks: false,
12715 },
12716 );
12717 });
12718 cx.executor().run_until_parked();
12719 assert_eq!(
12720 server_restarts.load(atomic::Ordering::Acquire),
12721 1,
12722 "Should restart LSP server on a related LSP settings change"
12723 );
12724
12725 update_test_project_settings(cx, |project_settings| {
12726 project_settings.lsp.insert(
12727 language_server_name.into(),
12728 LspSettings {
12729 binary: None,
12730 settings: None,
12731 initialization_options: Some(json!({
12732 "anotherInitValue": false
12733 })),
12734 enable_lsp_tasks: false,
12735 },
12736 );
12737 });
12738 cx.executor().run_until_parked();
12739 assert_eq!(
12740 server_restarts.load(atomic::Ordering::Acquire),
12741 1,
12742 "Should not restart LSP server on a related LSP settings change that is the same"
12743 );
12744
12745 update_test_project_settings(cx, |project_settings| {
12746 project_settings.lsp.insert(
12747 language_server_name.into(),
12748 LspSettings {
12749 binary: None,
12750 settings: None,
12751 initialization_options: None,
12752 enable_lsp_tasks: false,
12753 },
12754 );
12755 });
12756 cx.executor().run_until_parked();
12757 assert_eq!(
12758 server_restarts.load(atomic::Ordering::Acquire),
12759 2,
12760 "Should restart LSP server on another related LSP settings change"
12761 );
12762}
12763
12764#[gpui::test]
12765async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
12766 init_test(cx, |_| {});
12767
12768 let mut cx = EditorLspTestContext::new_rust(
12769 lsp::ServerCapabilities {
12770 completion_provider: Some(lsp::CompletionOptions {
12771 trigger_characters: Some(vec![".".to_string()]),
12772 resolve_provider: Some(true),
12773 ..Default::default()
12774 }),
12775 ..Default::default()
12776 },
12777 cx,
12778 )
12779 .await;
12780
12781 cx.set_state("fn main() { let a = 2ˇ; }");
12782 cx.simulate_keystroke(".");
12783 let completion_item = lsp::CompletionItem {
12784 label: "some".into(),
12785 kind: Some(lsp::CompletionItemKind::SNIPPET),
12786 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12787 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12788 kind: lsp::MarkupKind::Markdown,
12789 value: "```rust\nSome(2)\n```".to_string(),
12790 })),
12791 deprecated: Some(false),
12792 sort_text: Some("fffffff2".to_string()),
12793 filter_text: Some("some".to_string()),
12794 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12795 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12796 range: lsp::Range {
12797 start: lsp::Position {
12798 line: 0,
12799 character: 22,
12800 },
12801 end: lsp::Position {
12802 line: 0,
12803 character: 22,
12804 },
12805 },
12806 new_text: "Some(2)".to_string(),
12807 })),
12808 additional_text_edits: Some(vec![lsp::TextEdit {
12809 range: lsp::Range {
12810 start: lsp::Position {
12811 line: 0,
12812 character: 20,
12813 },
12814 end: lsp::Position {
12815 line: 0,
12816 character: 22,
12817 },
12818 },
12819 new_text: "".to_string(),
12820 }]),
12821 ..Default::default()
12822 };
12823
12824 let closure_completion_item = completion_item.clone();
12825 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12826 let task_completion_item = closure_completion_item.clone();
12827 async move {
12828 Ok(Some(lsp::CompletionResponse::Array(vec![
12829 task_completion_item,
12830 ])))
12831 }
12832 });
12833
12834 request.next().await;
12835
12836 cx.condition(|editor, _| editor.context_menu_visible())
12837 .await;
12838 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
12839 editor
12840 .confirm_completion(&ConfirmCompletion::default(), window, cx)
12841 .unwrap()
12842 });
12843 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
12844
12845 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
12846 let task_completion_item = completion_item.clone();
12847 async move { Ok(task_completion_item) }
12848 })
12849 .next()
12850 .await
12851 .unwrap();
12852 apply_additional_edits.await.unwrap();
12853 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
12854}
12855
12856#[gpui::test]
12857async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
12858 init_test(cx, |_| {});
12859
12860 let mut cx = EditorLspTestContext::new_rust(
12861 lsp::ServerCapabilities {
12862 completion_provider: Some(lsp::CompletionOptions {
12863 trigger_characters: Some(vec![".".to_string()]),
12864 resolve_provider: Some(true),
12865 ..Default::default()
12866 }),
12867 ..Default::default()
12868 },
12869 cx,
12870 )
12871 .await;
12872
12873 cx.set_state("fn main() { let a = 2ˇ; }");
12874 cx.simulate_keystroke(".");
12875
12876 let item1 = lsp::CompletionItem {
12877 label: "method id()".to_string(),
12878 filter_text: Some("id".to_string()),
12879 detail: None,
12880 documentation: None,
12881 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12882 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12883 new_text: ".id".to_string(),
12884 })),
12885 ..lsp::CompletionItem::default()
12886 };
12887
12888 let item2 = lsp::CompletionItem {
12889 label: "other".to_string(),
12890 filter_text: Some("other".to_string()),
12891 detail: None,
12892 documentation: None,
12893 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12894 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
12895 new_text: ".other".to_string(),
12896 })),
12897 ..lsp::CompletionItem::default()
12898 };
12899
12900 let item1 = item1.clone();
12901 cx.set_request_handler::<lsp::request::Completion, _, _>({
12902 let item1 = item1.clone();
12903 move |_, _, _| {
12904 let item1 = item1.clone();
12905 let item2 = item2.clone();
12906 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
12907 }
12908 })
12909 .next()
12910 .await;
12911
12912 cx.condition(|editor, _| editor.context_menu_visible())
12913 .await;
12914 cx.update_editor(|editor, _, _| {
12915 let context_menu = editor.context_menu.borrow_mut();
12916 let context_menu = context_menu
12917 .as_ref()
12918 .expect("Should have the context menu deployed");
12919 match context_menu {
12920 CodeContextMenu::Completions(completions_menu) => {
12921 let completions = completions_menu.completions.borrow_mut();
12922 assert_eq!(
12923 completions
12924 .iter()
12925 .map(|completion| &completion.label.text)
12926 .collect::<Vec<_>>(),
12927 vec!["method id()", "other"]
12928 )
12929 }
12930 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12931 }
12932 });
12933
12934 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
12935 let item1 = item1.clone();
12936 move |_, item_to_resolve, _| {
12937 let item1 = item1.clone();
12938 async move {
12939 if item1 == item_to_resolve {
12940 Ok(lsp::CompletionItem {
12941 label: "method id()".to_string(),
12942 filter_text: Some("id".to_string()),
12943 detail: Some("Now resolved!".to_string()),
12944 documentation: Some(lsp::Documentation::String("Docs".to_string())),
12945 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12946 range: lsp::Range::new(
12947 lsp::Position::new(0, 22),
12948 lsp::Position::new(0, 22),
12949 ),
12950 new_text: ".id".to_string(),
12951 })),
12952 ..lsp::CompletionItem::default()
12953 })
12954 } else {
12955 Ok(item_to_resolve)
12956 }
12957 }
12958 }
12959 })
12960 .next()
12961 .await
12962 .unwrap();
12963 cx.run_until_parked();
12964
12965 cx.update_editor(|editor, window, cx| {
12966 editor.context_menu_next(&Default::default(), window, cx);
12967 });
12968
12969 cx.update_editor(|editor, _, _| {
12970 let context_menu = editor.context_menu.borrow_mut();
12971 let context_menu = context_menu
12972 .as_ref()
12973 .expect("Should have the context menu deployed");
12974 match context_menu {
12975 CodeContextMenu::Completions(completions_menu) => {
12976 let completions = completions_menu.completions.borrow_mut();
12977 assert_eq!(
12978 completions
12979 .iter()
12980 .map(|completion| &completion.label.text)
12981 .collect::<Vec<_>>(),
12982 vec!["method id() Now resolved!", "other"],
12983 "Should update first completion label, but not second as the filter text did not match."
12984 );
12985 }
12986 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
12987 }
12988 });
12989}
12990
12991#[gpui::test]
12992async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
12993 init_test(cx, |_| {});
12994
12995 let mut cx = EditorLspTestContext::new_rust(
12996 lsp::ServerCapabilities {
12997 completion_provider: Some(lsp::CompletionOptions {
12998 trigger_characters: Some(vec![".".to_string()]),
12999 resolve_provider: Some(true),
13000 ..Default::default()
13001 }),
13002 ..Default::default()
13003 },
13004 cx,
13005 )
13006 .await;
13007
13008 cx.set_state("fn main() { let a = 2ˇ; }");
13009 cx.simulate_keystroke(".");
13010
13011 let unresolved_item_1 = lsp::CompletionItem {
13012 label: "id".to_string(),
13013 filter_text: Some("id".to_string()),
13014 detail: None,
13015 documentation: None,
13016 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13017 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13018 new_text: ".id".to_string(),
13019 })),
13020 ..lsp::CompletionItem::default()
13021 };
13022 let resolved_item_1 = lsp::CompletionItem {
13023 additional_text_edits: Some(vec![lsp::TextEdit {
13024 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13025 new_text: "!!".to_string(),
13026 }]),
13027 ..unresolved_item_1.clone()
13028 };
13029 let unresolved_item_2 = lsp::CompletionItem {
13030 label: "other".to_string(),
13031 filter_text: Some("other".to_string()),
13032 detail: None,
13033 documentation: None,
13034 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13035 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13036 new_text: ".other".to_string(),
13037 })),
13038 ..lsp::CompletionItem::default()
13039 };
13040 let resolved_item_2 = lsp::CompletionItem {
13041 additional_text_edits: Some(vec![lsp::TextEdit {
13042 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13043 new_text: "??".to_string(),
13044 }]),
13045 ..unresolved_item_2.clone()
13046 };
13047
13048 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13049 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13050 cx.lsp
13051 .server
13052 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13053 let unresolved_item_1 = unresolved_item_1.clone();
13054 let resolved_item_1 = resolved_item_1.clone();
13055 let unresolved_item_2 = unresolved_item_2.clone();
13056 let resolved_item_2 = resolved_item_2.clone();
13057 let resolve_requests_1 = resolve_requests_1.clone();
13058 let resolve_requests_2 = resolve_requests_2.clone();
13059 move |unresolved_request, _| {
13060 let unresolved_item_1 = unresolved_item_1.clone();
13061 let resolved_item_1 = resolved_item_1.clone();
13062 let unresolved_item_2 = unresolved_item_2.clone();
13063 let resolved_item_2 = resolved_item_2.clone();
13064 let resolve_requests_1 = resolve_requests_1.clone();
13065 let resolve_requests_2 = resolve_requests_2.clone();
13066 async move {
13067 if unresolved_request == unresolved_item_1 {
13068 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13069 Ok(resolved_item_1.clone())
13070 } else if unresolved_request == unresolved_item_2 {
13071 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13072 Ok(resolved_item_2.clone())
13073 } else {
13074 panic!("Unexpected completion item {unresolved_request:?}")
13075 }
13076 }
13077 }
13078 })
13079 .detach();
13080
13081 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13082 let unresolved_item_1 = unresolved_item_1.clone();
13083 let unresolved_item_2 = unresolved_item_2.clone();
13084 async move {
13085 Ok(Some(lsp::CompletionResponse::Array(vec![
13086 unresolved_item_1,
13087 unresolved_item_2,
13088 ])))
13089 }
13090 })
13091 .next()
13092 .await;
13093
13094 cx.condition(|editor, _| editor.context_menu_visible())
13095 .await;
13096 cx.update_editor(|editor, _, _| {
13097 let context_menu = editor.context_menu.borrow_mut();
13098 let context_menu = context_menu
13099 .as_ref()
13100 .expect("Should have the context menu deployed");
13101 match context_menu {
13102 CodeContextMenu::Completions(completions_menu) => {
13103 let completions = completions_menu.completions.borrow_mut();
13104 assert_eq!(
13105 completions
13106 .iter()
13107 .map(|completion| &completion.label.text)
13108 .collect::<Vec<_>>(),
13109 vec!["id", "other"]
13110 )
13111 }
13112 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13113 }
13114 });
13115 cx.run_until_parked();
13116
13117 cx.update_editor(|editor, window, cx| {
13118 editor.context_menu_next(&ContextMenuNext, window, cx);
13119 });
13120 cx.run_until_parked();
13121 cx.update_editor(|editor, window, cx| {
13122 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13123 });
13124 cx.run_until_parked();
13125 cx.update_editor(|editor, window, cx| {
13126 editor.context_menu_next(&ContextMenuNext, window, cx);
13127 });
13128 cx.run_until_parked();
13129 cx.update_editor(|editor, window, cx| {
13130 editor
13131 .compose_completion(&ComposeCompletion::default(), window, cx)
13132 .expect("No task returned")
13133 })
13134 .await
13135 .expect("Completion failed");
13136 cx.run_until_parked();
13137
13138 cx.update_editor(|editor, _, cx| {
13139 assert_eq!(
13140 resolve_requests_1.load(atomic::Ordering::Acquire),
13141 1,
13142 "Should always resolve once despite multiple selections"
13143 );
13144 assert_eq!(
13145 resolve_requests_2.load(atomic::Ordering::Acquire),
13146 1,
13147 "Should always resolve once after multiple selections and applying the completion"
13148 );
13149 assert_eq!(
13150 editor.text(cx),
13151 "fn main() { let a = ??.other; }",
13152 "Should use resolved data when applying the completion"
13153 );
13154 });
13155}
13156
13157#[gpui::test]
13158async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13159 init_test(cx, |_| {});
13160
13161 let item_0 = lsp::CompletionItem {
13162 label: "abs".into(),
13163 insert_text: Some("abs".into()),
13164 data: Some(json!({ "very": "special"})),
13165 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13166 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13167 lsp::InsertReplaceEdit {
13168 new_text: "abs".to_string(),
13169 insert: lsp::Range::default(),
13170 replace: lsp::Range::default(),
13171 },
13172 )),
13173 ..lsp::CompletionItem::default()
13174 };
13175 let items = iter::once(item_0.clone())
13176 .chain((11..51).map(|i| lsp::CompletionItem {
13177 label: format!("item_{}", i),
13178 insert_text: Some(format!("item_{}", i)),
13179 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13180 ..lsp::CompletionItem::default()
13181 }))
13182 .collect::<Vec<_>>();
13183
13184 let default_commit_characters = vec!["?".to_string()];
13185 let default_data = json!({ "default": "data"});
13186 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13187 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13188 let default_edit_range = lsp::Range {
13189 start: lsp::Position {
13190 line: 0,
13191 character: 5,
13192 },
13193 end: lsp::Position {
13194 line: 0,
13195 character: 5,
13196 },
13197 };
13198
13199 let mut cx = EditorLspTestContext::new_rust(
13200 lsp::ServerCapabilities {
13201 completion_provider: Some(lsp::CompletionOptions {
13202 trigger_characters: Some(vec![".".to_string()]),
13203 resolve_provider: Some(true),
13204 ..Default::default()
13205 }),
13206 ..Default::default()
13207 },
13208 cx,
13209 )
13210 .await;
13211
13212 cx.set_state("fn main() { let a = 2ˇ; }");
13213 cx.simulate_keystroke(".");
13214
13215 let completion_data = default_data.clone();
13216 let completion_characters = default_commit_characters.clone();
13217 let completion_items = items.clone();
13218 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13219 let default_data = completion_data.clone();
13220 let default_commit_characters = completion_characters.clone();
13221 let items = completion_items.clone();
13222 async move {
13223 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
13224 items,
13225 item_defaults: Some(lsp::CompletionListItemDefaults {
13226 data: Some(default_data.clone()),
13227 commit_characters: Some(default_commit_characters.clone()),
13228 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
13229 default_edit_range,
13230 )),
13231 insert_text_format: Some(default_insert_text_format),
13232 insert_text_mode: Some(default_insert_text_mode),
13233 }),
13234 ..lsp::CompletionList::default()
13235 })))
13236 }
13237 })
13238 .next()
13239 .await;
13240
13241 let resolved_items = Arc::new(Mutex::new(Vec::new()));
13242 cx.lsp
13243 .server
13244 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13245 let closure_resolved_items = resolved_items.clone();
13246 move |item_to_resolve, _| {
13247 let closure_resolved_items = closure_resolved_items.clone();
13248 async move {
13249 closure_resolved_items.lock().push(item_to_resolve.clone());
13250 Ok(item_to_resolve)
13251 }
13252 }
13253 })
13254 .detach();
13255
13256 cx.condition(|editor, _| editor.context_menu_visible())
13257 .await;
13258 cx.run_until_parked();
13259 cx.update_editor(|editor, _, _| {
13260 let menu = editor.context_menu.borrow_mut();
13261 match menu.as_ref().expect("should have the completions menu") {
13262 CodeContextMenu::Completions(completions_menu) => {
13263 assert_eq!(
13264 completions_menu
13265 .entries
13266 .borrow()
13267 .iter()
13268 .map(|mat| mat.string.clone())
13269 .collect::<Vec<String>>(),
13270 items
13271 .iter()
13272 .map(|completion| completion.label.clone())
13273 .collect::<Vec<String>>()
13274 );
13275 }
13276 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
13277 }
13278 });
13279 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
13280 // with 4 from the end.
13281 assert_eq!(
13282 *resolved_items.lock(),
13283 [&items[0..16], &items[items.len() - 4..items.len()]]
13284 .concat()
13285 .iter()
13286 .cloned()
13287 .map(|mut item| {
13288 if item.data.is_none() {
13289 item.data = Some(default_data.clone());
13290 }
13291 item
13292 })
13293 .collect::<Vec<lsp::CompletionItem>>(),
13294 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
13295 );
13296 resolved_items.lock().clear();
13297
13298 cx.update_editor(|editor, window, cx| {
13299 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13300 });
13301 cx.run_until_parked();
13302 // Completions that have already been resolved are skipped.
13303 assert_eq!(
13304 *resolved_items.lock(),
13305 items[items.len() - 16..items.len() - 4]
13306 .iter()
13307 .cloned()
13308 .map(|mut item| {
13309 if item.data.is_none() {
13310 item.data = Some(default_data.clone());
13311 }
13312 item
13313 })
13314 .collect::<Vec<lsp::CompletionItem>>()
13315 );
13316 resolved_items.lock().clear();
13317}
13318
13319#[gpui::test]
13320async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
13321 init_test(cx, |_| {});
13322
13323 let mut cx = EditorLspTestContext::new(
13324 Language::new(
13325 LanguageConfig {
13326 matcher: LanguageMatcher {
13327 path_suffixes: vec!["jsx".into()],
13328 ..Default::default()
13329 },
13330 overrides: [(
13331 "element".into(),
13332 LanguageConfigOverride {
13333 completion_query_characters: Override::Set(['-'].into_iter().collect()),
13334 ..Default::default()
13335 },
13336 )]
13337 .into_iter()
13338 .collect(),
13339 ..Default::default()
13340 },
13341 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
13342 )
13343 .with_override_query("(jsx_self_closing_element) @element")
13344 .unwrap(),
13345 lsp::ServerCapabilities {
13346 completion_provider: Some(lsp::CompletionOptions {
13347 trigger_characters: Some(vec![":".to_string()]),
13348 ..Default::default()
13349 }),
13350 ..Default::default()
13351 },
13352 cx,
13353 )
13354 .await;
13355
13356 cx.lsp
13357 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13358 Ok(Some(lsp::CompletionResponse::Array(vec![
13359 lsp::CompletionItem {
13360 label: "bg-blue".into(),
13361 ..Default::default()
13362 },
13363 lsp::CompletionItem {
13364 label: "bg-red".into(),
13365 ..Default::default()
13366 },
13367 lsp::CompletionItem {
13368 label: "bg-yellow".into(),
13369 ..Default::default()
13370 },
13371 ])))
13372 });
13373
13374 cx.set_state(r#"<p class="bgˇ" />"#);
13375
13376 // Trigger completion when typing a dash, because the dash is an extra
13377 // word character in the 'element' scope, which contains the cursor.
13378 cx.simulate_keystroke("-");
13379 cx.executor().run_until_parked();
13380 cx.update_editor(|editor, _, _| {
13381 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13382 {
13383 assert_eq!(
13384 completion_menu_entries(&menu),
13385 &["bg-red", "bg-blue", "bg-yellow"]
13386 );
13387 } else {
13388 panic!("expected completion menu to be open");
13389 }
13390 });
13391
13392 cx.simulate_keystroke("l");
13393 cx.executor().run_until_parked();
13394 cx.update_editor(|editor, _, _| {
13395 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13396 {
13397 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
13398 } else {
13399 panic!("expected completion menu to be open");
13400 }
13401 });
13402
13403 // When filtering completions, consider the character after the '-' to
13404 // be the start of a subword.
13405 cx.set_state(r#"<p class="yelˇ" />"#);
13406 cx.simulate_keystroke("l");
13407 cx.executor().run_until_parked();
13408 cx.update_editor(|editor, _, _| {
13409 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
13410 {
13411 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
13412 } else {
13413 panic!("expected completion menu to be open");
13414 }
13415 });
13416}
13417
13418fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
13419 let entries = menu.entries.borrow();
13420 entries.iter().map(|mat| mat.string.clone()).collect()
13421}
13422
13423#[gpui::test]
13424async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
13425 init_test(cx, |settings| {
13426 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
13427 FormatterList(vec![Formatter::Prettier].into()),
13428 ))
13429 });
13430
13431 let fs = FakeFs::new(cx.executor());
13432 fs.insert_file(path!("/file.ts"), Default::default()).await;
13433
13434 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
13435 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13436
13437 language_registry.add(Arc::new(Language::new(
13438 LanguageConfig {
13439 name: "TypeScript".into(),
13440 matcher: LanguageMatcher {
13441 path_suffixes: vec!["ts".to_string()],
13442 ..Default::default()
13443 },
13444 ..Default::default()
13445 },
13446 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
13447 )));
13448 update_test_language_settings(cx, |settings| {
13449 settings.defaults.prettier = Some(PrettierSettings {
13450 allowed: true,
13451 ..PrettierSettings::default()
13452 });
13453 });
13454
13455 let test_plugin = "test_plugin";
13456 let _ = language_registry.register_fake_lsp(
13457 "TypeScript",
13458 FakeLspAdapter {
13459 prettier_plugins: vec![test_plugin],
13460 ..Default::default()
13461 },
13462 );
13463
13464 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
13465 let buffer = project
13466 .update(cx, |project, cx| {
13467 project.open_local_buffer(path!("/file.ts"), cx)
13468 })
13469 .await
13470 .unwrap();
13471
13472 let buffer_text = "one\ntwo\nthree\n";
13473 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
13474 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
13475 editor.update_in(cx, |editor, window, cx| {
13476 editor.set_text(buffer_text, window, cx)
13477 });
13478
13479 editor
13480 .update_in(cx, |editor, window, cx| {
13481 editor.perform_format(
13482 project.clone(),
13483 FormatTrigger::Manual,
13484 FormatTarget::Buffers,
13485 window,
13486 cx,
13487 )
13488 })
13489 .unwrap()
13490 .await;
13491 assert_eq!(
13492 editor.update(cx, |editor, cx| editor.text(cx)),
13493 buffer_text.to_string() + prettier_format_suffix,
13494 "Test prettier formatting was not applied to the original buffer text",
13495 );
13496
13497 update_test_language_settings(cx, |settings| {
13498 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
13499 });
13500 let format = editor.update_in(cx, |editor, window, cx| {
13501 editor.perform_format(
13502 project.clone(),
13503 FormatTrigger::Manual,
13504 FormatTarget::Buffers,
13505 window,
13506 cx,
13507 )
13508 });
13509 format.await.unwrap();
13510 assert_eq!(
13511 editor.update(cx, |editor, cx| editor.text(cx)),
13512 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
13513 "Autoformatting (via test prettier) was not applied to the original buffer text",
13514 );
13515}
13516
13517#[gpui::test]
13518async fn test_addition_reverts(cx: &mut TestAppContext) {
13519 init_test(cx, |_| {});
13520 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13521 let base_text = indoc! {r#"
13522 struct Row;
13523 struct Row1;
13524 struct Row2;
13525
13526 struct Row4;
13527 struct Row5;
13528 struct Row6;
13529
13530 struct Row8;
13531 struct Row9;
13532 struct Row10;"#};
13533
13534 // When addition hunks are not adjacent to carets, no hunk revert is performed
13535 assert_hunk_revert(
13536 indoc! {r#"struct Row;
13537 struct Row1;
13538 struct Row1.1;
13539 struct Row1.2;
13540 struct Row2;ˇ
13541
13542 struct Row4;
13543 struct Row5;
13544 struct Row6;
13545
13546 struct Row8;
13547 ˇstruct Row9;
13548 struct Row9.1;
13549 struct Row9.2;
13550 struct Row9.3;
13551 struct Row10;"#},
13552 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13553 indoc! {r#"struct Row;
13554 struct Row1;
13555 struct Row1.1;
13556 struct Row1.2;
13557 struct Row2;ˇ
13558
13559 struct Row4;
13560 struct Row5;
13561 struct Row6;
13562
13563 struct Row8;
13564 ˇstruct Row9;
13565 struct Row9.1;
13566 struct Row9.2;
13567 struct Row9.3;
13568 struct Row10;"#},
13569 base_text,
13570 &mut cx,
13571 );
13572 // Same for selections
13573 assert_hunk_revert(
13574 indoc! {r#"struct Row;
13575 struct Row1;
13576 struct Row2;
13577 struct Row2.1;
13578 struct Row2.2;
13579 «ˇ
13580 struct Row4;
13581 struct» Row5;
13582 «struct Row6;
13583 ˇ»
13584 struct Row9.1;
13585 struct Row9.2;
13586 struct Row9.3;
13587 struct Row8;
13588 struct Row9;
13589 struct Row10;"#},
13590 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
13591 indoc! {r#"struct Row;
13592 struct Row1;
13593 struct Row2;
13594 struct Row2.1;
13595 struct Row2.2;
13596 «ˇ
13597 struct Row4;
13598 struct» Row5;
13599 «struct Row6;
13600 ˇ»
13601 struct Row9.1;
13602 struct Row9.2;
13603 struct Row9.3;
13604 struct Row8;
13605 struct Row9;
13606 struct Row10;"#},
13607 base_text,
13608 &mut cx,
13609 );
13610
13611 // When carets and selections intersect the addition hunks, those are reverted.
13612 // Adjacent carets got merged.
13613 assert_hunk_revert(
13614 indoc! {r#"struct Row;
13615 ˇ// something on the top
13616 struct Row1;
13617 struct Row2;
13618 struct Roˇw3.1;
13619 struct Row2.2;
13620 struct Row2.3;ˇ
13621
13622 struct Row4;
13623 struct ˇRow5.1;
13624 struct Row5.2;
13625 struct «Rowˇ»5.3;
13626 struct Row5;
13627 struct Row6;
13628 ˇ
13629 struct Row9.1;
13630 struct «Rowˇ»9.2;
13631 struct «ˇRow»9.3;
13632 struct Row8;
13633 struct Row9;
13634 «ˇ// something on bottom»
13635 struct Row10;"#},
13636 vec![
13637 DiffHunkStatusKind::Added,
13638 DiffHunkStatusKind::Added,
13639 DiffHunkStatusKind::Added,
13640 DiffHunkStatusKind::Added,
13641 DiffHunkStatusKind::Added,
13642 ],
13643 indoc! {r#"struct Row;
13644 ˇstruct Row1;
13645 struct Row2;
13646 ˇ
13647 struct Row4;
13648 ˇstruct Row5;
13649 struct Row6;
13650 ˇ
13651 ˇstruct Row8;
13652 struct Row9;
13653 ˇstruct Row10;"#},
13654 base_text,
13655 &mut cx,
13656 );
13657}
13658
13659#[gpui::test]
13660async fn test_modification_reverts(cx: &mut TestAppContext) {
13661 init_test(cx, |_| {});
13662 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13663 let base_text = indoc! {r#"
13664 struct Row;
13665 struct Row1;
13666 struct Row2;
13667
13668 struct Row4;
13669 struct Row5;
13670 struct Row6;
13671
13672 struct Row8;
13673 struct Row9;
13674 struct Row10;"#};
13675
13676 // Modification hunks behave the same as the addition ones.
13677 assert_hunk_revert(
13678 indoc! {r#"struct Row;
13679 struct Row1;
13680 struct Row33;
13681 ˇ
13682 struct Row4;
13683 struct Row5;
13684 struct Row6;
13685 ˇ
13686 struct Row99;
13687 struct Row9;
13688 struct Row10;"#},
13689 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13690 indoc! {r#"struct Row;
13691 struct Row1;
13692 struct Row33;
13693 ˇ
13694 struct Row4;
13695 struct Row5;
13696 struct Row6;
13697 ˇ
13698 struct Row99;
13699 struct Row9;
13700 struct Row10;"#},
13701 base_text,
13702 &mut cx,
13703 );
13704 assert_hunk_revert(
13705 indoc! {r#"struct Row;
13706 struct Row1;
13707 struct Row33;
13708 «ˇ
13709 struct Row4;
13710 struct» Row5;
13711 «struct Row6;
13712 ˇ»
13713 struct Row99;
13714 struct Row9;
13715 struct Row10;"#},
13716 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
13717 indoc! {r#"struct Row;
13718 struct Row1;
13719 struct Row33;
13720 «ˇ
13721 struct Row4;
13722 struct» Row5;
13723 «struct Row6;
13724 ˇ»
13725 struct Row99;
13726 struct Row9;
13727 struct Row10;"#},
13728 base_text,
13729 &mut cx,
13730 );
13731
13732 assert_hunk_revert(
13733 indoc! {r#"ˇstruct Row1.1;
13734 struct Row1;
13735 «ˇstr»uct Row22;
13736
13737 struct ˇRow44;
13738 struct Row5;
13739 struct «Rˇ»ow66;ˇ
13740
13741 «struˇ»ct Row88;
13742 struct Row9;
13743 struct Row1011;ˇ"#},
13744 vec![
13745 DiffHunkStatusKind::Modified,
13746 DiffHunkStatusKind::Modified,
13747 DiffHunkStatusKind::Modified,
13748 DiffHunkStatusKind::Modified,
13749 DiffHunkStatusKind::Modified,
13750 DiffHunkStatusKind::Modified,
13751 ],
13752 indoc! {r#"struct Row;
13753 ˇstruct Row1;
13754 struct Row2;
13755 ˇ
13756 struct Row4;
13757 ˇstruct Row5;
13758 struct Row6;
13759 ˇ
13760 struct Row8;
13761 ˇstruct Row9;
13762 struct Row10;ˇ"#},
13763 base_text,
13764 &mut cx,
13765 );
13766}
13767
13768#[gpui::test]
13769async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
13770 init_test(cx, |_| {});
13771 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13772 let base_text = indoc! {r#"
13773 one
13774
13775 two
13776 three
13777 "#};
13778
13779 cx.set_head_text(base_text);
13780 cx.set_state("\nˇ\n");
13781 cx.executor().run_until_parked();
13782 cx.update_editor(|editor, _window, cx| {
13783 editor.expand_selected_diff_hunks(cx);
13784 });
13785 cx.executor().run_until_parked();
13786 cx.update_editor(|editor, window, cx| {
13787 editor.backspace(&Default::default(), window, cx);
13788 });
13789 cx.run_until_parked();
13790 cx.assert_state_with_diff(
13791 indoc! {r#"
13792
13793 - two
13794 - threeˇ
13795 +
13796 "#}
13797 .to_string(),
13798 );
13799}
13800
13801#[gpui::test]
13802async fn test_deletion_reverts(cx: &mut TestAppContext) {
13803 init_test(cx, |_| {});
13804 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
13805 let base_text = indoc! {r#"struct Row;
13806struct Row1;
13807struct Row2;
13808
13809struct Row4;
13810struct Row5;
13811struct Row6;
13812
13813struct Row8;
13814struct Row9;
13815struct Row10;"#};
13816
13817 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
13818 assert_hunk_revert(
13819 indoc! {r#"struct Row;
13820 struct Row2;
13821
13822 ˇstruct Row4;
13823 struct Row5;
13824 struct Row6;
13825 ˇ
13826 struct Row8;
13827 struct Row10;"#},
13828 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13829 indoc! {r#"struct Row;
13830 struct Row2;
13831
13832 ˇstruct Row4;
13833 struct Row5;
13834 struct Row6;
13835 ˇ
13836 struct Row8;
13837 struct Row10;"#},
13838 base_text,
13839 &mut cx,
13840 );
13841 assert_hunk_revert(
13842 indoc! {r#"struct Row;
13843 struct Row2;
13844
13845 «ˇstruct Row4;
13846 struct» Row5;
13847 «struct Row6;
13848 ˇ»
13849 struct Row8;
13850 struct Row10;"#},
13851 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13852 indoc! {r#"struct Row;
13853 struct Row2;
13854
13855 «ˇstruct Row4;
13856 struct» Row5;
13857 «struct Row6;
13858 ˇ»
13859 struct Row8;
13860 struct Row10;"#},
13861 base_text,
13862 &mut cx,
13863 );
13864
13865 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
13866 assert_hunk_revert(
13867 indoc! {r#"struct Row;
13868 ˇstruct Row2;
13869
13870 struct Row4;
13871 struct Row5;
13872 struct Row6;
13873
13874 struct Row8;ˇ
13875 struct Row10;"#},
13876 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
13877 indoc! {r#"struct Row;
13878 struct Row1;
13879 ˇstruct Row2;
13880
13881 struct Row4;
13882 struct Row5;
13883 struct Row6;
13884
13885 struct Row8;ˇ
13886 struct Row9;
13887 struct Row10;"#},
13888 base_text,
13889 &mut cx,
13890 );
13891 assert_hunk_revert(
13892 indoc! {r#"struct Row;
13893 struct Row2«ˇ;
13894 struct Row4;
13895 struct» Row5;
13896 «struct Row6;
13897
13898 struct Row8;ˇ»
13899 struct Row10;"#},
13900 vec![
13901 DiffHunkStatusKind::Deleted,
13902 DiffHunkStatusKind::Deleted,
13903 DiffHunkStatusKind::Deleted,
13904 ],
13905 indoc! {r#"struct Row;
13906 struct Row1;
13907 struct Row2«ˇ;
13908
13909 struct Row4;
13910 struct» Row5;
13911 «struct Row6;
13912
13913 struct Row8;ˇ»
13914 struct Row9;
13915 struct Row10;"#},
13916 base_text,
13917 &mut cx,
13918 );
13919}
13920
13921#[gpui::test]
13922async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
13923 init_test(cx, |_| {});
13924
13925 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
13926 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
13927 let base_text_3 =
13928 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
13929
13930 let text_1 = edit_first_char_of_every_line(base_text_1);
13931 let text_2 = edit_first_char_of_every_line(base_text_2);
13932 let text_3 = edit_first_char_of_every_line(base_text_3);
13933
13934 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
13935 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
13936 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
13937
13938 let multibuffer = cx.new(|cx| {
13939 let mut multibuffer = MultiBuffer::new(ReadWrite);
13940 multibuffer.push_excerpts(
13941 buffer_1.clone(),
13942 [
13943 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13944 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13945 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13946 ],
13947 cx,
13948 );
13949 multibuffer.push_excerpts(
13950 buffer_2.clone(),
13951 [
13952 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13953 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13954 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13955 ],
13956 cx,
13957 );
13958 multibuffer.push_excerpts(
13959 buffer_3.clone(),
13960 [
13961 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
13962 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
13963 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
13964 ],
13965 cx,
13966 );
13967 multibuffer
13968 });
13969
13970 let fs = FakeFs::new(cx.executor());
13971 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
13972 let (editor, cx) = cx
13973 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
13974 editor.update_in(cx, |editor, _window, cx| {
13975 for (buffer, diff_base) in [
13976 (buffer_1.clone(), base_text_1),
13977 (buffer_2.clone(), base_text_2),
13978 (buffer_3.clone(), base_text_3),
13979 ] {
13980 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
13981 editor
13982 .buffer
13983 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
13984 }
13985 });
13986 cx.executor().run_until_parked();
13987
13988 editor.update_in(cx, |editor, window, cx| {
13989 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}");
13990 editor.select_all(&SelectAll, window, cx);
13991 editor.git_restore(&Default::default(), window, cx);
13992 });
13993 cx.executor().run_until_parked();
13994
13995 // When all ranges are selected, all buffer hunks are reverted.
13996 editor.update(cx, |editor, cx| {
13997 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");
13998 });
13999 buffer_1.update(cx, |buffer, _| {
14000 assert_eq!(buffer.text(), base_text_1);
14001 });
14002 buffer_2.update(cx, |buffer, _| {
14003 assert_eq!(buffer.text(), base_text_2);
14004 });
14005 buffer_3.update(cx, |buffer, _| {
14006 assert_eq!(buffer.text(), base_text_3);
14007 });
14008
14009 editor.update_in(cx, |editor, window, cx| {
14010 editor.undo(&Default::default(), window, cx);
14011 });
14012
14013 editor.update_in(cx, |editor, window, cx| {
14014 editor.change_selections(None, window, cx, |s| {
14015 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14016 });
14017 editor.git_restore(&Default::default(), window, cx);
14018 });
14019
14020 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14021 // but not affect buffer_2 and its related excerpts.
14022 editor.update(cx, |editor, cx| {
14023 assert_eq!(
14024 editor.text(cx),
14025 "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}"
14026 );
14027 });
14028 buffer_1.update(cx, |buffer, _| {
14029 assert_eq!(buffer.text(), base_text_1);
14030 });
14031 buffer_2.update(cx, |buffer, _| {
14032 assert_eq!(
14033 buffer.text(),
14034 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14035 );
14036 });
14037 buffer_3.update(cx, |buffer, _| {
14038 assert_eq!(
14039 buffer.text(),
14040 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14041 );
14042 });
14043
14044 fn edit_first_char_of_every_line(text: &str) -> String {
14045 text.split('\n')
14046 .map(|line| format!("X{}", &line[1..]))
14047 .collect::<Vec<_>>()
14048 .join("\n")
14049 }
14050}
14051
14052#[gpui::test]
14053async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14054 init_test(cx, |_| {});
14055
14056 let cols = 4;
14057 let rows = 10;
14058 let sample_text_1 = sample_text(rows, cols, 'a');
14059 assert_eq!(
14060 sample_text_1,
14061 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14062 );
14063 let sample_text_2 = sample_text(rows, cols, 'l');
14064 assert_eq!(
14065 sample_text_2,
14066 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14067 );
14068 let sample_text_3 = sample_text(rows, cols, 'v');
14069 assert_eq!(
14070 sample_text_3,
14071 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14072 );
14073
14074 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14075 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14076 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14077
14078 let multi_buffer = cx.new(|cx| {
14079 let mut multibuffer = MultiBuffer::new(ReadWrite);
14080 multibuffer.push_excerpts(
14081 buffer_1.clone(),
14082 [
14083 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14084 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14085 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14086 ],
14087 cx,
14088 );
14089 multibuffer.push_excerpts(
14090 buffer_2.clone(),
14091 [
14092 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14093 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14094 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14095 ],
14096 cx,
14097 );
14098 multibuffer.push_excerpts(
14099 buffer_3.clone(),
14100 [
14101 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14102 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14103 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14104 ],
14105 cx,
14106 );
14107 multibuffer
14108 });
14109
14110 let fs = FakeFs::new(cx.executor());
14111 fs.insert_tree(
14112 "/a",
14113 json!({
14114 "main.rs": sample_text_1,
14115 "other.rs": sample_text_2,
14116 "lib.rs": sample_text_3,
14117 }),
14118 )
14119 .await;
14120 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14121 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14122 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14123 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14124 Editor::new(
14125 EditorMode::Full,
14126 multi_buffer,
14127 Some(project.clone()),
14128 window,
14129 cx,
14130 )
14131 });
14132 let multibuffer_item_id = workspace
14133 .update(cx, |workspace, window, cx| {
14134 assert!(
14135 workspace.active_item(cx).is_none(),
14136 "active item should be None before the first item is added"
14137 );
14138 workspace.add_item_to_active_pane(
14139 Box::new(multi_buffer_editor.clone()),
14140 None,
14141 true,
14142 window,
14143 cx,
14144 );
14145 let active_item = workspace
14146 .active_item(cx)
14147 .expect("should have an active item after adding the multi buffer");
14148 assert!(
14149 !active_item.is_singleton(cx),
14150 "A multi buffer was expected to active after adding"
14151 );
14152 active_item.item_id()
14153 })
14154 .unwrap();
14155 cx.executor().run_until_parked();
14156
14157 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14158 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14159 s.select_ranges(Some(1..2))
14160 });
14161 editor.open_excerpts(&OpenExcerpts, window, cx);
14162 });
14163 cx.executor().run_until_parked();
14164 let first_item_id = workspace
14165 .update(cx, |workspace, window, cx| {
14166 let active_item = workspace
14167 .active_item(cx)
14168 .expect("should have an active item after navigating into the 1st buffer");
14169 let first_item_id = active_item.item_id();
14170 assert_ne!(
14171 first_item_id, multibuffer_item_id,
14172 "Should navigate into the 1st buffer and activate it"
14173 );
14174 assert!(
14175 active_item.is_singleton(cx),
14176 "New active item should be a singleton buffer"
14177 );
14178 assert_eq!(
14179 active_item
14180 .act_as::<Editor>(cx)
14181 .expect("should have navigated into an editor for the 1st buffer")
14182 .read(cx)
14183 .text(cx),
14184 sample_text_1
14185 );
14186
14187 workspace
14188 .go_back(workspace.active_pane().downgrade(), window, cx)
14189 .detach_and_log_err(cx);
14190
14191 first_item_id
14192 })
14193 .unwrap();
14194 cx.executor().run_until_parked();
14195 workspace
14196 .update(cx, |workspace, _, cx| {
14197 let active_item = workspace
14198 .active_item(cx)
14199 .expect("should have an active item after navigating back");
14200 assert_eq!(
14201 active_item.item_id(),
14202 multibuffer_item_id,
14203 "Should navigate back to the multi buffer"
14204 );
14205 assert!(!active_item.is_singleton(cx));
14206 })
14207 .unwrap();
14208
14209 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14210 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14211 s.select_ranges(Some(39..40))
14212 });
14213 editor.open_excerpts(&OpenExcerpts, window, cx);
14214 });
14215 cx.executor().run_until_parked();
14216 let second_item_id = workspace
14217 .update(cx, |workspace, window, cx| {
14218 let active_item = workspace
14219 .active_item(cx)
14220 .expect("should have an active item after navigating into the 2nd buffer");
14221 let second_item_id = active_item.item_id();
14222 assert_ne!(
14223 second_item_id, multibuffer_item_id,
14224 "Should navigate away from the multibuffer"
14225 );
14226 assert_ne!(
14227 second_item_id, first_item_id,
14228 "Should navigate into the 2nd buffer and activate it"
14229 );
14230 assert!(
14231 active_item.is_singleton(cx),
14232 "New active item should be a singleton buffer"
14233 );
14234 assert_eq!(
14235 active_item
14236 .act_as::<Editor>(cx)
14237 .expect("should have navigated into an editor")
14238 .read(cx)
14239 .text(cx),
14240 sample_text_2
14241 );
14242
14243 workspace
14244 .go_back(workspace.active_pane().downgrade(), window, cx)
14245 .detach_and_log_err(cx);
14246
14247 second_item_id
14248 })
14249 .unwrap();
14250 cx.executor().run_until_parked();
14251 workspace
14252 .update(cx, |workspace, _, cx| {
14253 let active_item = workspace
14254 .active_item(cx)
14255 .expect("should have an active item after navigating back from the 2nd buffer");
14256 assert_eq!(
14257 active_item.item_id(),
14258 multibuffer_item_id,
14259 "Should navigate back from the 2nd buffer to the multi buffer"
14260 );
14261 assert!(!active_item.is_singleton(cx));
14262 })
14263 .unwrap();
14264
14265 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14266 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14267 s.select_ranges(Some(70..70))
14268 });
14269 editor.open_excerpts(&OpenExcerpts, window, cx);
14270 });
14271 cx.executor().run_until_parked();
14272 workspace
14273 .update(cx, |workspace, window, cx| {
14274 let active_item = workspace
14275 .active_item(cx)
14276 .expect("should have an active item after navigating into the 3rd buffer");
14277 let third_item_id = active_item.item_id();
14278 assert_ne!(
14279 third_item_id, multibuffer_item_id,
14280 "Should navigate into the 3rd buffer and activate it"
14281 );
14282 assert_ne!(third_item_id, first_item_id);
14283 assert_ne!(third_item_id, second_item_id);
14284 assert!(
14285 active_item.is_singleton(cx),
14286 "New active item should be a singleton buffer"
14287 );
14288 assert_eq!(
14289 active_item
14290 .act_as::<Editor>(cx)
14291 .expect("should have navigated into an editor")
14292 .read(cx)
14293 .text(cx),
14294 sample_text_3
14295 );
14296
14297 workspace
14298 .go_back(workspace.active_pane().downgrade(), window, cx)
14299 .detach_and_log_err(cx);
14300 })
14301 .unwrap();
14302 cx.executor().run_until_parked();
14303 workspace
14304 .update(cx, |workspace, _, cx| {
14305 let active_item = workspace
14306 .active_item(cx)
14307 .expect("should have an active item after navigating back from the 3rd buffer");
14308 assert_eq!(
14309 active_item.item_id(),
14310 multibuffer_item_id,
14311 "Should navigate back from the 3rd buffer to the multi buffer"
14312 );
14313 assert!(!active_item.is_singleton(cx));
14314 })
14315 .unwrap();
14316}
14317
14318#[gpui::test]
14319async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
14320 init_test(cx, |_| {});
14321
14322 let mut cx = EditorTestContext::new(cx).await;
14323
14324 let diff_base = r#"
14325 use some::mod;
14326
14327 const A: u32 = 42;
14328
14329 fn main() {
14330 println!("hello");
14331
14332 println!("world");
14333 }
14334 "#
14335 .unindent();
14336
14337 cx.set_state(
14338 &r#"
14339 use some::modified;
14340
14341 ˇ
14342 fn main() {
14343 println!("hello there");
14344
14345 println!("around the");
14346 println!("world");
14347 }
14348 "#
14349 .unindent(),
14350 );
14351
14352 cx.set_head_text(&diff_base);
14353 executor.run_until_parked();
14354
14355 cx.update_editor(|editor, window, cx| {
14356 editor.go_to_next_hunk(&GoToHunk, window, cx);
14357 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14358 });
14359 executor.run_until_parked();
14360 cx.assert_state_with_diff(
14361 r#"
14362 use some::modified;
14363
14364
14365 fn main() {
14366 - println!("hello");
14367 + ˇ println!("hello there");
14368
14369 println!("around the");
14370 println!("world");
14371 }
14372 "#
14373 .unindent(),
14374 );
14375
14376 cx.update_editor(|editor, window, cx| {
14377 for _ in 0..2 {
14378 editor.go_to_next_hunk(&GoToHunk, window, cx);
14379 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14380 }
14381 });
14382 executor.run_until_parked();
14383 cx.assert_state_with_diff(
14384 r#"
14385 - use some::mod;
14386 + ˇuse some::modified;
14387
14388
14389 fn main() {
14390 - println!("hello");
14391 + println!("hello there");
14392
14393 + println!("around the");
14394 println!("world");
14395 }
14396 "#
14397 .unindent(),
14398 );
14399
14400 cx.update_editor(|editor, window, cx| {
14401 editor.go_to_next_hunk(&GoToHunk, window, cx);
14402 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14403 });
14404 executor.run_until_parked();
14405 cx.assert_state_with_diff(
14406 r#"
14407 - use some::mod;
14408 + use some::modified;
14409
14410 - const A: u32 = 42;
14411 ˇ
14412 fn main() {
14413 - println!("hello");
14414 + println!("hello there");
14415
14416 + println!("around the");
14417 println!("world");
14418 }
14419 "#
14420 .unindent(),
14421 );
14422
14423 cx.update_editor(|editor, window, cx| {
14424 editor.cancel(&Cancel, window, cx);
14425 });
14426
14427 cx.assert_state_with_diff(
14428 r#"
14429 use some::modified;
14430
14431 ˇ
14432 fn main() {
14433 println!("hello there");
14434
14435 println!("around the");
14436 println!("world");
14437 }
14438 "#
14439 .unindent(),
14440 );
14441}
14442
14443#[gpui::test]
14444async fn test_diff_base_change_with_expanded_diff_hunks(
14445 executor: BackgroundExecutor,
14446 cx: &mut TestAppContext,
14447) {
14448 init_test(cx, |_| {});
14449
14450 let mut cx = EditorTestContext::new(cx).await;
14451
14452 let diff_base = r#"
14453 use some::mod1;
14454 use some::mod2;
14455
14456 const A: u32 = 42;
14457 const B: u32 = 42;
14458 const C: u32 = 42;
14459
14460 fn main() {
14461 println!("hello");
14462
14463 println!("world");
14464 }
14465 "#
14466 .unindent();
14467
14468 cx.set_state(
14469 &r#"
14470 use some::mod2;
14471
14472 const A: u32 = 42;
14473 const C: u32 = 42;
14474
14475 fn main(ˇ) {
14476 //println!("hello");
14477
14478 println!("world");
14479 //
14480 //
14481 }
14482 "#
14483 .unindent(),
14484 );
14485
14486 cx.set_head_text(&diff_base);
14487 executor.run_until_parked();
14488
14489 cx.update_editor(|editor, window, cx| {
14490 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14491 });
14492 executor.run_until_parked();
14493 cx.assert_state_with_diff(
14494 r#"
14495 - use some::mod1;
14496 use some::mod2;
14497
14498 const A: u32 = 42;
14499 - const B: u32 = 42;
14500 const C: u32 = 42;
14501
14502 fn main(ˇ) {
14503 - println!("hello");
14504 + //println!("hello");
14505
14506 println!("world");
14507 + //
14508 + //
14509 }
14510 "#
14511 .unindent(),
14512 );
14513
14514 cx.set_head_text("new diff base!");
14515 executor.run_until_parked();
14516 cx.assert_state_with_diff(
14517 r#"
14518 - new diff base!
14519 + use some::mod2;
14520 +
14521 + const A: u32 = 42;
14522 + const C: u32 = 42;
14523 +
14524 + fn main(ˇ) {
14525 + //println!("hello");
14526 +
14527 + println!("world");
14528 + //
14529 + //
14530 + }
14531 "#
14532 .unindent(),
14533 );
14534}
14535
14536#[gpui::test]
14537async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
14538 init_test(cx, |_| {});
14539
14540 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14541 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
14542 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14543 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
14544 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
14545 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
14546
14547 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
14548 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
14549 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
14550
14551 let multi_buffer = cx.new(|cx| {
14552 let mut multibuffer = MultiBuffer::new(ReadWrite);
14553 multibuffer.push_excerpts(
14554 buffer_1.clone(),
14555 [
14556 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14557 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14558 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14559 ],
14560 cx,
14561 );
14562 multibuffer.push_excerpts(
14563 buffer_2.clone(),
14564 [
14565 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14566 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14567 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14568 ],
14569 cx,
14570 );
14571 multibuffer.push_excerpts(
14572 buffer_3.clone(),
14573 [
14574 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14575 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14576 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
14577 ],
14578 cx,
14579 );
14580 multibuffer
14581 });
14582
14583 let editor =
14584 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14585 editor
14586 .update(cx, |editor, _window, cx| {
14587 for (buffer, diff_base) in [
14588 (buffer_1.clone(), file_1_old),
14589 (buffer_2.clone(), file_2_old),
14590 (buffer_3.clone(), file_3_old),
14591 ] {
14592 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14593 editor
14594 .buffer
14595 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14596 }
14597 })
14598 .unwrap();
14599
14600 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14601 cx.run_until_parked();
14602
14603 cx.assert_editor_state(
14604 &"
14605 ˇaaa
14606 ccc
14607 ddd
14608
14609 ggg
14610 hhh
14611
14612
14613 lll
14614 mmm
14615 NNN
14616
14617 qqq
14618 rrr
14619
14620 uuu
14621 111
14622 222
14623 333
14624
14625 666
14626 777
14627
14628 000
14629 !!!"
14630 .unindent(),
14631 );
14632
14633 cx.update_editor(|editor, window, cx| {
14634 editor.select_all(&SelectAll, window, cx);
14635 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
14636 });
14637 cx.executor().run_until_parked();
14638
14639 cx.assert_state_with_diff(
14640 "
14641 «aaa
14642 - bbb
14643 ccc
14644 ddd
14645
14646 ggg
14647 hhh
14648
14649
14650 lll
14651 mmm
14652 - nnn
14653 + NNN
14654
14655 qqq
14656 rrr
14657
14658 uuu
14659 111
14660 222
14661 333
14662
14663 + 666
14664 777
14665
14666 000
14667 !!!ˇ»"
14668 .unindent(),
14669 );
14670}
14671
14672#[gpui::test]
14673async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
14674 init_test(cx, |_| {});
14675
14676 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
14677 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
14678
14679 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
14680 let multi_buffer = cx.new(|cx| {
14681 let mut multibuffer = MultiBuffer::new(ReadWrite);
14682 multibuffer.push_excerpts(
14683 buffer.clone(),
14684 [
14685 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
14686 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
14687 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
14688 ],
14689 cx,
14690 );
14691 multibuffer
14692 });
14693
14694 let editor =
14695 cx.add_window(|window, cx| Editor::new(EditorMode::Full, multi_buffer, None, window, cx));
14696 editor
14697 .update(cx, |editor, _window, cx| {
14698 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
14699 editor
14700 .buffer
14701 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
14702 })
14703 .unwrap();
14704
14705 let mut cx = EditorTestContext::for_editor(editor, cx).await;
14706 cx.run_until_parked();
14707
14708 cx.update_editor(|editor, window, cx| {
14709 editor.expand_all_diff_hunks(&Default::default(), window, cx)
14710 });
14711 cx.executor().run_until_parked();
14712
14713 // When the start of a hunk coincides with the start of its excerpt,
14714 // the hunk is expanded. When the start of a a hunk is earlier than
14715 // the start of its excerpt, the hunk is not expanded.
14716 cx.assert_state_with_diff(
14717 "
14718 ˇaaa
14719 - bbb
14720 + BBB
14721
14722 - ddd
14723 - eee
14724 + DDD
14725 + EEE
14726 fff
14727
14728 iii
14729 "
14730 .unindent(),
14731 );
14732}
14733
14734#[gpui::test]
14735async fn test_edits_around_expanded_insertion_hunks(
14736 executor: BackgroundExecutor,
14737 cx: &mut TestAppContext,
14738) {
14739 init_test(cx, |_| {});
14740
14741 let mut cx = EditorTestContext::new(cx).await;
14742
14743 let diff_base = r#"
14744 use some::mod1;
14745 use some::mod2;
14746
14747 const A: u32 = 42;
14748
14749 fn main() {
14750 println!("hello");
14751
14752 println!("world");
14753 }
14754 "#
14755 .unindent();
14756 executor.run_until_parked();
14757 cx.set_state(
14758 &r#"
14759 use some::mod1;
14760 use some::mod2;
14761
14762 const A: u32 = 42;
14763 const B: u32 = 42;
14764 const C: u32 = 42;
14765 ˇ
14766
14767 fn main() {
14768 println!("hello");
14769
14770 println!("world");
14771 }
14772 "#
14773 .unindent(),
14774 );
14775
14776 cx.set_head_text(&diff_base);
14777 executor.run_until_parked();
14778
14779 cx.update_editor(|editor, window, cx| {
14780 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
14781 });
14782 executor.run_until_parked();
14783
14784 cx.assert_state_with_diff(
14785 r#"
14786 use some::mod1;
14787 use some::mod2;
14788
14789 const A: u32 = 42;
14790 + const B: u32 = 42;
14791 + const C: u32 = 42;
14792 + ˇ
14793
14794 fn main() {
14795 println!("hello");
14796
14797 println!("world");
14798 }
14799 "#
14800 .unindent(),
14801 );
14802
14803 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
14804 executor.run_until_parked();
14805
14806 cx.assert_state_with_diff(
14807 r#"
14808 use some::mod1;
14809 use some::mod2;
14810
14811 const A: u32 = 42;
14812 + const B: u32 = 42;
14813 + const C: u32 = 42;
14814 + const D: u32 = 42;
14815 + ˇ
14816
14817 fn main() {
14818 println!("hello");
14819
14820 println!("world");
14821 }
14822 "#
14823 .unindent(),
14824 );
14825
14826 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
14827 executor.run_until_parked();
14828
14829 cx.assert_state_with_diff(
14830 r#"
14831 use some::mod1;
14832 use some::mod2;
14833
14834 const A: u32 = 42;
14835 + const B: u32 = 42;
14836 + const C: u32 = 42;
14837 + const D: u32 = 42;
14838 + const E: u32 = 42;
14839 + ˇ
14840
14841 fn main() {
14842 println!("hello");
14843
14844 println!("world");
14845 }
14846 "#
14847 .unindent(),
14848 );
14849
14850 cx.update_editor(|editor, window, cx| {
14851 editor.delete_line(&DeleteLine, window, cx);
14852 });
14853 executor.run_until_parked();
14854
14855 cx.assert_state_with_diff(
14856 r#"
14857 use some::mod1;
14858 use some::mod2;
14859
14860 const A: u32 = 42;
14861 + const B: u32 = 42;
14862 + const C: u32 = 42;
14863 + const D: u32 = 42;
14864 + const E: u32 = 42;
14865 ˇ
14866 fn main() {
14867 println!("hello");
14868
14869 println!("world");
14870 }
14871 "#
14872 .unindent(),
14873 );
14874
14875 cx.update_editor(|editor, window, cx| {
14876 editor.move_up(&MoveUp, window, cx);
14877 editor.delete_line(&DeleteLine, window, cx);
14878 editor.move_up(&MoveUp, window, cx);
14879 editor.delete_line(&DeleteLine, window, cx);
14880 editor.move_up(&MoveUp, window, cx);
14881 editor.delete_line(&DeleteLine, window, cx);
14882 });
14883 executor.run_until_parked();
14884 cx.assert_state_with_diff(
14885 r#"
14886 use some::mod1;
14887 use some::mod2;
14888
14889 const A: u32 = 42;
14890 + const B: u32 = 42;
14891 ˇ
14892 fn main() {
14893 println!("hello");
14894
14895 println!("world");
14896 }
14897 "#
14898 .unindent(),
14899 );
14900
14901 cx.update_editor(|editor, window, cx| {
14902 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
14903 editor.delete_line(&DeleteLine, window, cx);
14904 });
14905 executor.run_until_parked();
14906 cx.assert_state_with_diff(
14907 r#"
14908 ˇ
14909 fn main() {
14910 println!("hello");
14911
14912 println!("world");
14913 }
14914 "#
14915 .unindent(),
14916 );
14917}
14918
14919#[gpui::test]
14920async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
14921 init_test(cx, |_| {});
14922
14923 let mut cx = EditorTestContext::new(cx).await;
14924 cx.set_head_text(indoc! { "
14925 one
14926 two
14927 three
14928 four
14929 five
14930 "
14931 });
14932 cx.set_state(indoc! { "
14933 one
14934 ˇthree
14935 five
14936 "});
14937 cx.run_until_parked();
14938 cx.update_editor(|editor, window, cx| {
14939 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14940 });
14941 cx.assert_state_with_diff(
14942 indoc! { "
14943 one
14944 - two
14945 ˇthree
14946 - four
14947 five
14948 "}
14949 .to_string(),
14950 );
14951 cx.update_editor(|editor, window, cx| {
14952 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14953 });
14954
14955 cx.assert_state_with_diff(
14956 indoc! { "
14957 one
14958 ˇthree
14959 five
14960 "}
14961 .to_string(),
14962 );
14963
14964 cx.set_state(indoc! { "
14965 one
14966 ˇTWO
14967 three
14968 four
14969 five
14970 "});
14971 cx.run_until_parked();
14972 cx.update_editor(|editor, window, cx| {
14973 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14974 });
14975
14976 cx.assert_state_with_diff(
14977 indoc! { "
14978 one
14979 - two
14980 + ˇTWO
14981 three
14982 four
14983 five
14984 "}
14985 .to_string(),
14986 );
14987 cx.update_editor(|editor, window, cx| {
14988 editor.move_up(&Default::default(), window, cx);
14989 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
14990 });
14991 cx.assert_state_with_diff(
14992 indoc! { "
14993 one
14994 ˇTWO
14995 three
14996 four
14997 five
14998 "}
14999 .to_string(),
15000 );
15001}
15002
15003#[gpui::test]
15004async fn test_edits_around_expanded_deletion_hunks(
15005 executor: BackgroundExecutor,
15006 cx: &mut TestAppContext,
15007) {
15008 init_test(cx, |_| {});
15009
15010 let mut cx = EditorTestContext::new(cx).await;
15011
15012 let diff_base = r#"
15013 use some::mod1;
15014 use some::mod2;
15015
15016 const A: u32 = 42;
15017 const B: u32 = 42;
15018 const C: u32 = 42;
15019
15020
15021 fn main() {
15022 println!("hello");
15023
15024 println!("world");
15025 }
15026 "#
15027 .unindent();
15028 executor.run_until_parked();
15029 cx.set_state(
15030 &r#"
15031 use some::mod1;
15032 use some::mod2;
15033
15034 ˇconst B: u32 = 42;
15035 const C: u32 = 42;
15036
15037
15038 fn main() {
15039 println!("hello");
15040
15041 println!("world");
15042 }
15043 "#
15044 .unindent(),
15045 );
15046
15047 cx.set_head_text(&diff_base);
15048 executor.run_until_parked();
15049
15050 cx.update_editor(|editor, window, cx| {
15051 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15052 });
15053 executor.run_until_parked();
15054
15055 cx.assert_state_with_diff(
15056 r#"
15057 use some::mod1;
15058 use some::mod2;
15059
15060 - const A: u32 = 42;
15061 ˇconst B: u32 = 42;
15062 const C: u32 = 42;
15063
15064
15065 fn main() {
15066 println!("hello");
15067
15068 println!("world");
15069 }
15070 "#
15071 .unindent(),
15072 );
15073
15074 cx.update_editor(|editor, window, cx| {
15075 editor.delete_line(&DeleteLine, window, cx);
15076 });
15077 executor.run_until_parked();
15078 cx.assert_state_with_diff(
15079 r#"
15080 use some::mod1;
15081 use some::mod2;
15082
15083 - const A: u32 = 42;
15084 - const B: u32 = 42;
15085 ˇconst C: u32 = 42;
15086
15087
15088 fn main() {
15089 println!("hello");
15090
15091 println!("world");
15092 }
15093 "#
15094 .unindent(),
15095 );
15096
15097 cx.update_editor(|editor, window, cx| {
15098 editor.delete_line(&DeleteLine, window, cx);
15099 });
15100 executor.run_until_parked();
15101 cx.assert_state_with_diff(
15102 r#"
15103 use some::mod1;
15104 use some::mod2;
15105
15106 - const A: u32 = 42;
15107 - const B: u32 = 42;
15108 - const C: u32 = 42;
15109 ˇ
15110
15111 fn main() {
15112 println!("hello");
15113
15114 println!("world");
15115 }
15116 "#
15117 .unindent(),
15118 );
15119
15120 cx.update_editor(|editor, window, cx| {
15121 editor.handle_input("replacement", window, cx);
15122 });
15123 executor.run_until_parked();
15124 cx.assert_state_with_diff(
15125 r#"
15126 use some::mod1;
15127 use some::mod2;
15128
15129 - const A: u32 = 42;
15130 - const B: u32 = 42;
15131 - const C: u32 = 42;
15132 -
15133 + replacementˇ
15134
15135 fn main() {
15136 println!("hello");
15137
15138 println!("world");
15139 }
15140 "#
15141 .unindent(),
15142 );
15143}
15144
15145#[gpui::test]
15146async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15147 init_test(cx, |_| {});
15148
15149 let mut cx = EditorTestContext::new(cx).await;
15150
15151 let base_text = r#"
15152 one
15153 two
15154 three
15155 four
15156 five
15157 "#
15158 .unindent();
15159 executor.run_until_parked();
15160 cx.set_state(
15161 &r#"
15162 one
15163 two
15164 fˇour
15165 five
15166 "#
15167 .unindent(),
15168 );
15169
15170 cx.set_head_text(&base_text);
15171 executor.run_until_parked();
15172
15173 cx.update_editor(|editor, window, cx| {
15174 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15175 });
15176 executor.run_until_parked();
15177
15178 cx.assert_state_with_diff(
15179 r#"
15180 one
15181 two
15182 - three
15183 fˇour
15184 five
15185 "#
15186 .unindent(),
15187 );
15188
15189 cx.update_editor(|editor, window, cx| {
15190 editor.backspace(&Backspace, window, cx);
15191 editor.backspace(&Backspace, window, cx);
15192 });
15193 executor.run_until_parked();
15194 cx.assert_state_with_diff(
15195 r#"
15196 one
15197 two
15198 - threeˇ
15199 - four
15200 + our
15201 five
15202 "#
15203 .unindent(),
15204 );
15205}
15206
15207#[gpui::test]
15208async fn test_edit_after_expanded_modification_hunk(
15209 executor: BackgroundExecutor,
15210 cx: &mut TestAppContext,
15211) {
15212 init_test(cx, |_| {});
15213
15214 let mut cx = EditorTestContext::new(cx).await;
15215
15216 let diff_base = r#"
15217 use some::mod1;
15218 use some::mod2;
15219
15220 const A: u32 = 42;
15221 const B: u32 = 42;
15222 const C: u32 = 42;
15223 const D: u32 = 42;
15224
15225
15226 fn main() {
15227 println!("hello");
15228
15229 println!("world");
15230 }"#
15231 .unindent();
15232
15233 cx.set_state(
15234 &r#"
15235 use some::mod1;
15236 use some::mod2;
15237
15238 const A: u32 = 42;
15239 const B: u32 = 42;
15240 const C: u32 = 43ˇ
15241 const D: u32 = 42;
15242
15243
15244 fn main() {
15245 println!("hello");
15246
15247 println!("world");
15248 }"#
15249 .unindent(),
15250 );
15251
15252 cx.set_head_text(&diff_base);
15253 executor.run_until_parked();
15254 cx.update_editor(|editor, window, cx| {
15255 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15256 });
15257 executor.run_until_parked();
15258
15259 cx.assert_state_with_diff(
15260 r#"
15261 use some::mod1;
15262 use some::mod2;
15263
15264 const A: u32 = 42;
15265 const B: u32 = 42;
15266 - const C: u32 = 42;
15267 + const C: u32 = 43ˇ
15268 const D: u32 = 42;
15269
15270
15271 fn main() {
15272 println!("hello");
15273
15274 println!("world");
15275 }"#
15276 .unindent(),
15277 );
15278
15279 cx.update_editor(|editor, window, cx| {
15280 editor.handle_input("\nnew_line\n", window, cx);
15281 });
15282 executor.run_until_parked();
15283
15284 cx.assert_state_with_diff(
15285 r#"
15286 use some::mod1;
15287 use some::mod2;
15288
15289 const A: u32 = 42;
15290 const B: u32 = 42;
15291 - const C: u32 = 42;
15292 + const C: u32 = 43
15293 + new_line
15294 + ˇ
15295 const D: u32 = 42;
15296
15297
15298 fn main() {
15299 println!("hello");
15300
15301 println!("world");
15302 }"#
15303 .unindent(),
15304 );
15305}
15306
15307#[gpui::test]
15308async fn test_stage_and_unstage_added_file_hunk(
15309 executor: BackgroundExecutor,
15310 cx: &mut TestAppContext,
15311) {
15312 init_test(cx, |_| {});
15313
15314 let mut cx = EditorTestContext::new(cx).await;
15315 cx.update_editor(|editor, _, cx| {
15316 editor.set_expand_all_diff_hunks(cx);
15317 });
15318
15319 let working_copy = r#"
15320 ˇfn main() {
15321 println!("hello, world!");
15322 }
15323 "#
15324 .unindent();
15325
15326 cx.set_state(&working_copy);
15327 executor.run_until_parked();
15328
15329 cx.assert_state_with_diff(
15330 r#"
15331 + ˇfn main() {
15332 + println!("hello, world!");
15333 + }
15334 "#
15335 .unindent(),
15336 );
15337 cx.assert_index_text(None);
15338
15339 cx.update_editor(|editor, window, cx| {
15340 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15341 });
15342 executor.run_until_parked();
15343 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
15344 cx.assert_state_with_diff(
15345 r#"
15346 + ˇfn main() {
15347 + println!("hello, world!");
15348 + }
15349 "#
15350 .unindent(),
15351 );
15352
15353 cx.update_editor(|editor, window, cx| {
15354 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
15355 });
15356 executor.run_until_parked();
15357 cx.assert_index_text(None);
15358}
15359
15360async fn setup_indent_guides_editor(
15361 text: &str,
15362 cx: &mut TestAppContext,
15363) -> (BufferId, EditorTestContext) {
15364 init_test(cx, |_| {});
15365
15366 let mut cx = EditorTestContext::new(cx).await;
15367
15368 let buffer_id = cx.update_editor(|editor, window, cx| {
15369 editor.set_text(text, window, cx);
15370 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
15371
15372 buffer_ids[0]
15373 });
15374
15375 (buffer_id, cx)
15376}
15377
15378fn assert_indent_guides(
15379 range: Range<u32>,
15380 expected: Vec<IndentGuide>,
15381 active_indices: Option<Vec<usize>>,
15382 cx: &mut EditorTestContext,
15383) {
15384 let indent_guides = cx.update_editor(|editor, window, cx| {
15385 let snapshot = editor.snapshot(window, cx).display_snapshot;
15386 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
15387 editor,
15388 MultiBufferRow(range.start)..MultiBufferRow(range.end),
15389 true,
15390 &snapshot,
15391 cx,
15392 );
15393
15394 indent_guides.sort_by(|a, b| {
15395 a.depth.cmp(&b.depth).then(
15396 a.start_row
15397 .cmp(&b.start_row)
15398 .then(a.end_row.cmp(&b.end_row)),
15399 )
15400 });
15401 indent_guides
15402 });
15403
15404 if let Some(expected) = active_indices {
15405 let active_indices = cx.update_editor(|editor, window, cx| {
15406 let snapshot = editor.snapshot(window, cx).display_snapshot;
15407 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
15408 });
15409
15410 assert_eq!(
15411 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
15412 expected,
15413 "Active indent guide indices do not match"
15414 );
15415 }
15416
15417 assert_eq!(indent_guides, expected, "Indent guides do not match");
15418}
15419
15420fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
15421 IndentGuide {
15422 buffer_id,
15423 start_row: MultiBufferRow(start_row),
15424 end_row: MultiBufferRow(end_row),
15425 depth,
15426 tab_size: 4,
15427 settings: IndentGuideSettings {
15428 enabled: true,
15429 line_width: 1,
15430 active_line_width: 1,
15431 ..Default::default()
15432 },
15433 }
15434}
15435
15436#[gpui::test]
15437async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
15438 let (buffer_id, mut cx) = setup_indent_guides_editor(
15439 &"
15440 fn main() {
15441 let a = 1;
15442 }"
15443 .unindent(),
15444 cx,
15445 )
15446 .await;
15447
15448 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15449}
15450
15451#[gpui::test]
15452async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
15453 let (buffer_id, mut cx) = setup_indent_guides_editor(
15454 &"
15455 fn main() {
15456 let a = 1;
15457 let b = 2;
15458 }"
15459 .unindent(),
15460 cx,
15461 )
15462 .await;
15463
15464 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
15465}
15466
15467#[gpui::test]
15468async fn test_indent_guide_nested(cx: &mut TestAppContext) {
15469 let (buffer_id, mut cx) = setup_indent_guides_editor(
15470 &"
15471 fn main() {
15472 let a = 1;
15473 if a == 3 {
15474 let b = 2;
15475 } else {
15476 let c = 3;
15477 }
15478 }"
15479 .unindent(),
15480 cx,
15481 )
15482 .await;
15483
15484 assert_indent_guides(
15485 0..8,
15486 vec![
15487 indent_guide(buffer_id, 1, 6, 0),
15488 indent_guide(buffer_id, 3, 3, 1),
15489 indent_guide(buffer_id, 5, 5, 1),
15490 ],
15491 None,
15492 &mut cx,
15493 );
15494}
15495
15496#[gpui::test]
15497async fn test_indent_guide_tab(cx: &mut TestAppContext) {
15498 let (buffer_id, mut cx) = setup_indent_guides_editor(
15499 &"
15500 fn main() {
15501 let a = 1;
15502 let b = 2;
15503 let c = 3;
15504 }"
15505 .unindent(),
15506 cx,
15507 )
15508 .await;
15509
15510 assert_indent_guides(
15511 0..5,
15512 vec![
15513 indent_guide(buffer_id, 1, 3, 0),
15514 indent_guide(buffer_id, 2, 2, 1),
15515 ],
15516 None,
15517 &mut cx,
15518 );
15519}
15520
15521#[gpui::test]
15522async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
15523 let (buffer_id, mut cx) = setup_indent_guides_editor(
15524 &"
15525 fn main() {
15526 let a = 1;
15527
15528 let c = 3;
15529 }"
15530 .unindent(),
15531 cx,
15532 )
15533 .await;
15534
15535 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
15536}
15537
15538#[gpui::test]
15539async fn test_indent_guide_complex(cx: &mut TestAppContext) {
15540 let (buffer_id, mut cx) = setup_indent_guides_editor(
15541 &"
15542 fn main() {
15543 let a = 1;
15544
15545 let c = 3;
15546
15547 if a == 3 {
15548 let b = 2;
15549 } else {
15550 let c = 3;
15551 }
15552 }"
15553 .unindent(),
15554 cx,
15555 )
15556 .await;
15557
15558 assert_indent_guides(
15559 0..11,
15560 vec![
15561 indent_guide(buffer_id, 1, 9, 0),
15562 indent_guide(buffer_id, 6, 6, 1),
15563 indent_guide(buffer_id, 8, 8, 1),
15564 ],
15565 None,
15566 &mut cx,
15567 );
15568}
15569
15570#[gpui::test]
15571async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
15572 let (buffer_id, mut cx) = setup_indent_guides_editor(
15573 &"
15574 fn main() {
15575 let a = 1;
15576
15577 let c = 3;
15578
15579 if a == 3 {
15580 let b = 2;
15581 } else {
15582 let c = 3;
15583 }
15584 }"
15585 .unindent(),
15586 cx,
15587 )
15588 .await;
15589
15590 assert_indent_guides(
15591 1..11,
15592 vec![
15593 indent_guide(buffer_id, 1, 9, 0),
15594 indent_guide(buffer_id, 6, 6, 1),
15595 indent_guide(buffer_id, 8, 8, 1),
15596 ],
15597 None,
15598 &mut cx,
15599 );
15600}
15601
15602#[gpui::test]
15603async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
15604 let (buffer_id, mut cx) = setup_indent_guides_editor(
15605 &"
15606 fn main() {
15607 let a = 1;
15608
15609 let c = 3;
15610
15611 if a == 3 {
15612 let b = 2;
15613 } else {
15614 let c = 3;
15615 }
15616 }"
15617 .unindent(),
15618 cx,
15619 )
15620 .await;
15621
15622 assert_indent_guides(
15623 1..10,
15624 vec![
15625 indent_guide(buffer_id, 1, 9, 0),
15626 indent_guide(buffer_id, 6, 6, 1),
15627 indent_guide(buffer_id, 8, 8, 1),
15628 ],
15629 None,
15630 &mut cx,
15631 );
15632}
15633
15634#[gpui::test]
15635async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
15636 let (buffer_id, mut cx) = setup_indent_guides_editor(
15637 &"
15638 block1
15639 block2
15640 block3
15641 block4
15642 block2
15643 block1
15644 block1"
15645 .unindent(),
15646 cx,
15647 )
15648 .await;
15649
15650 assert_indent_guides(
15651 1..10,
15652 vec![
15653 indent_guide(buffer_id, 1, 4, 0),
15654 indent_guide(buffer_id, 2, 3, 1),
15655 indent_guide(buffer_id, 3, 3, 2),
15656 ],
15657 None,
15658 &mut cx,
15659 );
15660}
15661
15662#[gpui::test]
15663async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
15664 let (buffer_id, mut cx) = setup_indent_guides_editor(
15665 &"
15666 block1
15667 block2
15668 block3
15669
15670 block1
15671 block1"
15672 .unindent(),
15673 cx,
15674 )
15675 .await;
15676
15677 assert_indent_guides(
15678 0..6,
15679 vec![
15680 indent_guide(buffer_id, 1, 2, 0),
15681 indent_guide(buffer_id, 2, 2, 1),
15682 ],
15683 None,
15684 &mut cx,
15685 );
15686}
15687
15688#[gpui::test]
15689async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
15690 let (buffer_id, mut cx) = setup_indent_guides_editor(
15691 &"
15692 block1
15693
15694
15695
15696 block2
15697 "
15698 .unindent(),
15699 cx,
15700 )
15701 .await;
15702
15703 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
15704}
15705
15706#[gpui::test]
15707async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
15708 let (buffer_id, mut cx) = setup_indent_guides_editor(
15709 &"
15710 def a:
15711 \tb = 3
15712 \tif True:
15713 \t\tc = 4
15714 \t\td = 5
15715 \tprint(b)
15716 "
15717 .unindent(),
15718 cx,
15719 )
15720 .await;
15721
15722 assert_indent_guides(
15723 0..6,
15724 vec![
15725 indent_guide(buffer_id, 1, 6, 0),
15726 indent_guide(buffer_id, 3, 4, 1),
15727 ],
15728 None,
15729 &mut cx,
15730 );
15731}
15732
15733#[gpui::test]
15734async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
15735 let (buffer_id, mut cx) = setup_indent_guides_editor(
15736 &"
15737 fn main() {
15738 let a = 1;
15739 }"
15740 .unindent(),
15741 cx,
15742 )
15743 .await;
15744
15745 cx.update_editor(|editor, window, cx| {
15746 editor.change_selections(None, window, cx, |s| {
15747 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15748 });
15749 });
15750
15751 assert_indent_guides(
15752 0..3,
15753 vec![indent_guide(buffer_id, 1, 1, 0)],
15754 Some(vec![0]),
15755 &mut cx,
15756 );
15757}
15758
15759#[gpui::test]
15760async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
15761 let (buffer_id, mut cx) = setup_indent_guides_editor(
15762 &"
15763 fn main() {
15764 if 1 == 2 {
15765 let a = 1;
15766 }
15767 }"
15768 .unindent(),
15769 cx,
15770 )
15771 .await;
15772
15773 cx.update_editor(|editor, window, cx| {
15774 editor.change_selections(None, window, cx, |s| {
15775 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15776 });
15777 });
15778
15779 assert_indent_guides(
15780 0..4,
15781 vec![
15782 indent_guide(buffer_id, 1, 3, 0),
15783 indent_guide(buffer_id, 2, 2, 1),
15784 ],
15785 Some(vec![1]),
15786 &mut cx,
15787 );
15788
15789 cx.update_editor(|editor, window, cx| {
15790 editor.change_selections(None, window, cx, |s| {
15791 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15792 });
15793 });
15794
15795 assert_indent_guides(
15796 0..4,
15797 vec![
15798 indent_guide(buffer_id, 1, 3, 0),
15799 indent_guide(buffer_id, 2, 2, 1),
15800 ],
15801 Some(vec![1]),
15802 &mut cx,
15803 );
15804
15805 cx.update_editor(|editor, window, cx| {
15806 editor.change_selections(None, window, cx, |s| {
15807 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
15808 });
15809 });
15810
15811 assert_indent_guides(
15812 0..4,
15813 vec![
15814 indent_guide(buffer_id, 1, 3, 0),
15815 indent_guide(buffer_id, 2, 2, 1),
15816 ],
15817 Some(vec![0]),
15818 &mut cx,
15819 );
15820}
15821
15822#[gpui::test]
15823async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
15824 let (buffer_id, mut cx) = setup_indent_guides_editor(
15825 &"
15826 fn main() {
15827 let a = 1;
15828
15829 let b = 2;
15830 }"
15831 .unindent(),
15832 cx,
15833 )
15834 .await;
15835
15836 cx.update_editor(|editor, window, cx| {
15837 editor.change_selections(None, window, cx, |s| {
15838 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
15839 });
15840 });
15841
15842 assert_indent_guides(
15843 0..5,
15844 vec![indent_guide(buffer_id, 1, 3, 0)],
15845 Some(vec![0]),
15846 &mut cx,
15847 );
15848}
15849
15850#[gpui::test]
15851async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
15852 let (buffer_id, mut cx) = setup_indent_guides_editor(
15853 &"
15854 def m:
15855 a = 1
15856 pass"
15857 .unindent(),
15858 cx,
15859 )
15860 .await;
15861
15862 cx.update_editor(|editor, window, cx| {
15863 editor.change_selections(None, window, cx, |s| {
15864 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
15865 });
15866 });
15867
15868 assert_indent_guides(
15869 0..3,
15870 vec![indent_guide(buffer_id, 1, 2, 0)],
15871 Some(vec![0]),
15872 &mut cx,
15873 );
15874}
15875
15876#[gpui::test]
15877async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
15878 init_test(cx, |_| {});
15879 let mut cx = EditorTestContext::new(cx).await;
15880 let text = indoc! {
15881 "
15882 impl A {
15883 fn b() {
15884 0;
15885 3;
15886 5;
15887 6;
15888 7;
15889 }
15890 }
15891 "
15892 };
15893 let base_text = indoc! {
15894 "
15895 impl A {
15896 fn b() {
15897 0;
15898 1;
15899 2;
15900 3;
15901 4;
15902 }
15903 fn c() {
15904 5;
15905 6;
15906 7;
15907 }
15908 }
15909 "
15910 };
15911
15912 cx.update_editor(|editor, window, cx| {
15913 editor.set_text(text, window, cx);
15914
15915 editor.buffer().update(cx, |multibuffer, cx| {
15916 let buffer = multibuffer.as_singleton().unwrap();
15917 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
15918
15919 multibuffer.set_all_diff_hunks_expanded(cx);
15920 multibuffer.add_diff(diff, cx);
15921
15922 buffer.read(cx).remote_id()
15923 })
15924 });
15925 cx.run_until_parked();
15926
15927 cx.assert_state_with_diff(
15928 indoc! { "
15929 impl A {
15930 fn b() {
15931 0;
15932 - 1;
15933 - 2;
15934 3;
15935 - 4;
15936 - }
15937 - fn c() {
15938 5;
15939 6;
15940 7;
15941 }
15942 }
15943 ˇ"
15944 }
15945 .to_string(),
15946 );
15947
15948 let mut actual_guides = cx.update_editor(|editor, window, cx| {
15949 editor
15950 .snapshot(window, cx)
15951 .buffer_snapshot
15952 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
15953 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
15954 .collect::<Vec<_>>()
15955 });
15956 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
15957 assert_eq!(
15958 actual_guides,
15959 vec![
15960 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
15961 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
15962 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
15963 ]
15964 );
15965}
15966
15967#[gpui::test]
15968async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15969 init_test(cx, |_| {});
15970 let mut cx = EditorTestContext::new(cx).await;
15971
15972 let diff_base = r#"
15973 a
15974 b
15975 c
15976 "#
15977 .unindent();
15978
15979 cx.set_state(
15980 &r#"
15981 ˇA
15982 b
15983 C
15984 "#
15985 .unindent(),
15986 );
15987 cx.set_head_text(&diff_base);
15988 cx.update_editor(|editor, window, cx| {
15989 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15990 });
15991 executor.run_until_parked();
15992
15993 let both_hunks_expanded = r#"
15994 - a
15995 + ˇA
15996 b
15997 - c
15998 + C
15999 "#
16000 .unindent();
16001
16002 cx.assert_state_with_diff(both_hunks_expanded.clone());
16003
16004 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16005 let snapshot = editor.snapshot(window, cx);
16006 let hunks = editor
16007 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16008 .collect::<Vec<_>>();
16009 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16010 let buffer_id = hunks[0].buffer_id;
16011 hunks
16012 .into_iter()
16013 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16014 .collect::<Vec<_>>()
16015 });
16016 assert_eq!(hunk_ranges.len(), 2);
16017
16018 cx.update_editor(|editor, _, cx| {
16019 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16020 });
16021 executor.run_until_parked();
16022
16023 let second_hunk_expanded = r#"
16024 ˇA
16025 b
16026 - c
16027 + C
16028 "#
16029 .unindent();
16030
16031 cx.assert_state_with_diff(second_hunk_expanded);
16032
16033 cx.update_editor(|editor, _, cx| {
16034 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16035 });
16036 executor.run_until_parked();
16037
16038 cx.assert_state_with_diff(both_hunks_expanded.clone());
16039
16040 cx.update_editor(|editor, _, cx| {
16041 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16042 });
16043 executor.run_until_parked();
16044
16045 let first_hunk_expanded = r#"
16046 - a
16047 + ˇA
16048 b
16049 C
16050 "#
16051 .unindent();
16052
16053 cx.assert_state_with_diff(first_hunk_expanded);
16054
16055 cx.update_editor(|editor, _, cx| {
16056 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16057 });
16058 executor.run_until_parked();
16059
16060 cx.assert_state_with_diff(both_hunks_expanded);
16061
16062 cx.set_state(
16063 &r#"
16064 ˇA
16065 b
16066 "#
16067 .unindent(),
16068 );
16069 cx.run_until_parked();
16070
16071 // TODO this cursor position seems bad
16072 cx.assert_state_with_diff(
16073 r#"
16074 - ˇa
16075 + A
16076 b
16077 "#
16078 .unindent(),
16079 );
16080
16081 cx.update_editor(|editor, window, cx| {
16082 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16083 });
16084
16085 cx.assert_state_with_diff(
16086 r#"
16087 - ˇa
16088 + A
16089 b
16090 - c
16091 "#
16092 .unindent(),
16093 );
16094
16095 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16096 let snapshot = editor.snapshot(window, cx);
16097 let hunks = editor
16098 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16099 .collect::<Vec<_>>();
16100 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16101 let buffer_id = hunks[0].buffer_id;
16102 hunks
16103 .into_iter()
16104 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16105 .collect::<Vec<_>>()
16106 });
16107 assert_eq!(hunk_ranges.len(), 2);
16108
16109 cx.update_editor(|editor, _, cx| {
16110 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16111 });
16112 executor.run_until_parked();
16113
16114 cx.assert_state_with_diff(
16115 r#"
16116 - ˇa
16117 + A
16118 b
16119 "#
16120 .unindent(),
16121 );
16122}
16123
16124#[gpui::test]
16125async fn test_toggle_deletion_hunk_at_start_of_file(
16126 executor: BackgroundExecutor,
16127 cx: &mut TestAppContext,
16128) {
16129 init_test(cx, |_| {});
16130 let mut cx = EditorTestContext::new(cx).await;
16131
16132 let diff_base = r#"
16133 a
16134 b
16135 c
16136 "#
16137 .unindent();
16138
16139 cx.set_state(
16140 &r#"
16141 ˇb
16142 c
16143 "#
16144 .unindent(),
16145 );
16146 cx.set_head_text(&diff_base);
16147 cx.update_editor(|editor, window, cx| {
16148 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16149 });
16150 executor.run_until_parked();
16151
16152 let hunk_expanded = r#"
16153 - a
16154 ˇb
16155 c
16156 "#
16157 .unindent();
16158
16159 cx.assert_state_with_diff(hunk_expanded.clone());
16160
16161 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16162 let snapshot = editor.snapshot(window, cx);
16163 let hunks = editor
16164 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16165 .collect::<Vec<_>>();
16166 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16167 let buffer_id = hunks[0].buffer_id;
16168 hunks
16169 .into_iter()
16170 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16171 .collect::<Vec<_>>()
16172 });
16173 assert_eq!(hunk_ranges.len(), 1);
16174
16175 cx.update_editor(|editor, _, cx| {
16176 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16177 });
16178 executor.run_until_parked();
16179
16180 let hunk_collapsed = r#"
16181 ˇb
16182 c
16183 "#
16184 .unindent();
16185
16186 cx.assert_state_with_diff(hunk_collapsed);
16187
16188 cx.update_editor(|editor, _, cx| {
16189 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16190 });
16191 executor.run_until_parked();
16192
16193 cx.assert_state_with_diff(hunk_expanded.clone());
16194}
16195
16196#[gpui::test]
16197async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16198 init_test(cx, |_| {});
16199
16200 let fs = FakeFs::new(cx.executor());
16201 fs.insert_tree(
16202 path!("/test"),
16203 json!({
16204 ".git": {},
16205 "file-1": "ONE\n",
16206 "file-2": "TWO\n",
16207 "file-3": "THREE\n",
16208 }),
16209 )
16210 .await;
16211
16212 fs.set_head_for_repo(
16213 path!("/test/.git").as_ref(),
16214 &[
16215 ("file-1".into(), "one\n".into()),
16216 ("file-2".into(), "two\n".into()),
16217 ("file-3".into(), "three\n".into()),
16218 ],
16219 );
16220
16221 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
16222 let mut buffers = vec![];
16223 for i in 1..=3 {
16224 let buffer = project
16225 .update(cx, |project, cx| {
16226 let path = format!(path!("/test/file-{}"), i);
16227 project.open_local_buffer(path, cx)
16228 })
16229 .await
16230 .unwrap();
16231 buffers.push(buffer);
16232 }
16233
16234 let multibuffer = cx.new(|cx| {
16235 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
16236 multibuffer.set_all_diff_hunks_expanded(cx);
16237 for buffer in &buffers {
16238 let snapshot = buffer.read(cx).snapshot();
16239 multibuffer.set_excerpts_for_path(
16240 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
16241 buffer.clone(),
16242 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
16243 DEFAULT_MULTIBUFFER_CONTEXT,
16244 cx,
16245 );
16246 }
16247 multibuffer
16248 });
16249
16250 let editor = cx.add_window(|window, cx| {
16251 Editor::new(EditorMode::Full, multibuffer, Some(project), window, cx)
16252 });
16253 cx.run_until_parked();
16254
16255 let snapshot = editor
16256 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16257 .unwrap();
16258 let hunks = snapshot
16259 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
16260 .map(|hunk| match hunk {
16261 DisplayDiffHunk::Unfolded {
16262 display_row_range, ..
16263 } => display_row_range,
16264 DisplayDiffHunk::Folded { .. } => unreachable!(),
16265 })
16266 .collect::<Vec<_>>();
16267 assert_eq!(
16268 hunks,
16269 [
16270 DisplayRow(2)..DisplayRow(4),
16271 DisplayRow(7)..DisplayRow(9),
16272 DisplayRow(12)..DisplayRow(14),
16273 ]
16274 );
16275}
16276
16277#[gpui::test]
16278async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
16279 init_test(cx, |_| {});
16280
16281 let mut cx = EditorTestContext::new(cx).await;
16282 cx.set_head_text(indoc! { "
16283 one
16284 two
16285 three
16286 four
16287 five
16288 "
16289 });
16290 cx.set_index_text(indoc! { "
16291 one
16292 two
16293 three
16294 four
16295 five
16296 "
16297 });
16298 cx.set_state(indoc! {"
16299 one
16300 TWO
16301 ˇTHREE
16302 FOUR
16303 five
16304 "});
16305 cx.run_until_parked();
16306 cx.update_editor(|editor, window, cx| {
16307 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16308 });
16309 cx.run_until_parked();
16310 cx.assert_index_text(Some(indoc! {"
16311 one
16312 TWO
16313 THREE
16314 FOUR
16315 five
16316 "}));
16317 cx.set_state(indoc! { "
16318 one
16319 TWO
16320 ˇTHREE-HUNDRED
16321 FOUR
16322 five
16323 "});
16324 cx.run_until_parked();
16325 cx.update_editor(|editor, window, cx| {
16326 let snapshot = editor.snapshot(window, cx);
16327 let hunks = editor
16328 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16329 .collect::<Vec<_>>();
16330 assert_eq!(hunks.len(), 1);
16331 assert_eq!(
16332 hunks[0].status(),
16333 DiffHunkStatus {
16334 kind: DiffHunkStatusKind::Modified,
16335 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
16336 }
16337 );
16338
16339 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16340 });
16341 cx.run_until_parked();
16342 cx.assert_index_text(Some(indoc! {"
16343 one
16344 TWO
16345 THREE-HUNDRED
16346 FOUR
16347 five
16348 "}));
16349}
16350
16351#[gpui::test]
16352fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
16353 init_test(cx, |_| {});
16354
16355 let editor = cx.add_window(|window, cx| {
16356 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
16357 build_editor(buffer, window, cx)
16358 });
16359
16360 let render_args = Arc::new(Mutex::new(None));
16361 let snapshot = editor
16362 .update(cx, |editor, window, cx| {
16363 let snapshot = editor.buffer().read(cx).snapshot(cx);
16364 let range =
16365 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
16366
16367 struct RenderArgs {
16368 row: MultiBufferRow,
16369 folded: bool,
16370 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
16371 }
16372
16373 let crease = Crease::inline(
16374 range,
16375 FoldPlaceholder::test(),
16376 {
16377 let toggle_callback = render_args.clone();
16378 move |row, folded, callback, _window, _cx| {
16379 *toggle_callback.lock() = Some(RenderArgs {
16380 row,
16381 folded,
16382 callback,
16383 });
16384 div()
16385 }
16386 },
16387 |_row, _folded, _window, _cx| div(),
16388 );
16389
16390 editor.insert_creases(Some(crease), cx);
16391 let snapshot = editor.snapshot(window, cx);
16392 let _div = snapshot.render_crease_toggle(
16393 MultiBufferRow(1),
16394 false,
16395 cx.entity().clone(),
16396 window,
16397 cx,
16398 );
16399 snapshot
16400 })
16401 .unwrap();
16402
16403 let render_args = render_args.lock().take().unwrap();
16404 assert_eq!(render_args.row, MultiBufferRow(1));
16405 assert!(!render_args.folded);
16406 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16407
16408 cx.update_window(*editor, |_, window, cx| {
16409 (render_args.callback)(true, window, cx)
16410 })
16411 .unwrap();
16412 let snapshot = editor
16413 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16414 .unwrap();
16415 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
16416
16417 cx.update_window(*editor, |_, window, cx| {
16418 (render_args.callback)(false, window, cx)
16419 })
16420 .unwrap();
16421 let snapshot = editor
16422 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
16423 .unwrap();
16424 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
16425}
16426
16427#[gpui::test]
16428async fn test_input_text(cx: &mut TestAppContext) {
16429 init_test(cx, |_| {});
16430 let mut cx = EditorTestContext::new(cx).await;
16431
16432 cx.set_state(
16433 &r#"ˇone
16434 two
16435
16436 three
16437 fourˇ
16438 five
16439
16440 siˇx"#
16441 .unindent(),
16442 );
16443
16444 cx.dispatch_action(HandleInput(String::new()));
16445 cx.assert_editor_state(
16446 &r#"ˇone
16447 two
16448
16449 three
16450 fourˇ
16451 five
16452
16453 siˇx"#
16454 .unindent(),
16455 );
16456
16457 cx.dispatch_action(HandleInput("AAAA".to_string()));
16458 cx.assert_editor_state(
16459 &r#"AAAAˇone
16460 two
16461
16462 three
16463 fourAAAAˇ
16464 five
16465
16466 siAAAAˇx"#
16467 .unindent(),
16468 );
16469}
16470
16471#[gpui::test]
16472async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
16473 init_test(cx, |_| {});
16474
16475 let mut cx = EditorTestContext::new(cx).await;
16476 cx.set_state(
16477 r#"let foo = 1;
16478let foo = 2;
16479let foo = 3;
16480let fooˇ = 4;
16481let foo = 5;
16482let foo = 6;
16483let foo = 7;
16484let foo = 8;
16485let foo = 9;
16486let foo = 10;
16487let foo = 11;
16488let foo = 12;
16489let foo = 13;
16490let foo = 14;
16491let foo = 15;"#,
16492 );
16493
16494 cx.update_editor(|e, window, cx| {
16495 assert_eq!(
16496 e.next_scroll_position,
16497 NextScrollCursorCenterTopBottom::Center,
16498 "Default next scroll direction is center",
16499 );
16500
16501 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16502 assert_eq!(
16503 e.next_scroll_position,
16504 NextScrollCursorCenterTopBottom::Top,
16505 "After center, next scroll direction should be top",
16506 );
16507
16508 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16509 assert_eq!(
16510 e.next_scroll_position,
16511 NextScrollCursorCenterTopBottom::Bottom,
16512 "After top, next scroll direction should be bottom",
16513 );
16514
16515 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16516 assert_eq!(
16517 e.next_scroll_position,
16518 NextScrollCursorCenterTopBottom::Center,
16519 "After bottom, scrolling should start over",
16520 );
16521
16522 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
16523 assert_eq!(
16524 e.next_scroll_position,
16525 NextScrollCursorCenterTopBottom::Top,
16526 "Scrolling continues if retriggered fast enough"
16527 );
16528 });
16529
16530 cx.executor()
16531 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
16532 cx.executor().run_until_parked();
16533 cx.update_editor(|e, _, _| {
16534 assert_eq!(
16535 e.next_scroll_position,
16536 NextScrollCursorCenterTopBottom::Center,
16537 "If scrolling is not triggered fast enough, it should reset"
16538 );
16539 });
16540}
16541
16542#[gpui::test]
16543async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
16544 init_test(cx, |_| {});
16545 let mut cx = EditorLspTestContext::new_rust(
16546 lsp::ServerCapabilities {
16547 definition_provider: Some(lsp::OneOf::Left(true)),
16548 references_provider: Some(lsp::OneOf::Left(true)),
16549 ..lsp::ServerCapabilities::default()
16550 },
16551 cx,
16552 )
16553 .await;
16554
16555 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
16556 let go_to_definition = cx
16557 .lsp
16558 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16559 move |params, _| async move {
16560 if empty_go_to_definition {
16561 Ok(None)
16562 } else {
16563 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
16564 uri: params.text_document_position_params.text_document.uri,
16565 range: lsp::Range::new(
16566 lsp::Position::new(4, 3),
16567 lsp::Position::new(4, 6),
16568 ),
16569 })))
16570 }
16571 },
16572 );
16573 let references = cx
16574 .lsp
16575 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
16576 Ok(Some(vec![lsp::Location {
16577 uri: params.text_document_position.text_document.uri,
16578 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
16579 }]))
16580 });
16581 (go_to_definition, references)
16582 };
16583
16584 cx.set_state(
16585 &r#"fn one() {
16586 let mut a = ˇtwo();
16587 }
16588
16589 fn two() {}"#
16590 .unindent(),
16591 );
16592 set_up_lsp_handlers(false, &mut cx);
16593 let navigated = cx
16594 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16595 .await
16596 .expect("Failed to navigate to definition");
16597 assert_eq!(
16598 navigated,
16599 Navigated::Yes,
16600 "Should have navigated to definition from the GetDefinition response"
16601 );
16602 cx.assert_editor_state(
16603 &r#"fn one() {
16604 let mut a = two();
16605 }
16606
16607 fn «twoˇ»() {}"#
16608 .unindent(),
16609 );
16610
16611 let editors = cx.update_workspace(|workspace, _, cx| {
16612 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16613 });
16614 cx.update_editor(|_, _, test_editor_cx| {
16615 assert_eq!(
16616 editors.len(),
16617 1,
16618 "Initially, only one, test, editor should be open in the workspace"
16619 );
16620 assert_eq!(
16621 test_editor_cx.entity(),
16622 editors.last().expect("Asserted len is 1").clone()
16623 );
16624 });
16625
16626 set_up_lsp_handlers(true, &mut cx);
16627 let navigated = cx
16628 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16629 .await
16630 .expect("Failed to navigate to lookup references");
16631 assert_eq!(
16632 navigated,
16633 Navigated::Yes,
16634 "Should have navigated to references as a fallback after empty GoToDefinition response"
16635 );
16636 // We should not change the selections in the existing file,
16637 // if opening another milti buffer with the references
16638 cx.assert_editor_state(
16639 &r#"fn one() {
16640 let mut a = two();
16641 }
16642
16643 fn «twoˇ»() {}"#
16644 .unindent(),
16645 );
16646 let editors = cx.update_workspace(|workspace, _, cx| {
16647 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16648 });
16649 cx.update_editor(|_, _, test_editor_cx| {
16650 assert_eq!(
16651 editors.len(),
16652 2,
16653 "After falling back to references search, we open a new editor with the results"
16654 );
16655 let references_fallback_text = editors
16656 .into_iter()
16657 .find(|new_editor| *new_editor != test_editor_cx.entity())
16658 .expect("Should have one non-test editor now")
16659 .read(test_editor_cx)
16660 .text(test_editor_cx);
16661 assert_eq!(
16662 references_fallback_text, "fn one() {\n let mut a = two();\n}",
16663 "Should use the range from the references response and not the GoToDefinition one"
16664 );
16665 });
16666}
16667
16668#[gpui::test]
16669async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
16670 init_test(cx, |_| {});
16671 cx.update(|cx| {
16672 let mut editor_settings = EditorSettings::get_global(cx).clone();
16673 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
16674 EditorSettings::override_global(editor_settings, cx);
16675 });
16676 let mut cx = EditorLspTestContext::new_rust(
16677 lsp::ServerCapabilities {
16678 definition_provider: Some(lsp::OneOf::Left(true)),
16679 references_provider: Some(lsp::OneOf::Left(true)),
16680 ..lsp::ServerCapabilities::default()
16681 },
16682 cx,
16683 )
16684 .await;
16685 let original_state = r#"fn one() {
16686 let mut a = ˇtwo();
16687 }
16688
16689 fn two() {}"#
16690 .unindent();
16691 cx.set_state(&original_state);
16692
16693 let mut go_to_definition = cx
16694 .lsp
16695 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
16696 move |_, _| async move { Ok(None) },
16697 );
16698 let _references = cx
16699 .lsp
16700 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
16701 panic!("Should not call for references with no go to definition fallback")
16702 });
16703
16704 let navigated = cx
16705 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
16706 .await
16707 .expect("Failed to navigate to lookup references");
16708 go_to_definition
16709 .next()
16710 .await
16711 .expect("Should have called the go_to_definition handler");
16712
16713 assert_eq!(
16714 navigated,
16715 Navigated::No,
16716 "Should have navigated to references as a fallback after empty GoToDefinition response"
16717 );
16718 cx.assert_editor_state(&original_state);
16719 let editors = cx.update_workspace(|workspace, _, cx| {
16720 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
16721 });
16722 cx.update_editor(|_, _, _| {
16723 assert_eq!(
16724 editors.len(),
16725 1,
16726 "After unsuccessful fallback, no other editor should have been opened"
16727 );
16728 });
16729}
16730
16731#[gpui::test]
16732async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
16733 init_test(cx, |_| {});
16734
16735 let language = Arc::new(Language::new(
16736 LanguageConfig::default(),
16737 Some(tree_sitter_rust::LANGUAGE.into()),
16738 ));
16739
16740 let text = r#"
16741 #[cfg(test)]
16742 mod tests() {
16743 #[test]
16744 fn runnable_1() {
16745 let a = 1;
16746 }
16747
16748 #[test]
16749 fn runnable_2() {
16750 let a = 1;
16751 let b = 2;
16752 }
16753 }
16754 "#
16755 .unindent();
16756
16757 let fs = FakeFs::new(cx.executor());
16758 fs.insert_file("/file.rs", Default::default()).await;
16759
16760 let project = Project::test(fs, ["/a".as_ref()], cx).await;
16761 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16762 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16763 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16764 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
16765
16766 let editor = cx.new_window_entity(|window, cx| {
16767 Editor::new(
16768 EditorMode::Full,
16769 multi_buffer,
16770 Some(project.clone()),
16771 window,
16772 cx,
16773 )
16774 });
16775
16776 editor.update_in(cx, |editor, window, cx| {
16777 let snapshot = editor.buffer().read(cx).snapshot(cx);
16778 editor.tasks.insert(
16779 (buffer.read(cx).remote_id(), 3),
16780 RunnableTasks {
16781 templates: vec![],
16782 offset: snapshot.anchor_before(43),
16783 column: 0,
16784 extra_variables: HashMap::default(),
16785 context_range: BufferOffset(43)..BufferOffset(85),
16786 },
16787 );
16788 editor.tasks.insert(
16789 (buffer.read(cx).remote_id(), 8),
16790 RunnableTasks {
16791 templates: vec![],
16792 offset: snapshot.anchor_before(86),
16793 column: 0,
16794 extra_variables: HashMap::default(),
16795 context_range: BufferOffset(86)..BufferOffset(191),
16796 },
16797 );
16798
16799 // Test finding task when cursor is inside function body
16800 editor.change_selections(None, window, cx, |s| {
16801 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
16802 });
16803 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16804 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
16805
16806 // Test finding task when cursor is on function name
16807 editor.change_selections(None, window, cx, |s| {
16808 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
16809 });
16810 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
16811 assert_eq!(row, 8, "Should find task when cursor is on function name");
16812 });
16813}
16814
16815#[gpui::test]
16816async fn test_folding_buffers(cx: &mut TestAppContext) {
16817 init_test(cx, |_| {});
16818
16819 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
16820 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
16821 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
16822
16823 let fs = FakeFs::new(cx.executor());
16824 fs.insert_tree(
16825 path!("/a"),
16826 json!({
16827 "first.rs": sample_text_1,
16828 "second.rs": sample_text_2,
16829 "third.rs": sample_text_3,
16830 }),
16831 )
16832 .await;
16833 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16834 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16835 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16836 let worktree = project.update(cx, |project, cx| {
16837 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
16838 assert_eq!(worktrees.len(), 1);
16839 worktrees.pop().unwrap()
16840 });
16841 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
16842
16843 let buffer_1 = project
16844 .update(cx, |project, cx| {
16845 project.open_buffer((worktree_id, "first.rs"), cx)
16846 })
16847 .await
16848 .unwrap();
16849 let buffer_2 = project
16850 .update(cx, |project, cx| {
16851 project.open_buffer((worktree_id, "second.rs"), cx)
16852 })
16853 .await
16854 .unwrap();
16855 let buffer_3 = project
16856 .update(cx, |project, cx| {
16857 project.open_buffer((worktree_id, "third.rs"), cx)
16858 })
16859 .await
16860 .unwrap();
16861
16862 let multi_buffer = cx.new(|cx| {
16863 let mut multi_buffer = MultiBuffer::new(ReadWrite);
16864 multi_buffer.push_excerpts(
16865 buffer_1.clone(),
16866 [
16867 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16868 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16869 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16870 ],
16871 cx,
16872 );
16873 multi_buffer.push_excerpts(
16874 buffer_2.clone(),
16875 [
16876 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16877 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16878 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16879 ],
16880 cx,
16881 );
16882 multi_buffer.push_excerpts(
16883 buffer_3.clone(),
16884 [
16885 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
16886 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
16887 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
16888 ],
16889 cx,
16890 );
16891 multi_buffer
16892 });
16893 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
16894 Editor::new(
16895 EditorMode::Full,
16896 multi_buffer.clone(),
16897 Some(project.clone()),
16898 window,
16899 cx,
16900 )
16901 });
16902
16903 assert_eq!(
16904 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16905 "\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",
16906 );
16907
16908 multi_buffer_editor.update(cx, |editor, cx| {
16909 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
16910 });
16911 assert_eq!(
16912 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16913 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16914 "After folding the first buffer, its text should not be displayed"
16915 );
16916
16917 multi_buffer_editor.update(cx, |editor, cx| {
16918 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
16919 });
16920 assert_eq!(
16921 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16922 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
16923 "After folding the second buffer, its text should not be displayed"
16924 );
16925
16926 multi_buffer_editor.update(cx, |editor, cx| {
16927 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
16928 });
16929 assert_eq!(
16930 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16931 "\n\n\n\n\n",
16932 "After folding the third buffer, its text should not be displayed"
16933 );
16934
16935 // Emulate selection inside the fold logic, that should work
16936 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16937 editor
16938 .snapshot(window, cx)
16939 .next_line_boundary(Point::new(0, 4));
16940 });
16941
16942 multi_buffer_editor.update(cx, |editor, cx| {
16943 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
16944 });
16945 assert_eq!(
16946 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16947 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16948 "After unfolding the second buffer, its text should be displayed"
16949 );
16950
16951 // Typing inside of buffer 1 causes that buffer to be unfolded.
16952 multi_buffer_editor.update_in(cx, |editor, window, cx| {
16953 assert_eq!(
16954 multi_buffer
16955 .read(cx)
16956 .snapshot(cx)
16957 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
16958 .collect::<String>(),
16959 "bbbb"
16960 );
16961 editor.change_selections(None, window, cx, |selections| {
16962 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
16963 });
16964 editor.handle_input("B", window, cx);
16965 });
16966
16967 assert_eq!(
16968 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16969 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
16970 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
16971 );
16972
16973 multi_buffer_editor.update(cx, |editor, cx| {
16974 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
16975 });
16976 assert_eq!(
16977 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
16978 "\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",
16979 "After unfolding the all buffers, all original text should be displayed"
16980 );
16981}
16982
16983#[gpui::test]
16984async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
16985 init_test(cx, |_| {});
16986
16987 let sample_text_1 = "1111\n2222\n3333".to_string();
16988 let sample_text_2 = "4444\n5555\n6666".to_string();
16989 let sample_text_3 = "7777\n8888\n9999".to_string();
16990
16991 let fs = FakeFs::new(cx.executor());
16992 fs.insert_tree(
16993 path!("/a"),
16994 json!({
16995 "first.rs": sample_text_1,
16996 "second.rs": sample_text_2,
16997 "third.rs": sample_text_3,
16998 }),
16999 )
17000 .await;
17001 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17002 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17003 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17004 let worktree = project.update(cx, |project, cx| {
17005 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17006 assert_eq!(worktrees.len(), 1);
17007 worktrees.pop().unwrap()
17008 });
17009 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17010
17011 let buffer_1 = project
17012 .update(cx, |project, cx| {
17013 project.open_buffer((worktree_id, "first.rs"), cx)
17014 })
17015 .await
17016 .unwrap();
17017 let buffer_2 = project
17018 .update(cx, |project, cx| {
17019 project.open_buffer((worktree_id, "second.rs"), cx)
17020 })
17021 .await
17022 .unwrap();
17023 let buffer_3 = project
17024 .update(cx, |project, cx| {
17025 project.open_buffer((worktree_id, "third.rs"), cx)
17026 })
17027 .await
17028 .unwrap();
17029
17030 let multi_buffer = cx.new(|cx| {
17031 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17032 multi_buffer.push_excerpts(
17033 buffer_1.clone(),
17034 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17035 cx,
17036 );
17037 multi_buffer.push_excerpts(
17038 buffer_2.clone(),
17039 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17040 cx,
17041 );
17042 multi_buffer.push_excerpts(
17043 buffer_3.clone(),
17044 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17045 cx,
17046 );
17047 multi_buffer
17048 });
17049
17050 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17051 Editor::new(
17052 EditorMode::Full,
17053 multi_buffer,
17054 Some(project.clone()),
17055 window,
17056 cx,
17057 )
17058 });
17059
17060 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17061 assert_eq!(
17062 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17063 full_text,
17064 );
17065
17066 multi_buffer_editor.update(cx, |editor, cx| {
17067 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17068 });
17069 assert_eq!(
17070 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17071 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17072 "After folding the first buffer, its text should not be displayed"
17073 );
17074
17075 multi_buffer_editor.update(cx, |editor, cx| {
17076 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17077 });
17078
17079 assert_eq!(
17080 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17081 "\n\n\n\n\n\n7777\n8888\n9999",
17082 "After folding the second buffer, its text should not be displayed"
17083 );
17084
17085 multi_buffer_editor.update(cx, |editor, cx| {
17086 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17087 });
17088 assert_eq!(
17089 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17090 "\n\n\n\n\n",
17091 "After folding the third buffer, its text should not be displayed"
17092 );
17093
17094 multi_buffer_editor.update(cx, |editor, cx| {
17095 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17096 });
17097 assert_eq!(
17098 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17099 "\n\n\n\n4444\n5555\n6666\n\n",
17100 "After unfolding the second buffer, its text should be displayed"
17101 );
17102
17103 multi_buffer_editor.update(cx, |editor, cx| {
17104 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17105 });
17106 assert_eq!(
17107 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17108 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17109 "After unfolding the first buffer, its text should be displayed"
17110 );
17111
17112 multi_buffer_editor.update(cx, |editor, cx| {
17113 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17114 });
17115 assert_eq!(
17116 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17117 full_text,
17118 "After unfolding all buffers, all original text should be displayed"
17119 );
17120}
17121
17122#[gpui::test]
17123async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17124 init_test(cx, |_| {});
17125
17126 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17127
17128 let fs = FakeFs::new(cx.executor());
17129 fs.insert_tree(
17130 path!("/a"),
17131 json!({
17132 "main.rs": sample_text,
17133 }),
17134 )
17135 .await;
17136 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17137 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17138 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17139 let worktree = project.update(cx, |project, cx| {
17140 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17141 assert_eq!(worktrees.len(), 1);
17142 worktrees.pop().unwrap()
17143 });
17144 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17145
17146 let buffer_1 = project
17147 .update(cx, |project, cx| {
17148 project.open_buffer((worktree_id, "main.rs"), cx)
17149 })
17150 .await
17151 .unwrap();
17152
17153 let multi_buffer = cx.new(|cx| {
17154 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17155 multi_buffer.push_excerpts(
17156 buffer_1.clone(),
17157 [ExcerptRange::new(
17158 Point::new(0, 0)
17159 ..Point::new(
17160 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17161 0,
17162 ),
17163 )],
17164 cx,
17165 );
17166 multi_buffer
17167 });
17168 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17169 Editor::new(
17170 EditorMode::Full,
17171 multi_buffer,
17172 Some(project.clone()),
17173 window,
17174 cx,
17175 )
17176 });
17177
17178 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17179 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17180 enum TestHighlight {}
17181 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17182 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17183 editor.highlight_text::<TestHighlight>(
17184 vec![highlight_range.clone()],
17185 HighlightStyle::color(Hsla::green()),
17186 cx,
17187 );
17188 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17189 });
17190
17191 let full_text = format!("\n\n{sample_text}");
17192 assert_eq!(
17193 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17194 full_text,
17195 );
17196}
17197
17198#[gpui::test]
17199async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17200 init_test(cx, |_| {});
17201 cx.update(|cx| {
17202 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
17203 "keymaps/default-linux.json",
17204 cx,
17205 )
17206 .unwrap();
17207 cx.bind_keys(default_key_bindings);
17208 });
17209
17210 let (editor, cx) = cx.add_window_view(|window, cx| {
17211 let multi_buffer = MultiBuffer::build_multi(
17212 [
17213 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
17214 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
17215 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
17216 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
17217 ],
17218 cx,
17219 );
17220 let mut editor = Editor::new(EditorMode::Full, multi_buffer.clone(), None, window, cx);
17221
17222 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
17223 // fold all but the second buffer, so that we test navigating between two
17224 // adjacent folded buffers, as well as folded buffers at the start and
17225 // end the multibuffer
17226 editor.fold_buffer(buffer_ids[0], cx);
17227 editor.fold_buffer(buffer_ids[2], cx);
17228 editor.fold_buffer(buffer_ids[3], cx);
17229
17230 editor
17231 });
17232 cx.simulate_resize(size(px(1000.), px(1000.)));
17233
17234 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
17235 cx.assert_excerpts_with_selections(indoc! {"
17236 [EXCERPT]
17237 ˇ[FOLDED]
17238 [EXCERPT]
17239 a1
17240 b1
17241 [EXCERPT]
17242 [FOLDED]
17243 [EXCERPT]
17244 [FOLDED]
17245 "
17246 });
17247 cx.simulate_keystroke("down");
17248 cx.assert_excerpts_with_selections(indoc! {"
17249 [EXCERPT]
17250 [FOLDED]
17251 [EXCERPT]
17252 ˇa1
17253 b1
17254 [EXCERPT]
17255 [FOLDED]
17256 [EXCERPT]
17257 [FOLDED]
17258 "
17259 });
17260 cx.simulate_keystroke("down");
17261 cx.assert_excerpts_with_selections(indoc! {"
17262 [EXCERPT]
17263 [FOLDED]
17264 [EXCERPT]
17265 a1
17266 ˇb1
17267 [EXCERPT]
17268 [FOLDED]
17269 [EXCERPT]
17270 [FOLDED]
17271 "
17272 });
17273 cx.simulate_keystroke("down");
17274 cx.assert_excerpts_with_selections(indoc! {"
17275 [EXCERPT]
17276 [FOLDED]
17277 [EXCERPT]
17278 a1
17279 b1
17280 ˇ[EXCERPT]
17281 [FOLDED]
17282 [EXCERPT]
17283 [FOLDED]
17284 "
17285 });
17286 cx.simulate_keystroke("down");
17287 cx.assert_excerpts_with_selections(indoc! {"
17288 [EXCERPT]
17289 [FOLDED]
17290 [EXCERPT]
17291 a1
17292 b1
17293 [EXCERPT]
17294 ˇ[FOLDED]
17295 [EXCERPT]
17296 [FOLDED]
17297 "
17298 });
17299 for _ in 0..5 {
17300 cx.simulate_keystroke("down");
17301 cx.assert_excerpts_with_selections(indoc! {"
17302 [EXCERPT]
17303 [FOLDED]
17304 [EXCERPT]
17305 a1
17306 b1
17307 [EXCERPT]
17308 [FOLDED]
17309 [EXCERPT]
17310 ˇ[FOLDED]
17311 "
17312 });
17313 }
17314
17315 cx.simulate_keystroke("up");
17316 cx.assert_excerpts_with_selections(indoc! {"
17317 [EXCERPT]
17318 [FOLDED]
17319 [EXCERPT]
17320 a1
17321 b1
17322 [EXCERPT]
17323 ˇ[FOLDED]
17324 [EXCERPT]
17325 [FOLDED]
17326 "
17327 });
17328 cx.simulate_keystroke("up");
17329 cx.assert_excerpts_with_selections(indoc! {"
17330 [EXCERPT]
17331 [FOLDED]
17332 [EXCERPT]
17333 a1
17334 b1
17335 ˇ[EXCERPT]
17336 [FOLDED]
17337 [EXCERPT]
17338 [FOLDED]
17339 "
17340 });
17341 cx.simulate_keystroke("up");
17342 cx.assert_excerpts_with_selections(indoc! {"
17343 [EXCERPT]
17344 [FOLDED]
17345 [EXCERPT]
17346 a1
17347 ˇb1
17348 [EXCERPT]
17349 [FOLDED]
17350 [EXCERPT]
17351 [FOLDED]
17352 "
17353 });
17354 cx.simulate_keystroke("up");
17355 cx.assert_excerpts_with_selections(indoc! {"
17356 [EXCERPT]
17357 [FOLDED]
17358 [EXCERPT]
17359 ˇa1
17360 b1
17361 [EXCERPT]
17362 [FOLDED]
17363 [EXCERPT]
17364 [FOLDED]
17365 "
17366 });
17367 for _ in 0..5 {
17368 cx.simulate_keystroke("up");
17369 cx.assert_excerpts_with_selections(indoc! {"
17370 [EXCERPT]
17371 ˇ[FOLDED]
17372 [EXCERPT]
17373 a1
17374 b1
17375 [EXCERPT]
17376 [FOLDED]
17377 [EXCERPT]
17378 [FOLDED]
17379 "
17380 });
17381 }
17382}
17383
17384#[gpui::test]
17385async fn test_inline_completion_text(cx: &mut TestAppContext) {
17386 init_test(cx, |_| {});
17387
17388 // Simple insertion
17389 assert_highlighted_edits(
17390 "Hello, world!",
17391 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
17392 true,
17393 cx,
17394 |highlighted_edits, cx| {
17395 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
17396 assert_eq!(highlighted_edits.highlights.len(), 1);
17397 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
17398 assert_eq!(
17399 highlighted_edits.highlights[0].1.background_color,
17400 Some(cx.theme().status().created_background)
17401 );
17402 },
17403 )
17404 .await;
17405
17406 // Replacement
17407 assert_highlighted_edits(
17408 "This is a test.",
17409 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
17410 false,
17411 cx,
17412 |highlighted_edits, cx| {
17413 assert_eq!(highlighted_edits.text, "That is a test.");
17414 assert_eq!(highlighted_edits.highlights.len(), 1);
17415 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
17416 assert_eq!(
17417 highlighted_edits.highlights[0].1.background_color,
17418 Some(cx.theme().status().created_background)
17419 );
17420 },
17421 )
17422 .await;
17423
17424 // Multiple edits
17425 assert_highlighted_edits(
17426 "Hello, world!",
17427 vec![
17428 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
17429 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
17430 ],
17431 false,
17432 cx,
17433 |highlighted_edits, cx| {
17434 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
17435 assert_eq!(highlighted_edits.highlights.len(), 2);
17436 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
17437 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
17438 assert_eq!(
17439 highlighted_edits.highlights[0].1.background_color,
17440 Some(cx.theme().status().created_background)
17441 );
17442 assert_eq!(
17443 highlighted_edits.highlights[1].1.background_color,
17444 Some(cx.theme().status().created_background)
17445 );
17446 },
17447 )
17448 .await;
17449
17450 // Multiple lines with edits
17451 assert_highlighted_edits(
17452 "First line\nSecond line\nThird line\nFourth line",
17453 vec![
17454 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
17455 (
17456 Point::new(2, 0)..Point::new(2, 10),
17457 "New third line".to_string(),
17458 ),
17459 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
17460 ],
17461 false,
17462 cx,
17463 |highlighted_edits, cx| {
17464 assert_eq!(
17465 highlighted_edits.text,
17466 "Second modified\nNew third line\nFourth updated line"
17467 );
17468 assert_eq!(highlighted_edits.highlights.len(), 3);
17469 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
17470 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
17471 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
17472 for highlight in &highlighted_edits.highlights {
17473 assert_eq!(
17474 highlight.1.background_color,
17475 Some(cx.theme().status().created_background)
17476 );
17477 }
17478 },
17479 )
17480 .await;
17481}
17482
17483#[gpui::test]
17484async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
17485 init_test(cx, |_| {});
17486
17487 // Deletion
17488 assert_highlighted_edits(
17489 "Hello, world!",
17490 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
17491 true,
17492 cx,
17493 |highlighted_edits, cx| {
17494 assert_eq!(highlighted_edits.text, "Hello, world!");
17495 assert_eq!(highlighted_edits.highlights.len(), 1);
17496 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
17497 assert_eq!(
17498 highlighted_edits.highlights[0].1.background_color,
17499 Some(cx.theme().status().deleted_background)
17500 );
17501 },
17502 )
17503 .await;
17504
17505 // Insertion
17506 assert_highlighted_edits(
17507 "Hello, world!",
17508 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
17509 true,
17510 cx,
17511 |highlighted_edits, cx| {
17512 assert_eq!(highlighted_edits.highlights.len(), 1);
17513 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
17514 assert_eq!(
17515 highlighted_edits.highlights[0].1.background_color,
17516 Some(cx.theme().status().created_background)
17517 );
17518 },
17519 )
17520 .await;
17521}
17522
17523async fn assert_highlighted_edits(
17524 text: &str,
17525 edits: Vec<(Range<Point>, String)>,
17526 include_deletions: bool,
17527 cx: &mut TestAppContext,
17528 assertion_fn: impl Fn(HighlightedText, &App),
17529) {
17530 let window = cx.add_window(|window, cx| {
17531 let buffer = MultiBuffer::build_simple(text, cx);
17532 Editor::new(EditorMode::Full, buffer, None, window, cx)
17533 });
17534 let cx = &mut VisualTestContext::from_window(*window, cx);
17535
17536 let (buffer, snapshot) = window
17537 .update(cx, |editor, _window, cx| {
17538 (
17539 editor.buffer().clone(),
17540 editor.buffer().read(cx).snapshot(cx),
17541 )
17542 })
17543 .unwrap();
17544
17545 let edits = edits
17546 .into_iter()
17547 .map(|(range, edit)| {
17548 (
17549 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
17550 edit,
17551 )
17552 })
17553 .collect::<Vec<_>>();
17554
17555 let text_anchor_edits = edits
17556 .clone()
17557 .into_iter()
17558 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
17559 .collect::<Vec<_>>();
17560
17561 let edit_preview = window
17562 .update(cx, |_, _window, cx| {
17563 buffer
17564 .read(cx)
17565 .as_singleton()
17566 .unwrap()
17567 .read(cx)
17568 .preview_edits(text_anchor_edits.into(), cx)
17569 })
17570 .unwrap()
17571 .await;
17572
17573 cx.update(|_window, cx| {
17574 let highlighted_edits = inline_completion_edit_text(
17575 &snapshot.as_singleton().unwrap().2,
17576 &edits,
17577 &edit_preview,
17578 include_deletions,
17579 cx,
17580 );
17581 assertion_fn(highlighted_edits, cx)
17582 });
17583}
17584
17585#[track_caller]
17586fn assert_breakpoint(
17587 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
17588 path: &Arc<Path>,
17589 expected: Vec<(u32, Breakpoint)>,
17590) {
17591 if expected.len() == 0usize {
17592 assert!(!breakpoints.contains_key(path), "{}", path.display());
17593 } else {
17594 let mut breakpoint = breakpoints
17595 .get(path)
17596 .unwrap()
17597 .into_iter()
17598 .map(|breakpoint| {
17599 (
17600 breakpoint.row,
17601 Breakpoint {
17602 message: breakpoint.message.clone(),
17603 state: breakpoint.state,
17604 condition: breakpoint.condition.clone(),
17605 hit_condition: breakpoint.hit_condition.clone(),
17606 },
17607 )
17608 })
17609 .collect::<Vec<_>>();
17610
17611 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
17612
17613 assert_eq!(expected, breakpoint);
17614 }
17615}
17616
17617fn add_log_breakpoint_at_cursor(
17618 editor: &mut Editor,
17619 log_message: &str,
17620 window: &mut Window,
17621 cx: &mut Context<Editor>,
17622) {
17623 let (anchor, bp) = editor
17624 .breakpoint_at_cursor_head(window, cx)
17625 .unwrap_or_else(|| {
17626 let cursor_position: Point = editor.selections.newest(cx).head();
17627
17628 let breakpoint_position = editor
17629 .snapshot(window, cx)
17630 .display_snapshot
17631 .buffer_snapshot
17632 .anchor_before(Point::new(cursor_position.row, 0));
17633
17634 (breakpoint_position, Breakpoint::new_log(&log_message))
17635 });
17636
17637 editor.edit_breakpoint_at_anchor(
17638 anchor,
17639 bp,
17640 BreakpointEditAction::EditLogMessage(log_message.into()),
17641 cx,
17642 );
17643}
17644
17645#[gpui::test]
17646async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
17647 init_test(cx, |_| {});
17648
17649 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17650 let fs = FakeFs::new(cx.executor());
17651 fs.insert_tree(
17652 path!("/a"),
17653 json!({
17654 "main.rs": sample_text,
17655 }),
17656 )
17657 .await;
17658 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17659 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17660 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17661
17662 let fs = FakeFs::new(cx.executor());
17663 fs.insert_tree(
17664 path!("/a"),
17665 json!({
17666 "main.rs": sample_text,
17667 }),
17668 )
17669 .await;
17670 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17671 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17672 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17673 let worktree_id = workspace
17674 .update(cx, |workspace, _window, cx| {
17675 workspace.project().update(cx, |project, cx| {
17676 project.worktrees(cx).next().unwrap().read(cx).id()
17677 })
17678 })
17679 .unwrap();
17680
17681 let buffer = project
17682 .update(cx, |project, cx| {
17683 project.open_buffer((worktree_id, "main.rs"), cx)
17684 })
17685 .await
17686 .unwrap();
17687
17688 let (editor, cx) = cx.add_window_view(|window, cx| {
17689 Editor::new(
17690 EditorMode::Full,
17691 MultiBuffer::build_from_buffer(buffer, cx),
17692 Some(project.clone()),
17693 window,
17694 cx,
17695 )
17696 });
17697
17698 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17699 let abs_path = project.read_with(cx, |project, cx| {
17700 project
17701 .absolute_path(&project_path, cx)
17702 .map(|path_buf| Arc::from(path_buf.to_owned()))
17703 .unwrap()
17704 });
17705
17706 // assert we can add breakpoint on the first line
17707 editor.update_in(cx, |editor, window, cx| {
17708 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17709 editor.move_to_end(&MoveToEnd, window, cx);
17710 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17711 });
17712
17713 let breakpoints = editor.update(cx, |editor, cx| {
17714 editor
17715 .breakpoint_store()
17716 .as_ref()
17717 .unwrap()
17718 .read(cx)
17719 .all_breakpoints(cx)
17720 .clone()
17721 });
17722
17723 assert_eq!(1, breakpoints.len());
17724 assert_breakpoint(
17725 &breakpoints,
17726 &abs_path,
17727 vec![
17728 (0, Breakpoint::new_standard()),
17729 (3, Breakpoint::new_standard()),
17730 ],
17731 );
17732
17733 editor.update_in(cx, |editor, window, cx| {
17734 editor.move_to_beginning(&MoveToBeginning, window, cx);
17735 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17736 });
17737
17738 let breakpoints = editor.update(cx, |editor, cx| {
17739 editor
17740 .breakpoint_store()
17741 .as_ref()
17742 .unwrap()
17743 .read(cx)
17744 .all_breakpoints(cx)
17745 .clone()
17746 });
17747
17748 assert_eq!(1, breakpoints.len());
17749 assert_breakpoint(
17750 &breakpoints,
17751 &abs_path,
17752 vec![(3, Breakpoint::new_standard())],
17753 );
17754
17755 editor.update_in(cx, |editor, window, cx| {
17756 editor.move_to_end(&MoveToEnd, window, cx);
17757 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17758 });
17759
17760 let breakpoints = editor.update(cx, |editor, cx| {
17761 editor
17762 .breakpoint_store()
17763 .as_ref()
17764 .unwrap()
17765 .read(cx)
17766 .all_breakpoints(cx)
17767 .clone()
17768 });
17769
17770 assert_eq!(0, breakpoints.len());
17771 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17772}
17773
17774#[gpui::test]
17775async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
17776 init_test(cx, |_| {});
17777
17778 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17779
17780 let fs = FakeFs::new(cx.executor());
17781 fs.insert_tree(
17782 path!("/a"),
17783 json!({
17784 "main.rs": sample_text,
17785 }),
17786 )
17787 .await;
17788 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17789 let (workspace, cx) =
17790 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
17791
17792 let worktree_id = workspace.update(cx, |workspace, cx| {
17793 workspace.project().update(cx, |project, cx| {
17794 project.worktrees(cx).next().unwrap().read(cx).id()
17795 })
17796 });
17797
17798 let buffer = project
17799 .update(cx, |project, cx| {
17800 project.open_buffer((worktree_id, "main.rs"), cx)
17801 })
17802 .await
17803 .unwrap();
17804
17805 let (editor, cx) = cx.add_window_view(|window, cx| {
17806 Editor::new(
17807 EditorMode::Full,
17808 MultiBuffer::build_from_buffer(buffer, cx),
17809 Some(project.clone()),
17810 window,
17811 cx,
17812 )
17813 });
17814
17815 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17816 let abs_path = project.read_with(cx, |project, cx| {
17817 project
17818 .absolute_path(&project_path, cx)
17819 .map(|path_buf| Arc::from(path_buf.to_owned()))
17820 .unwrap()
17821 });
17822
17823 editor.update_in(cx, |editor, window, cx| {
17824 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17825 });
17826
17827 let breakpoints = editor.update(cx, |editor, cx| {
17828 editor
17829 .breakpoint_store()
17830 .as_ref()
17831 .unwrap()
17832 .read(cx)
17833 .all_breakpoints(cx)
17834 .clone()
17835 });
17836
17837 assert_breakpoint(
17838 &breakpoints,
17839 &abs_path,
17840 vec![(0, Breakpoint::new_log("hello world"))],
17841 );
17842
17843 // Removing a log message from a log breakpoint should remove it
17844 editor.update_in(cx, |editor, window, cx| {
17845 add_log_breakpoint_at_cursor(editor, "", window, cx);
17846 });
17847
17848 let breakpoints = editor.update(cx, |editor, cx| {
17849 editor
17850 .breakpoint_store()
17851 .as_ref()
17852 .unwrap()
17853 .read(cx)
17854 .all_breakpoints(cx)
17855 .clone()
17856 });
17857
17858 assert_breakpoint(&breakpoints, &abs_path, vec![]);
17859
17860 editor.update_in(cx, |editor, window, cx| {
17861 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17862 editor.move_to_end(&MoveToEnd, window, cx);
17863 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
17864 // Not adding a log message to a standard breakpoint shouldn't remove it
17865 add_log_breakpoint_at_cursor(editor, "", window, cx);
17866 });
17867
17868 let breakpoints = editor.update(cx, |editor, cx| {
17869 editor
17870 .breakpoint_store()
17871 .as_ref()
17872 .unwrap()
17873 .read(cx)
17874 .all_breakpoints(cx)
17875 .clone()
17876 });
17877
17878 assert_breakpoint(
17879 &breakpoints,
17880 &abs_path,
17881 vec![
17882 (0, Breakpoint::new_standard()),
17883 (3, Breakpoint::new_standard()),
17884 ],
17885 );
17886
17887 editor.update_in(cx, |editor, window, cx| {
17888 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
17889 });
17890
17891 let breakpoints = editor.update(cx, |editor, cx| {
17892 editor
17893 .breakpoint_store()
17894 .as_ref()
17895 .unwrap()
17896 .read(cx)
17897 .all_breakpoints(cx)
17898 .clone()
17899 });
17900
17901 assert_breakpoint(
17902 &breakpoints,
17903 &abs_path,
17904 vec![
17905 (0, Breakpoint::new_standard()),
17906 (3, Breakpoint::new_log("hello world")),
17907 ],
17908 );
17909
17910 editor.update_in(cx, |editor, window, cx| {
17911 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
17912 });
17913
17914 let breakpoints = editor.update(cx, |editor, cx| {
17915 editor
17916 .breakpoint_store()
17917 .as_ref()
17918 .unwrap()
17919 .read(cx)
17920 .all_breakpoints(cx)
17921 .clone()
17922 });
17923
17924 assert_breakpoint(
17925 &breakpoints,
17926 &abs_path,
17927 vec![
17928 (0, Breakpoint::new_standard()),
17929 (3, Breakpoint::new_log("hello Earth!!")),
17930 ],
17931 );
17932}
17933
17934/// This also tests that Editor::breakpoint_at_cursor_head is working properly
17935/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
17936/// or when breakpoints were placed out of order. This tests for a regression too
17937#[gpui::test]
17938async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
17939 init_test(cx, |_| {});
17940
17941 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
17942 let fs = FakeFs::new(cx.executor());
17943 fs.insert_tree(
17944 path!("/a"),
17945 json!({
17946 "main.rs": sample_text,
17947 }),
17948 )
17949 .await;
17950 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17951 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17952 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17953
17954 let fs = FakeFs::new(cx.executor());
17955 fs.insert_tree(
17956 path!("/a"),
17957 json!({
17958 "main.rs": sample_text,
17959 }),
17960 )
17961 .await;
17962 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17963 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17964 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17965 let worktree_id = workspace
17966 .update(cx, |workspace, _window, cx| {
17967 workspace.project().update(cx, |project, cx| {
17968 project.worktrees(cx).next().unwrap().read(cx).id()
17969 })
17970 })
17971 .unwrap();
17972
17973 let buffer = project
17974 .update(cx, |project, cx| {
17975 project.open_buffer((worktree_id, "main.rs"), cx)
17976 })
17977 .await
17978 .unwrap();
17979
17980 let (editor, cx) = cx.add_window_view(|window, cx| {
17981 Editor::new(
17982 EditorMode::Full,
17983 MultiBuffer::build_from_buffer(buffer, cx),
17984 Some(project.clone()),
17985 window,
17986 cx,
17987 )
17988 });
17989
17990 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
17991 let abs_path = project.read_with(cx, |project, cx| {
17992 project
17993 .absolute_path(&project_path, cx)
17994 .map(|path_buf| Arc::from(path_buf.to_owned()))
17995 .unwrap()
17996 });
17997
17998 // assert we can add breakpoint on the first line
17999 editor.update_in(cx, |editor, window, cx| {
18000 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18001 editor.move_to_end(&MoveToEnd, window, cx);
18002 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18003 editor.move_up(&MoveUp, window, cx);
18004 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18005 });
18006
18007 let breakpoints = editor.update(cx, |editor, cx| {
18008 editor
18009 .breakpoint_store()
18010 .as_ref()
18011 .unwrap()
18012 .read(cx)
18013 .all_breakpoints(cx)
18014 .clone()
18015 });
18016
18017 assert_eq!(1, breakpoints.len());
18018 assert_breakpoint(
18019 &breakpoints,
18020 &abs_path,
18021 vec![
18022 (0, Breakpoint::new_standard()),
18023 (2, Breakpoint::new_standard()),
18024 (3, Breakpoint::new_standard()),
18025 ],
18026 );
18027
18028 editor.update_in(cx, |editor, window, cx| {
18029 editor.move_to_beginning(&MoveToBeginning, window, cx);
18030 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18031 editor.move_to_end(&MoveToEnd, window, cx);
18032 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18033 // Disabling a breakpoint that doesn't exist should do nothing
18034 editor.move_up(&MoveUp, window, cx);
18035 editor.move_up(&MoveUp, window, cx);
18036 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18037 });
18038
18039 let breakpoints = editor.update(cx, |editor, cx| {
18040 editor
18041 .breakpoint_store()
18042 .as_ref()
18043 .unwrap()
18044 .read(cx)
18045 .all_breakpoints(cx)
18046 .clone()
18047 });
18048
18049 let disable_breakpoint = {
18050 let mut bp = Breakpoint::new_standard();
18051 bp.state = BreakpointState::Disabled;
18052 bp
18053 };
18054
18055 assert_eq!(1, breakpoints.len());
18056 assert_breakpoint(
18057 &breakpoints,
18058 &abs_path,
18059 vec![
18060 (0, disable_breakpoint.clone()),
18061 (2, Breakpoint::new_standard()),
18062 (3, disable_breakpoint.clone()),
18063 ],
18064 );
18065
18066 editor.update_in(cx, |editor, window, cx| {
18067 editor.move_to_beginning(&MoveToBeginning, window, cx);
18068 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18069 editor.move_to_end(&MoveToEnd, window, cx);
18070 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18071 editor.move_up(&MoveUp, window, cx);
18072 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18073 });
18074
18075 let breakpoints = editor.update(cx, |editor, cx| {
18076 editor
18077 .breakpoint_store()
18078 .as_ref()
18079 .unwrap()
18080 .read(cx)
18081 .all_breakpoints(cx)
18082 .clone()
18083 });
18084
18085 assert_eq!(1, breakpoints.len());
18086 assert_breakpoint(
18087 &breakpoints,
18088 &abs_path,
18089 vec![
18090 (0, Breakpoint::new_standard()),
18091 (2, disable_breakpoint),
18092 (3, Breakpoint::new_standard()),
18093 ],
18094 );
18095}
18096
18097#[gpui::test]
18098async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18099 init_test(cx, |_| {});
18100 let capabilities = lsp::ServerCapabilities {
18101 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18102 prepare_provider: Some(true),
18103 work_done_progress_options: Default::default(),
18104 })),
18105 ..Default::default()
18106 };
18107 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18108
18109 cx.set_state(indoc! {"
18110 struct Fˇoo {}
18111 "});
18112
18113 cx.update_editor(|editor, _, cx| {
18114 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18115 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18116 editor.highlight_background::<DocumentHighlightRead>(
18117 &[highlight_range],
18118 |c| c.editor_document_highlight_read_background,
18119 cx,
18120 );
18121 });
18122
18123 let mut prepare_rename_handler = cx
18124 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18125 move |_, _, _| async move {
18126 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18127 start: lsp::Position {
18128 line: 0,
18129 character: 7,
18130 },
18131 end: lsp::Position {
18132 line: 0,
18133 character: 10,
18134 },
18135 })))
18136 },
18137 );
18138 let prepare_rename_task = cx
18139 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18140 .expect("Prepare rename was not started");
18141 prepare_rename_handler.next().await.unwrap();
18142 prepare_rename_task.await.expect("Prepare rename failed");
18143
18144 let mut rename_handler =
18145 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18146 let edit = lsp::TextEdit {
18147 range: lsp::Range {
18148 start: lsp::Position {
18149 line: 0,
18150 character: 7,
18151 },
18152 end: lsp::Position {
18153 line: 0,
18154 character: 10,
18155 },
18156 },
18157 new_text: "FooRenamed".to_string(),
18158 };
18159 Ok(Some(lsp::WorkspaceEdit::new(
18160 // Specify the same edit twice
18161 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18162 )))
18163 });
18164 let rename_task = cx
18165 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18166 .expect("Confirm rename was not started");
18167 rename_handler.next().await.unwrap();
18168 rename_task.await.expect("Confirm rename failed");
18169 cx.run_until_parked();
18170
18171 // Despite two edits, only one is actually applied as those are identical
18172 cx.assert_editor_state(indoc! {"
18173 struct FooRenamedˇ {}
18174 "});
18175}
18176
18177#[gpui::test]
18178async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18179 init_test(cx, |_| {});
18180 // These capabilities indicate that the server does not support prepare rename.
18181 let capabilities = lsp::ServerCapabilities {
18182 rename_provider: Some(lsp::OneOf::Left(true)),
18183 ..Default::default()
18184 };
18185 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18186
18187 cx.set_state(indoc! {"
18188 struct Fˇoo {}
18189 "});
18190
18191 cx.update_editor(|editor, _window, cx| {
18192 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18193 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18194 editor.highlight_background::<DocumentHighlightRead>(
18195 &[highlight_range],
18196 |c| c.editor_document_highlight_read_background,
18197 cx,
18198 );
18199 });
18200
18201 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18202 .expect("Prepare rename was not started")
18203 .await
18204 .expect("Prepare rename failed");
18205
18206 let mut rename_handler =
18207 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18208 let edit = lsp::TextEdit {
18209 range: lsp::Range {
18210 start: lsp::Position {
18211 line: 0,
18212 character: 7,
18213 },
18214 end: lsp::Position {
18215 line: 0,
18216 character: 10,
18217 },
18218 },
18219 new_text: "FooRenamed".to_string(),
18220 };
18221 Ok(Some(lsp::WorkspaceEdit::new(
18222 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
18223 )))
18224 });
18225 let rename_task = cx
18226 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18227 .expect("Confirm rename was not started");
18228 rename_handler.next().await.unwrap();
18229 rename_task.await.expect("Confirm rename failed");
18230 cx.run_until_parked();
18231
18232 // Correct range is renamed, as `surrounding_word` is used to find it.
18233 cx.assert_editor_state(indoc! {"
18234 struct FooRenamedˇ {}
18235 "});
18236}
18237
18238#[gpui::test]
18239async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
18240 init_test(cx, |_| {});
18241 let mut cx = EditorTestContext::new(cx).await;
18242
18243 let language = Arc::new(
18244 Language::new(
18245 LanguageConfig::default(),
18246 Some(tree_sitter_html::LANGUAGE.into()),
18247 )
18248 .with_brackets_query(
18249 r#"
18250 ("<" @open "/>" @close)
18251 ("</" @open ">" @close)
18252 ("<" @open ">" @close)
18253 ("\"" @open "\"" @close)
18254 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
18255 "#,
18256 )
18257 .unwrap(),
18258 );
18259 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
18260
18261 cx.set_state(indoc! {"
18262 <span>ˇ</span>
18263 "});
18264 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18265 cx.assert_editor_state(indoc! {"
18266 <span>
18267 ˇ
18268 </span>
18269 "});
18270
18271 cx.set_state(indoc! {"
18272 <span><span></span>ˇ</span>
18273 "});
18274 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18275 cx.assert_editor_state(indoc! {"
18276 <span><span></span>
18277 ˇ</span>
18278 "});
18279
18280 cx.set_state(indoc! {"
18281 <span>ˇ
18282 </span>
18283 "});
18284 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
18285 cx.assert_editor_state(indoc! {"
18286 <span>
18287 ˇ
18288 </span>
18289 "});
18290}
18291
18292#[gpui::test(iterations = 10)]
18293async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
18294 init_test(cx, |_| {});
18295
18296 let fs = FakeFs::new(cx.executor());
18297 fs.insert_tree(
18298 path!("/dir"),
18299 json!({
18300 "a.ts": "a",
18301 }),
18302 )
18303 .await;
18304
18305 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
18306 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18307 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18308
18309 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18310 language_registry.add(Arc::new(Language::new(
18311 LanguageConfig {
18312 name: "TypeScript".into(),
18313 matcher: LanguageMatcher {
18314 path_suffixes: vec!["ts".to_string()],
18315 ..Default::default()
18316 },
18317 ..Default::default()
18318 },
18319 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18320 )));
18321 let mut fake_language_servers = language_registry.register_fake_lsp(
18322 "TypeScript",
18323 FakeLspAdapter {
18324 capabilities: lsp::ServerCapabilities {
18325 code_lens_provider: Some(lsp::CodeLensOptions {
18326 resolve_provider: Some(true),
18327 }),
18328 execute_command_provider: Some(lsp::ExecuteCommandOptions {
18329 commands: vec!["_the/command".to_string()],
18330 ..lsp::ExecuteCommandOptions::default()
18331 }),
18332 ..lsp::ServerCapabilities::default()
18333 },
18334 ..FakeLspAdapter::default()
18335 },
18336 );
18337
18338 let (buffer, _handle) = project
18339 .update(cx, |p, cx| {
18340 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
18341 })
18342 .await
18343 .unwrap();
18344 cx.executor().run_until_parked();
18345
18346 let fake_server = fake_language_servers.next().await.unwrap();
18347
18348 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
18349 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
18350 drop(buffer_snapshot);
18351 let actions = cx
18352 .update_window(*workspace, |_, window, cx| {
18353 project.code_actions(&buffer, anchor..anchor, window, cx)
18354 })
18355 .unwrap();
18356
18357 fake_server
18358 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
18359 Ok(Some(vec![
18360 lsp::CodeLens {
18361 range: lsp::Range::default(),
18362 command: Some(lsp::Command {
18363 title: "Code lens command".to_owned(),
18364 command: "_the/command".to_owned(),
18365 arguments: None,
18366 }),
18367 data: None,
18368 },
18369 lsp::CodeLens {
18370 range: lsp::Range::default(),
18371 command: Some(lsp::Command {
18372 title: "Command not in capabilities".to_owned(),
18373 command: "not in capabilities".to_owned(),
18374 arguments: None,
18375 }),
18376 data: None,
18377 },
18378 lsp::CodeLens {
18379 range: lsp::Range {
18380 start: lsp::Position {
18381 line: 1,
18382 character: 1,
18383 },
18384 end: lsp::Position {
18385 line: 1,
18386 character: 1,
18387 },
18388 },
18389 command: Some(lsp::Command {
18390 title: "Command not in range".to_owned(),
18391 command: "_the/command".to_owned(),
18392 arguments: None,
18393 }),
18394 data: None,
18395 },
18396 ]))
18397 })
18398 .next()
18399 .await;
18400
18401 let actions = actions.await.unwrap();
18402 assert_eq!(
18403 actions.len(),
18404 1,
18405 "Should have only one valid action for the 0..0 range"
18406 );
18407 let action = actions[0].clone();
18408 let apply = project.update(cx, |project, cx| {
18409 project.apply_code_action(buffer.clone(), action, true, cx)
18410 });
18411
18412 // Resolving the code action does not populate its edits. In absence of
18413 // edits, we must execute the given command.
18414 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
18415 |mut lens, _| async move {
18416 let lens_command = lens.command.as_mut().expect("should have a command");
18417 assert_eq!(lens_command.title, "Code lens command");
18418 lens_command.arguments = Some(vec![json!("the-argument")]);
18419 Ok(lens)
18420 },
18421 );
18422
18423 // While executing the command, the language server sends the editor
18424 // a `workspaceEdit` request.
18425 fake_server
18426 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
18427 let fake = fake_server.clone();
18428 move |params, _| {
18429 assert_eq!(params.command, "_the/command");
18430 let fake = fake.clone();
18431 async move {
18432 fake.server
18433 .request::<lsp::request::ApplyWorkspaceEdit>(
18434 lsp::ApplyWorkspaceEditParams {
18435 label: None,
18436 edit: lsp::WorkspaceEdit {
18437 changes: Some(
18438 [(
18439 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
18440 vec![lsp::TextEdit {
18441 range: lsp::Range::new(
18442 lsp::Position::new(0, 0),
18443 lsp::Position::new(0, 0),
18444 ),
18445 new_text: "X".into(),
18446 }],
18447 )]
18448 .into_iter()
18449 .collect(),
18450 ),
18451 ..Default::default()
18452 },
18453 },
18454 )
18455 .await
18456 .unwrap();
18457 Ok(Some(json!(null)))
18458 }
18459 }
18460 })
18461 .next()
18462 .await;
18463
18464 // Applying the code lens command returns a project transaction containing the edits
18465 // sent by the language server in its `workspaceEdit` request.
18466 let transaction = apply.await.unwrap();
18467 assert!(transaction.0.contains_key(&buffer));
18468 buffer.update(cx, |buffer, cx| {
18469 assert_eq!(buffer.text(), "Xa");
18470 buffer.undo(cx);
18471 assert_eq!(buffer.text(), "a");
18472 });
18473}
18474
18475#[gpui::test]
18476async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
18477 init_test(cx, |_| {});
18478
18479 let fs = FakeFs::new(cx.executor());
18480 let main_text = r#"fn main() {
18481println!("1");
18482println!("2");
18483println!("3");
18484println!("4");
18485println!("5");
18486}"#;
18487 let lib_text = "mod foo {}";
18488 fs.insert_tree(
18489 path!("/a"),
18490 json!({
18491 "lib.rs": lib_text,
18492 "main.rs": main_text,
18493 }),
18494 )
18495 .await;
18496
18497 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18498 let (workspace, cx) =
18499 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18500 let worktree_id = workspace.update(cx, |workspace, cx| {
18501 workspace.project().update(cx, |project, cx| {
18502 project.worktrees(cx).next().unwrap().read(cx).id()
18503 })
18504 });
18505
18506 let expected_ranges = vec![
18507 Point::new(0, 0)..Point::new(0, 0),
18508 Point::new(1, 0)..Point::new(1, 1),
18509 Point::new(2, 0)..Point::new(2, 2),
18510 Point::new(3, 0)..Point::new(3, 3),
18511 ];
18512
18513 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18514 let editor_1 = workspace
18515 .update_in(cx, |workspace, window, cx| {
18516 workspace.open_path(
18517 (worktree_id, "main.rs"),
18518 Some(pane_1.downgrade()),
18519 true,
18520 window,
18521 cx,
18522 )
18523 })
18524 .unwrap()
18525 .await
18526 .downcast::<Editor>()
18527 .unwrap();
18528 pane_1.update(cx, |pane, cx| {
18529 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18530 open_editor.update(cx, |editor, cx| {
18531 assert_eq!(
18532 editor.display_text(cx),
18533 main_text,
18534 "Original main.rs text on initial open",
18535 );
18536 assert_eq!(
18537 editor
18538 .selections
18539 .all::<Point>(cx)
18540 .into_iter()
18541 .map(|s| s.range())
18542 .collect::<Vec<_>>(),
18543 vec![Point::zero()..Point::zero()],
18544 "Default selections on initial open",
18545 );
18546 })
18547 });
18548 editor_1.update_in(cx, |editor, window, cx| {
18549 editor.change_selections(None, window, cx, |s| {
18550 s.select_ranges(expected_ranges.clone());
18551 });
18552 });
18553
18554 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
18555 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
18556 });
18557 let editor_2 = workspace
18558 .update_in(cx, |workspace, window, cx| {
18559 workspace.open_path(
18560 (worktree_id, "main.rs"),
18561 Some(pane_2.downgrade()),
18562 true,
18563 window,
18564 cx,
18565 )
18566 })
18567 .unwrap()
18568 .await
18569 .downcast::<Editor>()
18570 .unwrap();
18571 pane_2.update(cx, |pane, cx| {
18572 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18573 open_editor.update(cx, |editor, cx| {
18574 assert_eq!(
18575 editor.display_text(cx),
18576 main_text,
18577 "Original main.rs text on initial open in another panel",
18578 );
18579 assert_eq!(
18580 editor
18581 .selections
18582 .all::<Point>(cx)
18583 .into_iter()
18584 .map(|s| s.range())
18585 .collect::<Vec<_>>(),
18586 vec![Point::zero()..Point::zero()],
18587 "Default selections on initial open in another panel",
18588 );
18589 })
18590 });
18591
18592 editor_2.update_in(cx, |editor, window, cx| {
18593 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
18594 });
18595
18596 let _other_editor_1 = workspace
18597 .update_in(cx, |workspace, window, cx| {
18598 workspace.open_path(
18599 (worktree_id, "lib.rs"),
18600 Some(pane_1.downgrade()),
18601 true,
18602 window,
18603 cx,
18604 )
18605 })
18606 .unwrap()
18607 .await
18608 .downcast::<Editor>()
18609 .unwrap();
18610 pane_1
18611 .update_in(cx, |pane, window, cx| {
18612 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18613 .unwrap()
18614 })
18615 .await
18616 .unwrap();
18617 drop(editor_1);
18618 pane_1.update(cx, |pane, cx| {
18619 pane.active_item()
18620 .unwrap()
18621 .downcast::<Editor>()
18622 .unwrap()
18623 .update(cx, |editor, cx| {
18624 assert_eq!(
18625 editor.display_text(cx),
18626 lib_text,
18627 "Other file should be open and active",
18628 );
18629 });
18630 assert_eq!(pane.items().count(), 1, "No other editors should be open");
18631 });
18632
18633 let _other_editor_2 = workspace
18634 .update_in(cx, |workspace, window, cx| {
18635 workspace.open_path(
18636 (worktree_id, "lib.rs"),
18637 Some(pane_2.downgrade()),
18638 true,
18639 window,
18640 cx,
18641 )
18642 })
18643 .unwrap()
18644 .await
18645 .downcast::<Editor>()
18646 .unwrap();
18647 pane_2
18648 .update_in(cx, |pane, window, cx| {
18649 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
18650 .unwrap()
18651 })
18652 .await
18653 .unwrap();
18654 drop(editor_2);
18655 pane_2.update(cx, |pane, cx| {
18656 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18657 open_editor.update(cx, |editor, cx| {
18658 assert_eq!(
18659 editor.display_text(cx),
18660 lib_text,
18661 "Other file should be open and active in another panel too",
18662 );
18663 });
18664 assert_eq!(
18665 pane.items().count(),
18666 1,
18667 "No other editors should be open in another pane",
18668 );
18669 });
18670
18671 let _editor_1_reopened = workspace
18672 .update_in(cx, |workspace, window, cx| {
18673 workspace.open_path(
18674 (worktree_id, "main.rs"),
18675 Some(pane_1.downgrade()),
18676 true,
18677 window,
18678 cx,
18679 )
18680 })
18681 .unwrap()
18682 .await
18683 .downcast::<Editor>()
18684 .unwrap();
18685 let _editor_2_reopened = workspace
18686 .update_in(cx, |workspace, window, cx| {
18687 workspace.open_path(
18688 (worktree_id, "main.rs"),
18689 Some(pane_2.downgrade()),
18690 true,
18691 window,
18692 cx,
18693 )
18694 })
18695 .unwrap()
18696 .await
18697 .downcast::<Editor>()
18698 .unwrap();
18699 pane_1.update(cx, |pane, cx| {
18700 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18701 open_editor.update(cx, |editor, cx| {
18702 assert_eq!(
18703 editor.display_text(cx),
18704 main_text,
18705 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
18706 );
18707 assert_eq!(
18708 editor
18709 .selections
18710 .all::<Point>(cx)
18711 .into_iter()
18712 .map(|s| s.range())
18713 .collect::<Vec<_>>(),
18714 expected_ranges,
18715 "Previous editor in the 1st panel had selections and should get them restored on reopen",
18716 );
18717 })
18718 });
18719 pane_2.update(cx, |pane, cx| {
18720 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18721 open_editor.update(cx, |editor, cx| {
18722 assert_eq!(
18723 editor.display_text(cx),
18724 r#"fn main() {
18725⋯rintln!("1");
18726⋯intln!("2");
18727⋯ntln!("3");
18728println!("4");
18729println!("5");
18730}"#,
18731 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
18732 );
18733 assert_eq!(
18734 editor
18735 .selections
18736 .all::<Point>(cx)
18737 .into_iter()
18738 .map(|s| s.range())
18739 .collect::<Vec<_>>(),
18740 vec![Point::zero()..Point::zero()],
18741 "Previous editor in the 2nd pane had no selections changed hence should restore none",
18742 );
18743 })
18744 });
18745}
18746
18747#[gpui::test]
18748async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
18749 init_test(cx, |_| {});
18750
18751 let fs = FakeFs::new(cx.executor());
18752 let main_text = r#"fn main() {
18753println!("1");
18754println!("2");
18755println!("3");
18756println!("4");
18757println!("5");
18758}"#;
18759 let lib_text = "mod foo {}";
18760 fs.insert_tree(
18761 path!("/a"),
18762 json!({
18763 "lib.rs": lib_text,
18764 "main.rs": main_text,
18765 }),
18766 )
18767 .await;
18768
18769 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18770 let (workspace, cx) =
18771 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18772 let worktree_id = workspace.update(cx, |workspace, cx| {
18773 workspace.project().update(cx, |project, cx| {
18774 project.worktrees(cx).next().unwrap().read(cx).id()
18775 })
18776 });
18777
18778 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
18779 let editor = workspace
18780 .update_in(cx, |workspace, window, cx| {
18781 workspace.open_path(
18782 (worktree_id, "main.rs"),
18783 Some(pane.downgrade()),
18784 true,
18785 window,
18786 cx,
18787 )
18788 })
18789 .unwrap()
18790 .await
18791 .downcast::<Editor>()
18792 .unwrap();
18793 pane.update(cx, |pane, cx| {
18794 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18795 open_editor.update(cx, |editor, cx| {
18796 assert_eq!(
18797 editor.display_text(cx),
18798 main_text,
18799 "Original main.rs text on initial open",
18800 );
18801 })
18802 });
18803 editor.update_in(cx, |editor, window, cx| {
18804 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
18805 });
18806
18807 cx.update_global(|store: &mut SettingsStore, cx| {
18808 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18809 s.restore_on_file_reopen = Some(false);
18810 });
18811 });
18812 editor.update_in(cx, |editor, window, cx| {
18813 editor.fold_ranges(
18814 vec![
18815 Point::new(1, 0)..Point::new(1, 1),
18816 Point::new(2, 0)..Point::new(2, 2),
18817 Point::new(3, 0)..Point::new(3, 3),
18818 ],
18819 false,
18820 window,
18821 cx,
18822 );
18823 });
18824 pane.update_in(cx, |pane, window, cx| {
18825 pane.close_all_items(&CloseAllItems::default(), window, cx)
18826 .unwrap()
18827 })
18828 .await
18829 .unwrap();
18830 pane.update(cx, |pane, _| {
18831 assert!(pane.active_item().is_none());
18832 });
18833 cx.update_global(|store: &mut SettingsStore, cx| {
18834 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
18835 s.restore_on_file_reopen = Some(true);
18836 });
18837 });
18838
18839 let _editor_reopened = workspace
18840 .update_in(cx, |workspace, window, cx| {
18841 workspace.open_path(
18842 (worktree_id, "main.rs"),
18843 Some(pane.downgrade()),
18844 true,
18845 window,
18846 cx,
18847 )
18848 })
18849 .unwrap()
18850 .await
18851 .downcast::<Editor>()
18852 .unwrap();
18853 pane.update(cx, |pane, cx| {
18854 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
18855 open_editor.update(cx, |editor, cx| {
18856 assert_eq!(
18857 editor.display_text(cx),
18858 main_text,
18859 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
18860 );
18861 })
18862 });
18863}
18864
18865fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
18866 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
18867 point..point
18868}
18869
18870fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
18871 let (text, ranges) = marked_text_ranges(marked_text, true);
18872 assert_eq!(editor.text(cx), text);
18873 assert_eq!(
18874 editor.selections.ranges(cx),
18875 ranges,
18876 "Assert selections are {}",
18877 marked_text
18878 );
18879}
18880
18881pub fn handle_signature_help_request(
18882 cx: &mut EditorLspTestContext,
18883 mocked_response: lsp::SignatureHelp,
18884) -> impl Future<Output = ()> + use<> {
18885 let mut request =
18886 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
18887 let mocked_response = mocked_response.clone();
18888 async move { Ok(Some(mocked_response)) }
18889 });
18890
18891 async move {
18892 request.next().await;
18893 }
18894}
18895
18896/// Handle completion request passing a marked string specifying where the completion
18897/// should be triggered from using '|' character, what range should be replaced, and what completions
18898/// should be returned using '<' and '>' to delimit the range.
18899///
18900/// Also see `handle_completion_request_with_insert_and_replace`.
18901#[track_caller]
18902pub fn handle_completion_request(
18903 cx: &mut EditorLspTestContext,
18904 marked_string: &str,
18905 completions: Vec<&'static str>,
18906 counter: Arc<AtomicUsize>,
18907) -> impl Future<Output = ()> {
18908 let complete_from_marker: TextRangeMarker = '|'.into();
18909 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18910 let (_, mut marked_ranges) = marked_text_ranges_by(
18911 marked_string,
18912 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18913 );
18914
18915 let complete_from_position =
18916 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18917 let replace_range =
18918 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18919
18920 let mut request =
18921 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18922 let completions = completions.clone();
18923 counter.fetch_add(1, atomic::Ordering::Release);
18924 async move {
18925 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18926 assert_eq!(
18927 params.text_document_position.position,
18928 complete_from_position
18929 );
18930 Ok(Some(lsp::CompletionResponse::Array(
18931 completions
18932 .iter()
18933 .map(|completion_text| lsp::CompletionItem {
18934 label: completion_text.to_string(),
18935 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18936 range: replace_range,
18937 new_text: completion_text.to_string(),
18938 })),
18939 ..Default::default()
18940 })
18941 .collect(),
18942 )))
18943 }
18944 });
18945
18946 async move {
18947 request.next().await;
18948 }
18949}
18950
18951/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
18952/// given instead, which also contains an `insert` range.
18953///
18954/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
18955/// that is, `replace_range.start..cursor_pos`.
18956pub fn handle_completion_request_with_insert_and_replace(
18957 cx: &mut EditorLspTestContext,
18958 marked_string: &str,
18959 completions: Vec<&'static str>,
18960 counter: Arc<AtomicUsize>,
18961) -> impl Future<Output = ()> {
18962 let complete_from_marker: TextRangeMarker = '|'.into();
18963 let replace_range_marker: TextRangeMarker = ('<', '>').into();
18964 let (_, mut marked_ranges) = marked_text_ranges_by(
18965 marked_string,
18966 vec![complete_from_marker.clone(), replace_range_marker.clone()],
18967 );
18968
18969 let complete_from_position =
18970 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
18971 let replace_range =
18972 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
18973
18974 let mut request =
18975 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
18976 let completions = completions.clone();
18977 counter.fetch_add(1, atomic::Ordering::Release);
18978 async move {
18979 assert_eq!(params.text_document_position.text_document.uri, url.clone());
18980 assert_eq!(
18981 params.text_document_position.position, complete_from_position,
18982 "marker `|` position doesn't match",
18983 );
18984 Ok(Some(lsp::CompletionResponse::Array(
18985 completions
18986 .iter()
18987 .map(|completion_text| lsp::CompletionItem {
18988 label: completion_text.to_string(),
18989 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18990 lsp::InsertReplaceEdit {
18991 insert: lsp::Range {
18992 start: replace_range.start,
18993 end: complete_from_position,
18994 },
18995 replace: replace_range,
18996 new_text: completion_text.to_string(),
18997 },
18998 )),
18999 ..Default::default()
19000 })
19001 .collect(),
19002 )))
19003 }
19004 });
19005
19006 async move {
19007 request.next().await;
19008 }
19009}
19010
19011fn handle_resolve_completion_request(
19012 cx: &mut EditorLspTestContext,
19013 edits: Option<Vec<(&'static str, &'static str)>>,
19014) -> impl Future<Output = ()> {
19015 let edits = edits.map(|edits| {
19016 edits
19017 .iter()
19018 .map(|(marked_string, new_text)| {
19019 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
19020 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
19021 lsp::TextEdit::new(replace_range, new_text.to_string())
19022 })
19023 .collect::<Vec<_>>()
19024 });
19025
19026 let mut request =
19027 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
19028 let edits = edits.clone();
19029 async move {
19030 Ok(lsp::CompletionItem {
19031 additional_text_edits: edits,
19032 ..Default::default()
19033 })
19034 }
19035 });
19036
19037 async move {
19038 request.next().await;
19039 }
19040}
19041
19042pub(crate) fn update_test_language_settings(
19043 cx: &mut TestAppContext,
19044 f: impl Fn(&mut AllLanguageSettingsContent),
19045) {
19046 cx.update(|cx| {
19047 SettingsStore::update_global(cx, |store, cx| {
19048 store.update_user_settings::<AllLanguageSettings>(cx, f);
19049 });
19050 });
19051}
19052
19053pub(crate) fn update_test_project_settings(
19054 cx: &mut TestAppContext,
19055 f: impl Fn(&mut ProjectSettings),
19056) {
19057 cx.update(|cx| {
19058 SettingsStore::update_global(cx, |store, cx| {
19059 store.update_user_settings::<ProjectSettings>(cx, f);
19060 });
19061 });
19062}
19063
19064pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
19065 cx.update(|cx| {
19066 assets::Assets.load_test_fonts(cx);
19067 let store = SettingsStore::test(cx);
19068 cx.set_global(store);
19069 theme::init(theme::LoadThemes::JustBase, cx);
19070 release_channel::init(SemanticVersion::default(), cx);
19071 client::init_settings(cx);
19072 language::init(cx);
19073 Project::init_settings(cx);
19074 workspace::init_settings(cx);
19075 crate::init(cx);
19076 });
19077
19078 update_test_language_settings(cx, f);
19079}
19080
19081#[track_caller]
19082fn assert_hunk_revert(
19083 not_reverted_text_with_selections: &str,
19084 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
19085 expected_reverted_text_with_selections: &str,
19086 base_text: &str,
19087 cx: &mut EditorLspTestContext,
19088) {
19089 cx.set_state(not_reverted_text_with_selections);
19090 cx.set_head_text(base_text);
19091 cx.executor().run_until_parked();
19092
19093 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
19094 let snapshot = editor.snapshot(window, cx);
19095 let reverted_hunk_statuses = snapshot
19096 .buffer_snapshot
19097 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
19098 .map(|hunk| hunk.status().kind)
19099 .collect::<Vec<_>>();
19100
19101 editor.git_restore(&Default::default(), window, cx);
19102 reverted_hunk_statuses
19103 });
19104 cx.executor().run_until_parked();
19105 cx.assert_editor_state(expected_reverted_text_with_selections);
19106 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
19107}