1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 linked_editing_ranges::LinkedEditingRanges,
7 scroll::scroll_amount::ScrollAmount,
8 test::{
9 assert_text_with_selections, build_editor,
10 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
11 editor_test_context::EditorTestContext,
12 select_ranges,
13 },
14};
15use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
16use futures::StreamExt;
17use gpui::{
18 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
19 VisualTestContext, WindowBounds, WindowOptions, div,
20};
21use indoc::indoc;
22use language::{
23 BracketPairConfig,
24 Capability::ReadWrite,
25 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
26 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
27 language_settings::{
28 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
29 SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::Formatter;
34use lsp::CompletionParams;
35use multi_buffer::{IndentGuide, PathKey};
36use parking_lot::Mutex;
37use pretty_assertions::{assert_eq, assert_ne};
38use project::{
39 FakeFs,
40 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
41 project_settings::LspSettings,
42};
43use serde_json::{self, json};
44use settings::{AllLanguageSettingsContent, ProjectSettingsContent};
45use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
46use std::{
47 iter,
48 sync::atomic::{self, AtomicUsize},
49};
50use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
51use text::ToPoint as _;
52use unindent::Unindent;
53use util::{
54 assert_set_eq, path,
55 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
56 uri,
57};
58use workspace::{
59 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
60 OpenOptions, ViewId,
61 invalid_buffer_view::InvalidBufferView,
62 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
63 register_project_item,
64};
65
66#[gpui::test]
67fn test_edit_events(cx: &mut TestAppContext) {
68 init_test(cx, |_| {});
69
70 let buffer = cx.new(|cx| {
71 let mut buffer = language::Buffer::local("123456", cx);
72 buffer.set_group_interval(Duration::from_secs(1));
73 buffer
74 });
75
76 let events = Rc::new(RefCell::new(Vec::new()));
77 let editor1 = cx.add_window({
78 let events = events.clone();
79 |window, cx| {
80 let entity = cx.entity();
81 cx.subscribe_in(
82 &entity,
83 window,
84 move |_, _, event: &EditorEvent, _, _| match event {
85 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
86 EditorEvent::BufferEdited => {
87 events.borrow_mut().push(("editor1", "buffer edited"))
88 }
89 _ => {}
90 },
91 )
92 .detach();
93 Editor::for_buffer(buffer.clone(), None, window, cx)
94 }
95 });
96
97 let editor2 = cx.add_window({
98 let events = events.clone();
99 |window, cx| {
100 cx.subscribe_in(
101 &cx.entity(),
102 window,
103 move |_, _, event: &EditorEvent, _, _| match event {
104 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
105 EditorEvent::BufferEdited => {
106 events.borrow_mut().push(("editor2", "buffer edited"))
107 }
108 _ => {}
109 },
110 )
111 .detach();
112 Editor::for_buffer(buffer.clone(), None, window, cx)
113 }
114 });
115
116 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
117
118 // Mutating editor 1 will emit an `Edited` event only for that editor.
119 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
120 assert_eq!(
121 mem::take(&mut *events.borrow_mut()),
122 [
123 ("editor1", "edited"),
124 ("editor1", "buffer edited"),
125 ("editor2", "buffer edited"),
126 ]
127 );
128
129 // Mutating editor 2 will emit an `Edited` event only for that editor.
130 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
131 assert_eq!(
132 mem::take(&mut *events.borrow_mut()),
133 [
134 ("editor2", "edited"),
135 ("editor1", "buffer edited"),
136 ("editor2", "buffer edited"),
137 ]
138 );
139
140 // Undoing on editor 1 will emit an `Edited` event only for that editor.
141 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
142 assert_eq!(
143 mem::take(&mut *events.borrow_mut()),
144 [
145 ("editor1", "edited"),
146 ("editor1", "buffer edited"),
147 ("editor2", "buffer edited"),
148 ]
149 );
150
151 // Redoing on editor 1 will emit an `Edited` event only for that editor.
152 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
153 assert_eq!(
154 mem::take(&mut *events.borrow_mut()),
155 [
156 ("editor1", "edited"),
157 ("editor1", "buffer edited"),
158 ("editor2", "buffer edited"),
159 ]
160 );
161
162 // Undoing on editor 2 will emit an `Edited` event only for that editor.
163 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
164 assert_eq!(
165 mem::take(&mut *events.borrow_mut()),
166 [
167 ("editor2", "edited"),
168 ("editor1", "buffer edited"),
169 ("editor2", "buffer edited"),
170 ]
171 );
172
173 // Redoing on editor 2 will emit an `Edited` event only for that editor.
174 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
175 assert_eq!(
176 mem::take(&mut *events.borrow_mut()),
177 [
178 ("editor2", "edited"),
179 ("editor1", "buffer edited"),
180 ("editor2", "buffer edited"),
181 ]
182 );
183
184 // No event is emitted when the mutation is a no-op.
185 _ = editor2.update(cx, |editor, window, cx| {
186 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
187 s.select_ranges([0..0])
188 });
189
190 editor.backspace(&Backspace, window, cx);
191 });
192 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
193}
194
195#[gpui::test]
196fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
197 init_test(cx, |_| {});
198
199 let mut now = Instant::now();
200 let group_interval = Duration::from_millis(1);
201 let buffer = cx.new(|cx| {
202 let mut buf = language::Buffer::local("123456", cx);
203 buf.set_group_interval(group_interval);
204 buf
205 });
206 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
207 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
208
209 _ = editor.update(cx, |editor, window, cx| {
210 editor.start_transaction_at(now, window, cx);
211 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
212 s.select_ranges([2..4])
213 });
214
215 editor.insert("cd", window, cx);
216 editor.end_transaction_at(now, cx);
217 assert_eq!(editor.text(cx), "12cd56");
218 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
219
220 editor.start_transaction_at(now, window, cx);
221 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
222 s.select_ranges([4..5])
223 });
224 editor.insert("e", window, cx);
225 editor.end_transaction_at(now, cx);
226 assert_eq!(editor.text(cx), "12cde6");
227 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
228
229 now += group_interval + Duration::from_millis(1);
230 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
231 s.select_ranges([2..2])
232 });
233
234 // Simulate an edit in another editor
235 buffer.update(cx, |buffer, cx| {
236 buffer.start_transaction_at(now, cx);
237 buffer.edit([(0..1, "a")], None, cx);
238 buffer.edit([(1..1, "b")], None, cx);
239 buffer.end_transaction_at(now, cx);
240 });
241
242 assert_eq!(editor.text(cx), "ab2cde6");
243 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
244
245 // Last transaction happened past the group interval in a different editor.
246 // Undo it individually and don't restore selections.
247 editor.undo(&Undo, window, cx);
248 assert_eq!(editor.text(cx), "12cde6");
249 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
250
251 // First two transactions happened within the group interval in this editor.
252 // Undo them together and restore selections.
253 editor.undo(&Undo, window, cx);
254 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
255 assert_eq!(editor.text(cx), "123456");
256 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
257
258 // Redo the first two transactions together.
259 editor.redo(&Redo, window, cx);
260 assert_eq!(editor.text(cx), "12cde6");
261 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
262
263 // Redo the last transaction on its own.
264 editor.redo(&Redo, window, cx);
265 assert_eq!(editor.text(cx), "ab2cde6");
266 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
267
268 // Test empty transactions.
269 editor.start_transaction_at(now, window, cx);
270 editor.end_transaction_at(now, cx);
271 editor.undo(&Undo, window, cx);
272 assert_eq!(editor.text(cx), "12cde6");
273 });
274}
275
276#[gpui::test]
277fn test_ime_composition(cx: &mut TestAppContext) {
278 init_test(cx, |_| {});
279
280 let buffer = cx.new(|cx| {
281 let mut buffer = language::Buffer::local("abcde", cx);
282 // Ensure automatic grouping doesn't occur.
283 buffer.set_group_interval(Duration::ZERO);
284 buffer
285 });
286
287 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
288 cx.add_window(|window, cx| {
289 let mut editor = build_editor(buffer.clone(), window, cx);
290
291 // Start a new IME composition.
292 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
293 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
294 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
295 assert_eq!(editor.text(cx), "äbcde");
296 assert_eq!(
297 editor.marked_text_ranges(cx),
298 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
299 );
300
301 // Finalize IME composition.
302 editor.replace_text_in_range(None, "ā", window, cx);
303 assert_eq!(editor.text(cx), "ābcde");
304 assert_eq!(editor.marked_text_ranges(cx), None);
305
306 // IME composition edits are grouped and are undone/redone at once.
307 editor.undo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "abcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310 editor.redo(&Default::default(), window, cx);
311 assert_eq!(editor.text(cx), "ābcde");
312 assert_eq!(editor.marked_text_ranges(cx), None);
313
314 // Start a new IME composition.
315 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
316 assert_eq!(
317 editor.marked_text_ranges(cx),
318 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
319 );
320
321 // Undoing during an IME composition cancels it.
322 editor.undo(&Default::default(), window, cx);
323 assert_eq!(editor.text(cx), "ābcde");
324 assert_eq!(editor.marked_text_ranges(cx), None);
325
326 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
327 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
328 assert_eq!(editor.text(cx), "ābcdè");
329 assert_eq!(
330 editor.marked_text_ranges(cx),
331 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
332 );
333
334 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
335 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
336 assert_eq!(editor.text(cx), "ābcdę");
337 assert_eq!(editor.marked_text_ranges(cx), None);
338
339 // Start a new IME composition with multiple cursors.
340 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
341 s.select_ranges([
342 OffsetUtf16(1)..OffsetUtf16(1),
343 OffsetUtf16(3)..OffsetUtf16(3),
344 OffsetUtf16(5)..OffsetUtf16(5),
345 ])
346 });
347 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
348 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
349 assert_eq!(
350 editor.marked_text_ranges(cx),
351 Some(vec![
352 OffsetUtf16(0)..OffsetUtf16(3),
353 OffsetUtf16(4)..OffsetUtf16(7),
354 OffsetUtf16(8)..OffsetUtf16(11)
355 ])
356 );
357
358 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
359 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
360 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
361 assert_eq!(
362 editor.marked_text_ranges(cx),
363 Some(vec![
364 OffsetUtf16(1)..OffsetUtf16(2),
365 OffsetUtf16(5)..OffsetUtf16(6),
366 OffsetUtf16(9)..OffsetUtf16(10)
367 ])
368 );
369
370 // Finalize IME composition with multiple cursors.
371 editor.replace_text_in_range(Some(9..10), "2", window, cx);
372 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
373 assert_eq!(editor.marked_text_ranges(cx), None);
374
375 editor
376 });
377}
378
379#[gpui::test]
380fn test_selection_with_mouse(cx: &mut TestAppContext) {
381 init_test(cx, |_| {});
382
383 let editor = cx.add_window(|window, cx| {
384 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
385 build_editor(buffer, window, cx)
386 });
387
388 _ = editor.update(cx, |editor, window, cx| {
389 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
390 });
391 assert_eq!(
392 editor
393 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
394 .unwrap(),
395 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
396 );
397
398 _ = editor.update(cx, |editor, window, cx| {
399 editor.update_selection(
400 DisplayPoint::new(DisplayRow(3), 3),
401 0,
402 gpui::Point::<f32>::default(),
403 window,
404 cx,
405 );
406 });
407
408 assert_eq!(
409 editor
410 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
411 .unwrap(),
412 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
413 );
414
415 _ = editor.update(cx, |editor, window, cx| {
416 editor.update_selection(
417 DisplayPoint::new(DisplayRow(1), 1),
418 0,
419 gpui::Point::<f32>::default(),
420 window,
421 cx,
422 );
423 });
424
425 assert_eq!(
426 editor
427 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
428 .unwrap(),
429 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
430 );
431
432 _ = editor.update(cx, |editor, window, cx| {
433 editor.end_selection(window, cx);
434 editor.update_selection(
435 DisplayPoint::new(DisplayRow(3), 3),
436 0,
437 gpui::Point::<f32>::default(),
438 window,
439 cx,
440 );
441 });
442
443 assert_eq!(
444 editor
445 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
446 .unwrap(),
447 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
448 );
449
450 _ = editor.update(cx, |editor, window, cx| {
451 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
452 editor.update_selection(
453 DisplayPoint::new(DisplayRow(0), 0),
454 0,
455 gpui::Point::<f32>::default(),
456 window,
457 cx,
458 );
459 });
460
461 assert_eq!(
462 editor
463 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
464 .unwrap(),
465 [
466 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
467 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
468 ]
469 );
470
471 _ = editor.update(cx, |editor, window, cx| {
472 editor.end_selection(window, cx);
473 });
474
475 assert_eq!(
476 editor
477 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
478 .unwrap(),
479 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
480 );
481}
482
483#[gpui::test]
484fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
485 init_test(cx, |_| {});
486
487 let editor = cx.add_window(|window, cx| {
488 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
489 build_editor(buffer, window, cx)
490 });
491
492 _ = editor.update(cx, |editor, window, cx| {
493 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
494 });
495
496 _ = editor.update(cx, |editor, window, cx| {
497 editor.end_selection(window, cx);
498 });
499
500 _ = editor.update(cx, |editor, window, cx| {
501 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
502 });
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.end_selection(window, cx);
506 });
507
508 assert_eq!(
509 editor
510 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
511 .unwrap(),
512 [
513 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
514 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
515 ]
516 );
517
518 _ = editor.update(cx, |editor, window, cx| {
519 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
520 });
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.end_selection(window, cx);
524 });
525
526 assert_eq!(
527 editor
528 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
529 .unwrap(),
530 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
531 );
532}
533
534#[gpui::test]
535fn test_canceling_pending_selection(cx: &mut TestAppContext) {
536 init_test(cx, |_| {});
537
538 let editor = cx.add_window(|window, cx| {
539 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
540 build_editor(buffer, window, cx)
541 });
542
543 _ = editor.update(cx, |editor, window, cx| {
544 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
545 assert_eq!(
546 editor.selections.display_ranges(cx),
547 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
548 );
549 });
550
551 _ = editor.update(cx, |editor, window, cx| {
552 editor.update_selection(
553 DisplayPoint::new(DisplayRow(3), 3),
554 0,
555 gpui::Point::<f32>::default(),
556 window,
557 cx,
558 );
559 assert_eq!(
560 editor.selections.display_ranges(cx),
561 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
562 );
563 });
564
565 _ = editor.update(cx, |editor, window, cx| {
566 editor.cancel(&Cancel, window, cx);
567 editor.update_selection(
568 DisplayPoint::new(DisplayRow(1), 1),
569 0,
570 gpui::Point::<f32>::default(),
571 window,
572 cx,
573 );
574 assert_eq!(
575 editor.selections.display_ranges(cx),
576 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
577 );
578 });
579}
580
581#[gpui::test]
582fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
583 init_test(cx, |_| {});
584
585 let editor = cx.add_window(|window, cx| {
586 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
587 build_editor(buffer, window, cx)
588 });
589
590 _ = editor.update(cx, |editor, window, cx| {
591 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
592 assert_eq!(
593 editor.selections.display_ranges(cx),
594 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
595 );
596
597 editor.move_down(&Default::default(), window, cx);
598 assert_eq!(
599 editor.selections.display_ranges(cx),
600 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
601 );
602
603 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
604 assert_eq!(
605 editor.selections.display_ranges(cx),
606 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
607 );
608
609 editor.move_up(&Default::default(), window, cx);
610 assert_eq!(
611 editor.selections.display_ranges(cx),
612 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
613 );
614 });
615}
616
617#[gpui::test]
618fn test_clone(cx: &mut TestAppContext) {
619 init_test(cx, |_| {});
620
621 let (text, selection_ranges) = marked_text_ranges(
622 indoc! {"
623 one
624 two
625 threeˇ
626 four
627 fiveˇ
628 "},
629 true,
630 );
631
632 let editor = cx.add_window(|window, cx| {
633 let buffer = MultiBuffer::build_simple(&text, cx);
634 build_editor(buffer, window, cx)
635 });
636
637 _ = editor.update(cx, |editor, window, cx| {
638 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
639 s.select_ranges(selection_ranges.clone())
640 });
641 editor.fold_creases(
642 vec![
643 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
644 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
645 ],
646 true,
647 window,
648 cx,
649 );
650 });
651
652 let cloned_editor = editor
653 .update(cx, |editor, _, cx| {
654 cx.open_window(Default::default(), |window, cx| {
655 cx.new(|cx| editor.clone(window, cx))
656 })
657 })
658 .unwrap()
659 .unwrap();
660
661 let snapshot = editor
662 .update(cx, |e, window, cx| e.snapshot(window, cx))
663 .unwrap();
664 let cloned_snapshot = cloned_editor
665 .update(cx, |e, window, cx| e.snapshot(window, cx))
666 .unwrap();
667
668 assert_eq!(
669 cloned_editor
670 .update(cx, |e, _, cx| e.display_text(cx))
671 .unwrap(),
672 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
673 );
674 assert_eq!(
675 cloned_snapshot
676 .folds_in_range(0..text.len())
677 .collect::<Vec<_>>(),
678 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
679 );
680 assert_set_eq!(
681 cloned_editor
682 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
683 .unwrap(),
684 editor
685 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
686 .unwrap()
687 );
688 assert_set_eq!(
689 cloned_editor
690 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
691 .unwrap(),
692 editor
693 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
694 .unwrap()
695 );
696}
697
698#[gpui::test]
699async fn test_navigation_history(cx: &mut TestAppContext) {
700 init_test(cx, |_| {});
701
702 use workspace::item::Item;
703
704 let fs = FakeFs::new(cx.executor());
705 let project = Project::test(fs, [], cx).await;
706 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
707 let pane = workspace
708 .update(cx, |workspace, _, _| workspace.active_pane().clone())
709 .unwrap();
710
711 _ = workspace.update(cx, |_v, window, cx| {
712 cx.new(|cx| {
713 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
714 let mut editor = build_editor(buffer, window, cx);
715 let handle = cx.entity();
716 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
717
718 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
719 editor.nav_history.as_mut().unwrap().pop_backward(cx)
720 }
721
722 // Move the cursor a small distance.
723 // Nothing is added to the navigation history.
724 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
725 s.select_display_ranges([
726 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
727 ])
728 });
729 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
730 s.select_display_ranges([
731 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
732 ])
733 });
734 assert!(pop_history(&mut editor, cx).is_none());
735
736 // Move the cursor a large distance.
737 // The history can jump back to the previous position.
738 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
739 s.select_display_ranges([
740 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
741 ])
742 });
743 let nav_entry = pop_history(&mut editor, cx).unwrap();
744 editor.navigate(nav_entry.data.unwrap(), window, cx);
745 assert_eq!(nav_entry.item.id(), cx.entity_id());
746 assert_eq!(
747 editor.selections.display_ranges(cx),
748 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
749 );
750 assert!(pop_history(&mut editor, cx).is_none());
751
752 // Move the cursor a small distance via the mouse.
753 // Nothing is added to the navigation history.
754 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
755 editor.end_selection(window, cx);
756 assert_eq!(
757 editor.selections.display_ranges(cx),
758 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
759 );
760 assert!(pop_history(&mut editor, cx).is_none());
761
762 // Move the cursor a large distance via the mouse.
763 // The history can jump back to the previous position.
764 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
765 editor.end_selection(window, cx);
766 assert_eq!(
767 editor.selections.display_ranges(cx),
768 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
769 );
770 let nav_entry = pop_history(&mut editor, cx).unwrap();
771 editor.navigate(nav_entry.data.unwrap(), window, cx);
772 assert_eq!(nav_entry.item.id(), cx.entity_id());
773 assert_eq!(
774 editor.selections.display_ranges(cx),
775 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
776 );
777 assert!(pop_history(&mut editor, cx).is_none());
778
779 // Set scroll position to check later
780 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
781 let original_scroll_position = editor.scroll_manager.anchor();
782
783 // Jump to the end of the document and adjust scroll
784 editor.move_to_end(&MoveToEnd, window, cx);
785 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
786 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
787
788 let nav_entry = pop_history(&mut editor, cx).unwrap();
789 editor.navigate(nav_entry.data.unwrap(), window, cx);
790 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
791
792 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
793 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
794 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
795 let invalid_point = Point::new(9999, 0);
796 editor.navigate(
797 Box::new(NavigationData {
798 cursor_anchor: invalid_anchor,
799 cursor_position: invalid_point,
800 scroll_anchor: ScrollAnchor {
801 anchor: invalid_anchor,
802 offset: Default::default(),
803 },
804 scroll_top_row: invalid_point.row,
805 }),
806 window,
807 cx,
808 );
809 assert_eq!(
810 editor.selections.display_ranges(cx),
811 &[editor.max_point(cx)..editor.max_point(cx)]
812 );
813 assert_eq!(
814 editor.scroll_position(cx),
815 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
816 );
817
818 editor
819 })
820 });
821}
822
823#[gpui::test]
824fn test_cancel(cx: &mut TestAppContext) {
825 init_test(cx, |_| {});
826
827 let editor = cx.add_window(|window, cx| {
828 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
829 build_editor(buffer, window, cx)
830 });
831
832 _ = editor.update(cx, |editor, window, cx| {
833 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
834 editor.update_selection(
835 DisplayPoint::new(DisplayRow(1), 1),
836 0,
837 gpui::Point::<f32>::default(),
838 window,
839 cx,
840 );
841 editor.end_selection(window, cx);
842
843 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
844 editor.update_selection(
845 DisplayPoint::new(DisplayRow(0), 3),
846 0,
847 gpui::Point::<f32>::default(),
848 window,
849 cx,
850 );
851 editor.end_selection(window, cx);
852 assert_eq!(
853 editor.selections.display_ranges(cx),
854 [
855 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
856 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
857 ]
858 );
859 });
860
861 _ = editor.update(cx, |editor, window, cx| {
862 editor.cancel(&Cancel, window, cx);
863 assert_eq!(
864 editor.selections.display_ranges(cx),
865 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
866 );
867 });
868
869 _ = editor.update(cx, |editor, window, cx| {
870 editor.cancel(&Cancel, window, cx);
871 assert_eq!(
872 editor.selections.display_ranges(cx),
873 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
874 );
875 });
876}
877
878#[gpui::test]
879fn test_fold_action(cx: &mut TestAppContext) {
880 init_test(cx, |_| {});
881
882 let editor = cx.add_window(|window, cx| {
883 let buffer = MultiBuffer::build_simple(
884 &"
885 impl Foo {
886 // Hello!
887
888 fn a() {
889 1
890 }
891
892 fn b() {
893 2
894 }
895
896 fn c() {
897 3
898 }
899 }
900 "
901 .unindent(),
902 cx,
903 );
904 build_editor(buffer, window, cx)
905 });
906
907 _ = editor.update(cx, |editor, window, cx| {
908 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
909 s.select_display_ranges([
910 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
911 ]);
912 });
913 editor.fold(&Fold, window, cx);
914 assert_eq!(
915 editor.display_text(cx),
916 "
917 impl Foo {
918 // Hello!
919
920 fn a() {
921 1
922 }
923
924 fn b() {⋯
925 }
926
927 fn c() {⋯
928 }
929 }
930 "
931 .unindent(),
932 );
933
934 editor.fold(&Fold, window, cx);
935 assert_eq!(
936 editor.display_text(cx),
937 "
938 impl Foo {⋯
939 }
940 "
941 .unindent(),
942 );
943
944 editor.unfold_lines(&UnfoldLines, window, cx);
945 assert_eq!(
946 editor.display_text(cx),
947 "
948 impl Foo {
949 // Hello!
950
951 fn a() {
952 1
953 }
954
955 fn b() {⋯
956 }
957
958 fn c() {⋯
959 }
960 }
961 "
962 .unindent(),
963 );
964
965 editor.unfold_lines(&UnfoldLines, window, cx);
966 assert_eq!(
967 editor.display_text(cx),
968 editor.buffer.read(cx).read(cx).text()
969 );
970 });
971}
972
973#[gpui::test]
974fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
975 init_test(cx, |_| {});
976
977 let editor = cx.add_window(|window, cx| {
978 let buffer = MultiBuffer::build_simple(
979 &"
980 class Foo:
981 # Hello!
982
983 def a():
984 print(1)
985
986 def b():
987 print(2)
988
989 def c():
990 print(3)
991 "
992 .unindent(),
993 cx,
994 );
995 build_editor(buffer, window, cx)
996 });
997
998 _ = editor.update(cx, |editor, window, cx| {
999 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1000 s.select_display_ranges([
1001 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1002 ]);
1003 });
1004 editor.fold(&Fold, window, cx);
1005 assert_eq!(
1006 editor.display_text(cx),
1007 "
1008 class Foo:
1009 # Hello!
1010
1011 def a():
1012 print(1)
1013
1014 def b():⋯
1015
1016 def c():⋯
1017 "
1018 .unindent(),
1019 );
1020
1021 editor.fold(&Fold, window, cx);
1022 assert_eq!(
1023 editor.display_text(cx),
1024 "
1025 class Foo:⋯
1026 "
1027 .unindent(),
1028 );
1029
1030 editor.unfold_lines(&UnfoldLines, window, cx);
1031 assert_eq!(
1032 editor.display_text(cx),
1033 "
1034 class Foo:
1035 # Hello!
1036
1037 def a():
1038 print(1)
1039
1040 def b():⋯
1041
1042 def c():⋯
1043 "
1044 .unindent(),
1045 );
1046
1047 editor.unfold_lines(&UnfoldLines, window, cx);
1048 assert_eq!(
1049 editor.display_text(cx),
1050 editor.buffer.read(cx).read(cx).text()
1051 );
1052 });
1053}
1054
1055#[gpui::test]
1056fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1057 init_test(cx, |_| {});
1058
1059 let editor = cx.add_window(|window, cx| {
1060 let buffer = MultiBuffer::build_simple(
1061 &"
1062 class Foo:
1063 # Hello!
1064
1065 def a():
1066 print(1)
1067
1068 def b():
1069 print(2)
1070
1071
1072 def c():
1073 print(3)
1074
1075
1076 "
1077 .unindent(),
1078 cx,
1079 );
1080 build_editor(buffer, window, cx)
1081 });
1082
1083 _ = editor.update(cx, |editor, window, cx| {
1084 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1085 s.select_display_ranges([
1086 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1087 ]);
1088 });
1089 editor.fold(&Fold, window, cx);
1090 assert_eq!(
1091 editor.display_text(cx),
1092 "
1093 class Foo:
1094 # Hello!
1095
1096 def a():
1097 print(1)
1098
1099 def b():⋯
1100
1101
1102 def c():⋯
1103
1104
1105 "
1106 .unindent(),
1107 );
1108
1109 editor.fold(&Fold, window, cx);
1110 assert_eq!(
1111 editor.display_text(cx),
1112 "
1113 class Foo:⋯
1114
1115
1116 "
1117 .unindent(),
1118 );
1119
1120 editor.unfold_lines(&UnfoldLines, window, cx);
1121 assert_eq!(
1122 editor.display_text(cx),
1123 "
1124 class Foo:
1125 # Hello!
1126
1127 def a():
1128 print(1)
1129
1130 def b():⋯
1131
1132
1133 def c():⋯
1134
1135
1136 "
1137 .unindent(),
1138 );
1139
1140 editor.unfold_lines(&UnfoldLines, window, cx);
1141 assert_eq!(
1142 editor.display_text(cx),
1143 editor.buffer.read(cx).read(cx).text()
1144 );
1145 });
1146}
1147
1148#[gpui::test]
1149fn test_fold_at_level(cx: &mut TestAppContext) {
1150 init_test(cx, |_| {});
1151
1152 let editor = cx.add_window(|window, cx| {
1153 let buffer = MultiBuffer::build_simple(
1154 &"
1155 class Foo:
1156 # Hello!
1157
1158 def a():
1159 print(1)
1160
1161 def b():
1162 print(2)
1163
1164
1165 class Bar:
1166 # World!
1167
1168 def a():
1169 print(1)
1170
1171 def b():
1172 print(2)
1173
1174
1175 "
1176 .unindent(),
1177 cx,
1178 );
1179 build_editor(buffer, window, cx)
1180 });
1181
1182 _ = editor.update(cx, |editor, window, cx| {
1183 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1184 assert_eq!(
1185 editor.display_text(cx),
1186 "
1187 class Foo:
1188 # Hello!
1189
1190 def a():⋯
1191
1192 def b():⋯
1193
1194
1195 class Bar:
1196 # World!
1197
1198 def a():⋯
1199
1200 def b():⋯
1201
1202
1203 "
1204 .unindent(),
1205 );
1206
1207 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1208 assert_eq!(
1209 editor.display_text(cx),
1210 "
1211 class Foo:⋯
1212
1213
1214 class Bar:⋯
1215
1216
1217 "
1218 .unindent(),
1219 );
1220
1221 editor.unfold_all(&UnfoldAll, window, cx);
1222 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1223 assert_eq!(
1224 editor.display_text(cx),
1225 "
1226 class Foo:
1227 # Hello!
1228
1229 def a():
1230 print(1)
1231
1232 def b():
1233 print(2)
1234
1235
1236 class Bar:
1237 # World!
1238
1239 def a():
1240 print(1)
1241
1242 def b():
1243 print(2)
1244
1245
1246 "
1247 .unindent(),
1248 );
1249
1250 assert_eq!(
1251 editor.display_text(cx),
1252 editor.buffer.read(cx).read(cx).text()
1253 );
1254 });
1255}
1256
1257#[gpui::test]
1258fn test_move_cursor(cx: &mut TestAppContext) {
1259 init_test(cx, |_| {});
1260
1261 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1262 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1263
1264 buffer.update(cx, |buffer, cx| {
1265 buffer.edit(
1266 vec![
1267 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1268 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1269 ],
1270 None,
1271 cx,
1272 );
1273 });
1274 _ = editor.update(cx, |editor, window, cx| {
1275 assert_eq!(
1276 editor.selections.display_ranges(cx),
1277 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1278 );
1279
1280 editor.move_down(&MoveDown, window, cx);
1281 assert_eq!(
1282 editor.selections.display_ranges(cx),
1283 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1284 );
1285
1286 editor.move_right(&MoveRight, window, cx);
1287 assert_eq!(
1288 editor.selections.display_ranges(cx),
1289 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1290 );
1291
1292 editor.move_left(&MoveLeft, window, cx);
1293 assert_eq!(
1294 editor.selections.display_ranges(cx),
1295 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1296 );
1297
1298 editor.move_up(&MoveUp, window, cx);
1299 assert_eq!(
1300 editor.selections.display_ranges(cx),
1301 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1302 );
1303
1304 editor.move_to_end(&MoveToEnd, window, cx);
1305 assert_eq!(
1306 editor.selections.display_ranges(cx),
1307 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1308 );
1309
1310 editor.move_to_beginning(&MoveToBeginning, window, cx);
1311 assert_eq!(
1312 editor.selections.display_ranges(cx),
1313 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1314 );
1315
1316 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1317 s.select_display_ranges([
1318 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1319 ]);
1320 });
1321 editor.select_to_beginning(&SelectToBeginning, window, cx);
1322 assert_eq!(
1323 editor.selections.display_ranges(cx),
1324 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1325 );
1326
1327 editor.select_to_end(&SelectToEnd, window, cx);
1328 assert_eq!(
1329 editor.selections.display_ranges(cx),
1330 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1331 );
1332 });
1333}
1334
1335#[gpui::test]
1336fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1337 init_test(cx, |_| {});
1338
1339 let editor = cx.add_window(|window, cx| {
1340 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1341 build_editor(buffer, window, cx)
1342 });
1343
1344 assert_eq!('🟥'.len_utf8(), 4);
1345 assert_eq!('α'.len_utf8(), 2);
1346
1347 _ = editor.update(cx, |editor, window, cx| {
1348 editor.fold_creases(
1349 vec![
1350 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1351 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1352 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1353 ],
1354 true,
1355 window,
1356 cx,
1357 );
1358 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1359
1360 editor.move_right(&MoveRight, window, cx);
1361 assert_eq!(
1362 editor.selections.display_ranges(cx),
1363 &[empty_range(0, "🟥".len())]
1364 );
1365 editor.move_right(&MoveRight, window, cx);
1366 assert_eq!(
1367 editor.selections.display_ranges(cx),
1368 &[empty_range(0, "🟥🟧".len())]
1369 );
1370 editor.move_right(&MoveRight, window, cx);
1371 assert_eq!(
1372 editor.selections.display_ranges(cx),
1373 &[empty_range(0, "🟥🟧⋯".len())]
1374 );
1375
1376 editor.move_down(&MoveDown, window, cx);
1377 assert_eq!(
1378 editor.selections.display_ranges(cx),
1379 &[empty_range(1, "ab⋯e".len())]
1380 );
1381 editor.move_left(&MoveLeft, window, cx);
1382 assert_eq!(
1383 editor.selections.display_ranges(cx),
1384 &[empty_range(1, "ab⋯".len())]
1385 );
1386 editor.move_left(&MoveLeft, window, cx);
1387 assert_eq!(
1388 editor.selections.display_ranges(cx),
1389 &[empty_range(1, "ab".len())]
1390 );
1391 editor.move_left(&MoveLeft, window, cx);
1392 assert_eq!(
1393 editor.selections.display_ranges(cx),
1394 &[empty_range(1, "a".len())]
1395 );
1396
1397 editor.move_down(&MoveDown, window, cx);
1398 assert_eq!(
1399 editor.selections.display_ranges(cx),
1400 &[empty_range(2, "α".len())]
1401 );
1402 editor.move_right(&MoveRight, window, cx);
1403 assert_eq!(
1404 editor.selections.display_ranges(cx),
1405 &[empty_range(2, "αβ".len())]
1406 );
1407 editor.move_right(&MoveRight, window, cx);
1408 assert_eq!(
1409 editor.selections.display_ranges(cx),
1410 &[empty_range(2, "αβ⋯".len())]
1411 );
1412 editor.move_right(&MoveRight, window, cx);
1413 assert_eq!(
1414 editor.selections.display_ranges(cx),
1415 &[empty_range(2, "αβ⋯ε".len())]
1416 );
1417
1418 editor.move_up(&MoveUp, window, cx);
1419 assert_eq!(
1420 editor.selections.display_ranges(cx),
1421 &[empty_range(1, "ab⋯e".len())]
1422 );
1423 editor.move_down(&MoveDown, window, cx);
1424 assert_eq!(
1425 editor.selections.display_ranges(cx),
1426 &[empty_range(2, "αβ⋯ε".len())]
1427 );
1428 editor.move_up(&MoveUp, window, cx);
1429 assert_eq!(
1430 editor.selections.display_ranges(cx),
1431 &[empty_range(1, "ab⋯e".len())]
1432 );
1433
1434 editor.move_up(&MoveUp, window, cx);
1435 assert_eq!(
1436 editor.selections.display_ranges(cx),
1437 &[empty_range(0, "🟥🟧".len())]
1438 );
1439 editor.move_left(&MoveLeft, window, cx);
1440 assert_eq!(
1441 editor.selections.display_ranges(cx),
1442 &[empty_range(0, "🟥".len())]
1443 );
1444 editor.move_left(&MoveLeft, window, cx);
1445 assert_eq!(
1446 editor.selections.display_ranges(cx),
1447 &[empty_range(0, "".len())]
1448 );
1449 });
1450}
1451
1452#[gpui::test]
1453fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1454 init_test(cx, |_| {});
1455
1456 let editor = cx.add_window(|window, cx| {
1457 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1458 build_editor(buffer, window, cx)
1459 });
1460 _ = editor.update(cx, |editor, window, cx| {
1461 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1462 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1463 });
1464
1465 // moving above start of document should move selection to start of document,
1466 // but the next move down should still be at the original goal_x
1467 editor.move_up(&MoveUp, window, cx);
1468 assert_eq!(
1469 editor.selections.display_ranges(cx),
1470 &[empty_range(0, "".len())]
1471 );
1472
1473 editor.move_down(&MoveDown, window, cx);
1474 assert_eq!(
1475 editor.selections.display_ranges(cx),
1476 &[empty_range(1, "abcd".len())]
1477 );
1478
1479 editor.move_down(&MoveDown, window, cx);
1480 assert_eq!(
1481 editor.selections.display_ranges(cx),
1482 &[empty_range(2, "αβγ".len())]
1483 );
1484
1485 editor.move_down(&MoveDown, window, cx);
1486 assert_eq!(
1487 editor.selections.display_ranges(cx),
1488 &[empty_range(3, "abcd".len())]
1489 );
1490
1491 editor.move_down(&MoveDown, window, cx);
1492 assert_eq!(
1493 editor.selections.display_ranges(cx),
1494 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1495 );
1496
1497 // moving past end of document should not change goal_x
1498 editor.move_down(&MoveDown, window, cx);
1499 assert_eq!(
1500 editor.selections.display_ranges(cx),
1501 &[empty_range(5, "".len())]
1502 );
1503
1504 editor.move_down(&MoveDown, window, cx);
1505 assert_eq!(
1506 editor.selections.display_ranges(cx),
1507 &[empty_range(5, "".len())]
1508 );
1509
1510 editor.move_up(&MoveUp, window, cx);
1511 assert_eq!(
1512 editor.selections.display_ranges(cx),
1513 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1514 );
1515
1516 editor.move_up(&MoveUp, window, cx);
1517 assert_eq!(
1518 editor.selections.display_ranges(cx),
1519 &[empty_range(3, "abcd".len())]
1520 );
1521
1522 editor.move_up(&MoveUp, window, cx);
1523 assert_eq!(
1524 editor.selections.display_ranges(cx),
1525 &[empty_range(2, "αβγ".len())]
1526 );
1527 });
1528}
1529
1530#[gpui::test]
1531fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1532 init_test(cx, |_| {});
1533 let move_to_beg = MoveToBeginningOfLine {
1534 stop_at_soft_wraps: true,
1535 stop_at_indent: true,
1536 };
1537
1538 let delete_to_beg = DeleteToBeginningOfLine {
1539 stop_at_indent: false,
1540 };
1541
1542 let move_to_end = MoveToEndOfLine {
1543 stop_at_soft_wraps: true,
1544 };
1545
1546 let editor = cx.add_window(|window, cx| {
1547 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1548 build_editor(buffer, window, cx)
1549 });
1550 _ = editor.update(cx, |editor, window, cx| {
1551 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1552 s.select_display_ranges([
1553 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1554 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1555 ]);
1556 });
1557 });
1558
1559 _ = editor.update(cx, |editor, window, cx| {
1560 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1561 assert_eq!(
1562 editor.selections.display_ranges(cx),
1563 &[
1564 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1565 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1566 ]
1567 );
1568 });
1569
1570 _ = editor.update(cx, |editor, window, cx| {
1571 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1572 assert_eq!(
1573 editor.selections.display_ranges(cx),
1574 &[
1575 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1576 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1577 ]
1578 );
1579 });
1580
1581 _ = editor.update(cx, |editor, window, cx| {
1582 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1583 assert_eq!(
1584 editor.selections.display_ranges(cx),
1585 &[
1586 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1587 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1588 ]
1589 );
1590 });
1591
1592 _ = editor.update(cx, |editor, window, cx| {
1593 editor.move_to_end_of_line(&move_to_end, window, cx);
1594 assert_eq!(
1595 editor.selections.display_ranges(cx),
1596 &[
1597 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1598 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1599 ]
1600 );
1601 });
1602
1603 // Moving to the end of line again is a no-op.
1604 _ = editor.update(cx, |editor, window, cx| {
1605 editor.move_to_end_of_line(&move_to_end, window, cx);
1606 assert_eq!(
1607 editor.selections.display_ranges(cx),
1608 &[
1609 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1610 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1611 ]
1612 );
1613 });
1614
1615 _ = editor.update(cx, |editor, window, cx| {
1616 editor.move_left(&MoveLeft, window, cx);
1617 editor.select_to_beginning_of_line(
1618 &SelectToBeginningOfLine {
1619 stop_at_soft_wraps: true,
1620 stop_at_indent: true,
1621 },
1622 window,
1623 cx,
1624 );
1625 assert_eq!(
1626 editor.selections.display_ranges(cx),
1627 &[
1628 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1629 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1630 ]
1631 );
1632 });
1633
1634 _ = editor.update(cx, |editor, window, cx| {
1635 editor.select_to_beginning_of_line(
1636 &SelectToBeginningOfLine {
1637 stop_at_soft_wraps: true,
1638 stop_at_indent: true,
1639 },
1640 window,
1641 cx,
1642 );
1643 assert_eq!(
1644 editor.selections.display_ranges(cx),
1645 &[
1646 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1647 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1648 ]
1649 );
1650 });
1651
1652 _ = editor.update(cx, |editor, window, cx| {
1653 editor.select_to_beginning_of_line(
1654 &SelectToBeginningOfLine {
1655 stop_at_soft_wraps: true,
1656 stop_at_indent: true,
1657 },
1658 window,
1659 cx,
1660 );
1661 assert_eq!(
1662 editor.selections.display_ranges(cx),
1663 &[
1664 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1665 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1666 ]
1667 );
1668 });
1669
1670 _ = editor.update(cx, |editor, window, cx| {
1671 editor.select_to_end_of_line(
1672 &SelectToEndOfLine {
1673 stop_at_soft_wraps: true,
1674 },
1675 window,
1676 cx,
1677 );
1678 assert_eq!(
1679 editor.selections.display_ranges(cx),
1680 &[
1681 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1682 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1683 ]
1684 );
1685 });
1686
1687 _ = editor.update(cx, |editor, window, cx| {
1688 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1689 assert_eq!(editor.display_text(cx), "ab\n de");
1690 assert_eq!(
1691 editor.selections.display_ranges(cx),
1692 &[
1693 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1694 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1695 ]
1696 );
1697 });
1698
1699 _ = editor.update(cx, |editor, window, cx| {
1700 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1701 assert_eq!(editor.display_text(cx), "\n");
1702 assert_eq!(
1703 editor.selections.display_ranges(cx),
1704 &[
1705 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1706 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1707 ]
1708 );
1709 });
1710}
1711
1712#[gpui::test]
1713fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1714 init_test(cx, |_| {});
1715 let move_to_beg = MoveToBeginningOfLine {
1716 stop_at_soft_wraps: false,
1717 stop_at_indent: false,
1718 };
1719
1720 let move_to_end = MoveToEndOfLine {
1721 stop_at_soft_wraps: false,
1722 };
1723
1724 let editor = cx.add_window(|window, cx| {
1725 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1726 build_editor(buffer, window, cx)
1727 });
1728
1729 _ = editor.update(cx, |editor, window, cx| {
1730 editor.set_wrap_width(Some(140.0.into()), cx);
1731
1732 // We expect the following lines after wrapping
1733 // ```
1734 // thequickbrownfox
1735 // jumpedoverthelazydo
1736 // gs
1737 // ```
1738 // The final `gs` was soft-wrapped onto a new line.
1739 assert_eq!(
1740 "thequickbrownfox\njumpedoverthelaz\nydogs",
1741 editor.display_text(cx),
1742 );
1743
1744 // First, let's assert behavior on the first line, that was not soft-wrapped.
1745 // Start the cursor at the `k` on the first line
1746 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1747 s.select_display_ranges([
1748 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1749 ]);
1750 });
1751
1752 // Moving to the beginning of the line should put us at the beginning of the line.
1753 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1754 assert_eq!(
1755 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1756 editor.selections.display_ranges(cx)
1757 );
1758
1759 // Moving to the end of the line should put us at the end of the line.
1760 editor.move_to_end_of_line(&move_to_end, window, cx);
1761 assert_eq!(
1762 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1763 editor.selections.display_ranges(cx)
1764 );
1765
1766 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1767 // Start the cursor at the last line (`y` that was wrapped to a new line)
1768 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1769 s.select_display_ranges([
1770 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1771 ]);
1772 });
1773
1774 // Moving to the beginning of the line should put us at the start of the second line of
1775 // display text, i.e., the `j`.
1776 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1777 assert_eq!(
1778 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1779 editor.selections.display_ranges(cx)
1780 );
1781
1782 // Moving to the beginning of the line again should be a no-op.
1783 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1784 assert_eq!(
1785 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1786 editor.selections.display_ranges(cx)
1787 );
1788
1789 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1790 // next display line.
1791 editor.move_to_end_of_line(&move_to_end, window, cx);
1792 assert_eq!(
1793 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1794 editor.selections.display_ranges(cx)
1795 );
1796
1797 // Moving to the end of the line again should be a no-op.
1798 editor.move_to_end_of_line(&move_to_end, window, cx);
1799 assert_eq!(
1800 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1801 editor.selections.display_ranges(cx)
1802 );
1803 });
1804}
1805
1806#[gpui::test]
1807fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1808 init_test(cx, |_| {});
1809
1810 let move_to_beg = MoveToBeginningOfLine {
1811 stop_at_soft_wraps: true,
1812 stop_at_indent: true,
1813 };
1814
1815 let select_to_beg = SelectToBeginningOfLine {
1816 stop_at_soft_wraps: true,
1817 stop_at_indent: true,
1818 };
1819
1820 let delete_to_beg = DeleteToBeginningOfLine {
1821 stop_at_indent: true,
1822 };
1823
1824 let move_to_end = MoveToEndOfLine {
1825 stop_at_soft_wraps: false,
1826 };
1827
1828 let editor = cx.add_window(|window, cx| {
1829 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1830 build_editor(buffer, window, cx)
1831 });
1832
1833 _ = editor.update(cx, |editor, window, cx| {
1834 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1835 s.select_display_ranges([
1836 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1837 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1838 ]);
1839 });
1840
1841 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1842 // and the second cursor at the first non-whitespace character in the line.
1843 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1844 assert_eq!(
1845 editor.selections.display_ranges(cx),
1846 &[
1847 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1848 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1849 ]
1850 );
1851
1852 // Moving to the beginning of the line again should be a no-op for the first cursor,
1853 // and should move the second cursor to the beginning of the line.
1854 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1855 assert_eq!(
1856 editor.selections.display_ranges(cx),
1857 &[
1858 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1859 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1860 ]
1861 );
1862
1863 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1864 // and should move the second cursor back to the first non-whitespace character in the line.
1865 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1866 assert_eq!(
1867 editor.selections.display_ranges(cx),
1868 &[
1869 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1870 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1871 ]
1872 );
1873
1874 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1875 // and to the first non-whitespace character in the line for the second cursor.
1876 editor.move_to_end_of_line(&move_to_end, window, cx);
1877 editor.move_left(&MoveLeft, window, cx);
1878 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1879 assert_eq!(
1880 editor.selections.display_ranges(cx),
1881 &[
1882 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1883 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1884 ]
1885 );
1886
1887 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1888 // and should select to the beginning of the line for the second cursor.
1889 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1890 assert_eq!(
1891 editor.selections.display_ranges(cx),
1892 &[
1893 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1894 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1895 ]
1896 );
1897
1898 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1899 // and should delete to the first non-whitespace character in the line for the second cursor.
1900 editor.move_to_end_of_line(&move_to_end, window, cx);
1901 editor.move_left(&MoveLeft, window, cx);
1902 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1903 assert_eq!(editor.text(cx), "c\n f");
1904 });
1905}
1906
1907#[gpui::test]
1908fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1909 init_test(cx, |_| {});
1910
1911 let move_to_beg = MoveToBeginningOfLine {
1912 stop_at_soft_wraps: true,
1913 stop_at_indent: true,
1914 };
1915
1916 let editor = cx.add_window(|window, cx| {
1917 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1918 build_editor(buffer, window, cx)
1919 });
1920
1921 _ = editor.update(cx, |editor, window, cx| {
1922 // test cursor between line_start and indent_start
1923 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1924 s.select_display_ranges([
1925 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1926 ]);
1927 });
1928
1929 // cursor should move to line_start
1930 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1931 assert_eq!(
1932 editor.selections.display_ranges(cx),
1933 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1934 );
1935
1936 // cursor should move to indent_start
1937 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1938 assert_eq!(
1939 editor.selections.display_ranges(cx),
1940 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
1941 );
1942
1943 // cursor should move to back to line_start
1944 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1945 assert_eq!(
1946 editor.selections.display_ranges(cx),
1947 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1948 );
1949 });
1950}
1951
1952#[gpui::test]
1953fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1954 init_test(cx, |_| {});
1955
1956 let editor = cx.add_window(|window, cx| {
1957 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1958 build_editor(buffer, window, cx)
1959 });
1960 _ = editor.update(cx, |editor, window, cx| {
1961 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1962 s.select_display_ranges([
1963 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1964 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1965 ])
1966 });
1967 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1968 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1969
1970 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1971 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1972
1973 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1974 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1975
1976 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1977 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1978
1979 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1980 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1981
1982 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1983 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1984
1985 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1986 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1987
1988 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1989 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1990
1991 editor.move_right(&MoveRight, window, cx);
1992 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1993 assert_selection_ranges(
1994 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1995 editor,
1996 cx,
1997 );
1998
1999 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2000 assert_selection_ranges(
2001 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2002 editor,
2003 cx,
2004 );
2005
2006 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2007 assert_selection_ranges(
2008 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2009 editor,
2010 cx,
2011 );
2012 });
2013}
2014
2015#[gpui::test]
2016fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2017 init_test(cx, |_| {});
2018
2019 let editor = cx.add_window(|window, cx| {
2020 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2021 build_editor(buffer, window, cx)
2022 });
2023
2024 _ = editor.update(cx, |editor, window, cx| {
2025 editor.set_wrap_width(Some(140.0.into()), cx);
2026 assert_eq!(
2027 editor.display_text(cx),
2028 "use one::{\n two::three::\n four::five\n};"
2029 );
2030
2031 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2032 s.select_display_ranges([
2033 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2034 ]);
2035 });
2036
2037 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2038 assert_eq!(
2039 editor.selections.display_ranges(cx),
2040 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2041 );
2042
2043 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2044 assert_eq!(
2045 editor.selections.display_ranges(cx),
2046 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2047 );
2048
2049 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2050 assert_eq!(
2051 editor.selections.display_ranges(cx),
2052 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2053 );
2054
2055 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2056 assert_eq!(
2057 editor.selections.display_ranges(cx),
2058 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2059 );
2060
2061 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2062 assert_eq!(
2063 editor.selections.display_ranges(cx),
2064 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2065 );
2066
2067 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2068 assert_eq!(
2069 editor.selections.display_ranges(cx),
2070 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2071 );
2072 });
2073}
2074
2075#[gpui::test]
2076async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2077 init_test(cx, |_| {});
2078 let mut cx = EditorTestContext::new(cx).await;
2079
2080 let line_height = cx.editor(|editor, window, _| {
2081 editor
2082 .style()
2083 .unwrap()
2084 .text
2085 .line_height_in_pixels(window.rem_size())
2086 });
2087 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2088
2089 cx.set_state(
2090 &r#"ˇone
2091 two
2092
2093 three
2094 fourˇ
2095 five
2096
2097 six"#
2098 .unindent(),
2099 );
2100
2101 cx.update_editor(|editor, window, cx| {
2102 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2103 });
2104 cx.assert_editor_state(
2105 &r#"one
2106 two
2107 ˇ
2108 three
2109 four
2110 five
2111 ˇ
2112 six"#
2113 .unindent(),
2114 );
2115
2116 cx.update_editor(|editor, window, cx| {
2117 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2118 });
2119 cx.assert_editor_state(
2120 &r#"one
2121 two
2122
2123 three
2124 four
2125 five
2126 ˇ
2127 sixˇ"#
2128 .unindent(),
2129 );
2130
2131 cx.update_editor(|editor, window, cx| {
2132 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2133 });
2134 cx.assert_editor_state(
2135 &r#"one
2136 two
2137
2138 three
2139 four
2140 five
2141
2142 sixˇ"#
2143 .unindent(),
2144 );
2145
2146 cx.update_editor(|editor, window, cx| {
2147 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2148 });
2149 cx.assert_editor_state(
2150 &r#"one
2151 two
2152
2153 three
2154 four
2155 five
2156 ˇ
2157 six"#
2158 .unindent(),
2159 );
2160
2161 cx.update_editor(|editor, window, cx| {
2162 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2163 });
2164 cx.assert_editor_state(
2165 &r#"one
2166 two
2167 ˇ
2168 three
2169 four
2170 five
2171
2172 six"#
2173 .unindent(),
2174 );
2175
2176 cx.update_editor(|editor, window, cx| {
2177 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2178 });
2179 cx.assert_editor_state(
2180 &r#"ˇone
2181 two
2182
2183 three
2184 four
2185 five
2186
2187 six"#
2188 .unindent(),
2189 );
2190}
2191
2192#[gpui::test]
2193async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2194 init_test(cx, |_| {});
2195 let mut cx = EditorTestContext::new(cx).await;
2196 let line_height = cx.editor(|editor, window, _| {
2197 editor
2198 .style()
2199 .unwrap()
2200 .text
2201 .line_height_in_pixels(window.rem_size())
2202 });
2203 let window = cx.window;
2204 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2205
2206 cx.set_state(
2207 r#"ˇone
2208 two
2209 three
2210 four
2211 five
2212 six
2213 seven
2214 eight
2215 nine
2216 ten
2217 "#,
2218 );
2219
2220 cx.update_editor(|editor, window, cx| {
2221 assert_eq!(
2222 editor.snapshot(window, cx).scroll_position(),
2223 gpui::Point::new(0., 0.)
2224 );
2225 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2226 assert_eq!(
2227 editor.snapshot(window, cx).scroll_position(),
2228 gpui::Point::new(0., 3.)
2229 );
2230 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2231 assert_eq!(
2232 editor.snapshot(window, cx).scroll_position(),
2233 gpui::Point::new(0., 6.)
2234 );
2235 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2236 assert_eq!(
2237 editor.snapshot(window, cx).scroll_position(),
2238 gpui::Point::new(0., 3.)
2239 );
2240
2241 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2242 assert_eq!(
2243 editor.snapshot(window, cx).scroll_position(),
2244 gpui::Point::new(0., 1.)
2245 );
2246 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2247 assert_eq!(
2248 editor.snapshot(window, cx).scroll_position(),
2249 gpui::Point::new(0., 3.)
2250 );
2251 });
2252}
2253
2254#[gpui::test]
2255async fn test_autoscroll(cx: &mut TestAppContext) {
2256 init_test(cx, |_| {});
2257 let mut cx = EditorTestContext::new(cx).await;
2258
2259 let line_height = cx.update_editor(|editor, window, cx| {
2260 editor.set_vertical_scroll_margin(2, cx);
2261 editor
2262 .style()
2263 .unwrap()
2264 .text
2265 .line_height_in_pixels(window.rem_size())
2266 });
2267 let window = cx.window;
2268 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2269
2270 cx.set_state(
2271 r#"ˇone
2272 two
2273 three
2274 four
2275 five
2276 six
2277 seven
2278 eight
2279 nine
2280 ten
2281 "#,
2282 );
2283 cx.update_editor(|editor, window, cx| {
2284 assert_eq!(
2285 editor.snapshot(window, cx).scroll_position(),
2286 gpui::Point::new(0., 0.0)
2287 );
2288 });
2289
2290 // Add a cursor below the visible area. Since both cursors cannot fit
2291 // on screen, the editor autoscrolls to reveal the newest cursor, and
2292 // allows the vertical scroll margin below that cursor.
2293 cx.update_editor(|editor, window, cx| {
2294 editor.change_selections(Default::default(), window, cx, |selections| {
2295 selections.select_ranges([
2296 Point::new(0, 0)..Point::new(0, 0),
2297 Point::new(6, 0)..Point::new(6, 0),
2298 ]);
2299 })
2300 });
2301 cx.update_editor(|editor, window, cx| {
2302 assert_eq!(
2303 editor.snapshot(window, cx).scroll_position(),
2304 gpui::Point::new(0., 3.0)
2305 );
2306 });
2307
2308 // Move down. The editor cursor scrolls down to track the newest cursor.
2309 cx.update_editor(|editor, window, cx| {
2310 editor.move_down(&Default::default(), window, cx);
2311 });
2312 cx.update_editor(|editor, window, cx| {
2313 assert_eq!(
2314 editor.snapshot(window, cx).scroll_position(),
2315 gpui::Point::new(0., 4.0)
2316 );
2317 });
2318
2319 // Add a cursor above the visible area. Since both cursors fit on screen,
2320 // the editor scrolls to show both.
2321 cx.update_editor(|editor, window, cx| {
2322 editor.change_selections(Default::default(), window, cx, |selections| {
2323 selections.select_ranges([
2324 Point::new(1, 0)..Point::new(1, 0),
2325 Point::new(6, 0)..Point::new(6, 0),
2326 ]);
2327 })
2328 });
2329 cx.update_editor(|editor, window, cx| {
2330 assert_eq!(
2331 editor.snapshot(window, cx).scroll_position(),
2332 gpui::Point::new(0., 1.0)
2333 );
2334 });
2335}
2336
2337#[gpui::test]
2338async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2339 init_test(cx, |_| {});
2340 let mut cx = EditorTestContext::new(cx).await;
2341
2342 let line_height = cx.editor(|editor, window, _cx| {
2343 editor
2344 .style()
2345 .unwrap()
2346 .text
2347 .line_height_in_pixels(window.rem_size())
2348 });
2349 let window = cx.window;
2350 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2351 cx.set_state(
2352 &r#"
2353 ˇone
2354 two
2355 threeˇ
2356 four
2357 five
2358 six
2359 seven
2360 eight
2361 nine
2362 ten
2363 "#
2364 .unindent(),
2365 );
2366
2367 cx.update_editor(|editor, window, cx| {
2368 editor.move_page_down(&MovePageDown::default(), window, cx)
2369 });
2370 cx.assert_editor_state(
2371 &r#"
2372 one
2373 two
2374 three
2375 ˇfour
2376 five
2377 sixˇ
2378 seven
2379 eight
2380 nine
2381 ten
2382 "#
2383 .unindent(),
2384 );
2385
2386 cx.update_editor(|editor, window, cx| {
2387 editor.move_page_down(&MovePageDown::default(), window, cx)
2388 });
2389 cx.assert_editor_state(
2390 &r#"
2391 one
2392 two
2393 three
2394 four
2395 five
2396 six
2397 ˇseven
2398 eight
2399 nineˇ
2400 ten
2401 "#
2402 .unindent(),
2403 );
2404
2405 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2406 cx.assert_editor_state(
2407 &r#"
2408 one
2409 two
2410 three
2411 ˇfour
2412 five
2413 sixˇ
2414 seven
2415 eight
2416 nine
2417 ten
2418 "#
2419 .unindent(),
2420 );
2421
2422 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2423 cx.assert_editor_state(
2424 &r#"
2425 ˇone
2426 two
2427 threeˇ
2428 four
2429 five
2430 six
2431 seven
2432 eight
2433 nine
2434 ten
2435 "#
2436 .unindent(),
2437 );
2438
2439 // Test select collapsing
2440 cx.update_editor(|editor, window, cx| {
2441 editor.move_page_down(&MovePageDown::default(), window, cx);
2442 editor.move_page_down(&MovePageDown::default(), window, cx);
2443 editor.move_page_down(&MovePageDown::default(), window, cx);
2444 });
2445 cx.assert_editor_state(
2446 &r#"
2447 one
2448 two
2449 three
2450 four
2451 five
2452 six
2453 seven
2454 eight
2455 nine
2456 ˇten
2457 ˇ"#
2458 .unindent(),
2459 );
2460}
2461
2462#[gpui::test]
2463async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2464 init_test(cx, |_| {});
2465 let mut cx = EditorTestContext::new(cx).await;
2466 cx.set_state("one «two threeˇ» four");
2467 cx.update_editor(|editor, window, cx| {
2468 editor.delete_to_beginning_of_line(
2469 &DeleteToBeginningOfLine {
2470 stop_at_indent: false,
2471 },
2472 window,
2473 cx,
2474 );
2475 assert_eq!(editor.text(cx), " four");
2476 });
2477}
2478
2479#[gpui::test]
2480async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2481 init_test(cx, |_| {});
2482
2483 let mut cx = EditorTestContext::new(cx).await;
2484
2485 // For an empty selection, the preceding word fragment is deleted.
2486 // For non-empty selections, only selected characters are deleted.
2487 cx.set_state("onˇe two t«hreˇ»e four");
2488 cx.update_editor(|editor, window, cx| {
2489 editor.delete_to_previous_word_start(
2490 &DeleteToPreviousWordStart {
2491 ignore_newlines: false,
2492 ignore_brackets: false,
2493 },
2494 window,
2495 cx,
2496 );
2497 });
2498 cx.assert_editor_state("ˇe two tˇe four");
2499
2500 cx.set_state("e tˇwo te «fˇ»our");
2501 cx.update_editor(|editor, window, cx| {
2502 editor.delete_to_next_word_end(
2503 &DeleteToNextWordEnd {
2504 ignore_newlines: false,
2505 ignore_brackets: false,
2506 },
2507 window,
2508 cx,
2509 );
2510 });
2511 cx.assert_editor_state("e tˇ te ˇour");
2512}
2513
2514#[gpui::test]
2515async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2516 init_test(cx, |_| {});
2517
2518 let mut cx = EditorTestContext::new(cx).await;
2519
2520 cx.set_state("here is some text ˇwith a space");
2521 cx.update_editor(|editor, window, cx| {
2522 editor.delete_to_previous_word_start(
2523 &DeleteToPreviousWordStart {
2524 ignore_newlines: false,
2525 ignore_brackets: true,
2526 },
2527 window,
2528 cx,
2529 );
2530 });
2531 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2532 cx.assert_editor_state("here is some textˇwith a space");
2533
2534 cx.set_state("here is some text ˇwith a space");
2535 cx.update_editor(|editor, window, cx| {
2536 editor.delete_to_previous_word_start(
2537 &DeleteToPreviousWordStart {
2538 ignore_newlines: false,
2539 ignore_brackets: false,
2540 },
2541 window,
2542 cx,
2543 );
2544 });
2545 cx.assert_editor_state("here is some textˇwith a space");
2546
2547 cx.set_state("here is some textˇ with a space");
2548 cx.update_editor(|editor, window, cx| {
2549 editor.delete_to_next_word_end(
2550 &DeleteToNextWordEnd {
2551 ignore_newlines: false,
2552 ignore_brackets: true,
2553 },
2554 window,
2555 cx,
2556 );
2557 });
2558 // Same happens in the other direction.
2559 cx.assert_editor_state("here is some textˇwith a space");
2560
2561 cx.set_state("here is some textˇ with a space");
2562 cx.update_editor(|editor, window, cx| {
2563 editor.delete_to_next_word_end(
2564 &DeleteToNextWordEnd {
2565 ignore_newlines: false,
2566 ignore_brackets: false,
2567 },
2568 window,
2569 cx,
2570 );
2571 });
2572 cx.assert_editor_state("here is some textˇwith a space");
2573
2574 cx.set_state("here is some textˇ with a space");
2575 cx.update_editor(|editor, window, cx| {
2576 editor.delete_to_next_word_end(
2577 &DeleteToNextWordEnd {
2578 ignore_newlines: true,
2579 ignore_brackets: false,
2580 },
2581 window,
2582 cx,
2583 );
2584 });
2585 cx.assert_editor_state("here is some textˇwith a space");
2586 cx.update_editor(|editor, window, cx| {
2587 editor.delete_to_previous_word_start(
2588 &DeleteToPreviousWordStart {
2589 ignore_newlines: true,
2590 ignore_brackets: false,
2591 },
2592 window,
2593 cx,
2594 );
2595 });
2596 cx.assert_editor_state("here is some ˇwith a space");
2597 cx.update_editor(|editor, window, cx| {
2598 editor.delete_to_previous_word_start(
2599 &DeleteToPreviousWordStart {
2600 ignore_newlines: true,
2601 ignore_brackets: false,
2602 },
2603 window,
2604 cx,
2605 );
2606 });
2607 // Single whitespaces are removed with the word behind them.
2608 cx.assert_editor_state("here is ˇwith a space");
2609 cx.update_editor(|editor, window, cx| {
2610 editor.delete_to_previous_word_start(
2611 &DeleteToPreviousWordStart {
2612 ignore_newlines: true,
2613 ignore_brackets: false,
2614 },
2615 window,
2616 cx,
2617 );
2618 });
2619 cx.assert_editor_state("here ˇwith a space");
2620 cx.update_editor(|editor, window, cx| {
2621 editor.delete_to_previous_word_start(
2622 &DeleteToPreviousWordStart {
2623 ignore_newlines: true,
2624 ignore_brackets: false,
2625 },
2626 window,
2627 cx,
2628 );
2629 });
2630 cx.assert_editor_state("ˇwith a space");
2631 cx.update_editor(|editor, window, cx| {
2632 editor.delete_to_previous_word_start(
2633 &DeleteToPreviousWordStart {
2634 ignore_newlines: true,
2635 ignore_brackets: false,
2636 },
2637 window,
2638 cx,
2639 );
2640 });
2641 cx.assert_editor_state("ˇwith a space");
2642 cx.update_editor(|editor, window, cx| {
2643 editor.delete_to_next_word_end(
2644 &DeleteToNextWordEnd {
2645 ignore_newlines: true,
2646 ignore_brackets: false,
2647 },
2648 window,
2649 cx,
2650 );
2651 });
2652 // Same happens in the other direction.
2653 cx.assert_editor_state("ˇ a space");
2654 cx.update_editor(|editor, window, cx| {
2655 editor.delete_to_next_word_end(
2656 &DeleteToNextWordEnd {
2657 ignore_newlines: true,
2658 ignore_brackets: false,
2659 },
2660 window,
2661 cx,
2662 );
2663 });
2664 cx.assert_editor_state("ˇ space");
2665 cx.update_editor(|editor, window, cx| {
2666 editor.delete_to_next_word_end(
2667 &DeleteToNextWordEnd {
2668 ignore_newlines: true,
2669 ignore_brackets: false,
2670 },
2671 window,
2672 cx,
2673 );
2674 });
2675 cx.assert_editor_state("ˇ");
2676 cx.update_editor(|editor, window, cx| {
2677 editor.delete_to_next_word_end(
2678 &DeleteToNextWordEnd {
2679 ignore_newlines: true,
2680 ignore_brackets: false,
2681 },
2682 window,
2683 cx,
2684 );
2685 });
2686 cx.assert_editor_state("ˇ");
2687 cx.update_editor(|editor, window, cx| {
2688 editor.delete_to_previous_word_start(
2689 &DeleteToPreviousWordStart {
2690 ignore_newlines: true,
2691 ignore_brackets: false,
2692 },
2693 window,
2694 cx,
2695 );
2696 });
2697 cx.assert_editor_state("ˇ");
2698}
2699
2700#[gpui::test]
2701async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2702 init_test(cx, |_| {});
2703
2704 let language = Arc::new(
2705 Language::new(
2706 LanguageConfig {
2707 brackets: BracketPairConfig {
2708 pairs: vec![
2709 BracketPair {
2710 start: "\"".to_string(),
2711 end: "\"".to_string(),
2712 close: true,
2713 surround: true,
2714 newline: false,
2715 },
2716 BracketPair {
2717 start: "(".to_string(),
2718 end: ")".to_string(),
2719 close: true,
2720 surround: true,
2721 newline: true,
2722 },
2723 ],
2724 ..BracketPairConfig::default()
2725 },
2726 ..LanguageConfig::default()
2727 },
2728 Some(tree_sitter_rust::LANGUAGE.into()),
2729 )
2730 .with_brackets_query(
2731 r#"
2732 ("(" @open ")" @close)
2733 ("\"" @open "\"" @close)
2734 "#,
2735 )
2736 .unwrap(),
2737 );
2738
2739 let mut cx = EditorTestContext::new(cx).await;
2740 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2741
2742 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2743 cx.update_editor(|editor, window, cx| {
2744 editor.delete_to_previous_word_start(
2745 &DeleteToPreviousWordStart {
2746 ignore_newlines: true,
2747 ignore_brackets: false,
2748 },
2749 window,
2750 cx,
2751 );
2752 });
2753 // Deletion stops before brackets if asked to not ignore them.
2754 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2755 cx.update_editor(|editor, window, cx| {
2756 editor.delete_to_previous_word_start(
2757 &DeleteToPreviousWordStart {
2758 ignore_newlines: true,
2759 ignore_brackets: false,
2760 },
2761 window,
2762 cx,
2763 );
2764 });
2765 // Deletion has to remove a single bracket and then stop again.
2766 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2767
2768 cx.update_editor(|editor, window, cx| {
2769 editor.delete_to_previous_word_start(
2770 &DeleteToPreviousWordStart {
2771 ignore_newlines: true,
2772 ignore_brackets: false,
2773 },
2774 window,
2775 cx,
2776 );
2777 });
2778 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2779
2780 cx.update_editor(|editor, window, cx| {
2781 editor.delete_to_previous_word_start(
2782 &DeleteToPreviousWordStart {
2783 ignore_newlines: true,
2784 ignore_brackets: false,
2785 },
2786 window,
2787 cx,
2788 );
2789 });
2790 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2791
2792 cx.update_editor(|editor, window, cx| {
2793 editor.delete_to_previous_word_start(
2794 &DeleteToPreviousWordStart {
2795 ignore_newlines: true,
2796 ignore_brackets: false,
2797 },
2798 window,
2799 cx,
2800 );
2801 });
2802 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2803
2804 cx.update_editor(|editor, window, cx| {
2805 editor.delete_to_next_word_end(
2806 &DeleteToNextWordEnd {
2807 ignore_newlines: true,
2808 ignore_brackets: false,
2809 },
2810 window,
2811 cx,
2812 );
2813 });
2814 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2815 cx.assert_editor_state(r#"ˇ");"#);
2816
2817 cx.update_editor(|editor, window, cx| {
2818 editor.delete_to_next_word_end(
2819 &DeleteToNextWordEnd {
2820 ignore_newlines: true,
2821 ignore_brackets: false,
2822 },
2823 window,
2824 cx,
2825 );
2826 });
2827 cx.assert_editor_state(r#"ˇ"#);
2828
2829 cx.update_editor(|editor, window, cx| {
2830 editor.delete_to_next_word_end(
2831 &DeleteToNextWordEnd {
2832 ignore_newlines: true,
2833 ignore_brackets: false,
2834 },
2835 window,
2836 cx,
2837 );
2838 });
2839 cx.assert_editor_state(r#"ˇ"#);
2840
2841 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2842 cx.update_editor(|editor, window, cx| {
2843 editor.delete_to_previous_word_start(
2844 &DeleteToPreviousWordStart {
2845 ignore_newlines: true,
2846 ignore_brackets: true,
2847 },
2848 window,
2849 cx,
2850 );
2851 });
2852 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2853}
2854
2855#[gpui::test]
2856fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2857 init_test(cx, |_| {});
2858
2859 let editor = cx.add_window(|window, cx| {
2860 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2861 build_editor(buffer, window, cx)
2862 });
2863 let del_to_prev_word_start = DeleteToPreviousWordStart {
2864 ignore_newlines: false,
2865 ignore_brackets: false,
2866 };
2867 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2868 ignore_newlines: true,
2869 ignore_brackets: false,
2870 };
2871
2872 _ = editor.update(cx, |editor, window, cx| {
2873 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2874 s.select_display_ranges([
2875 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2876 ])
2877 });
2878 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2879 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2880 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2881 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2882 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2883 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2884 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2885 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2886 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2887 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2888 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2889 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2890 });
2891}
2892
2893#[gpui::test]
2894fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2895 init_test(cx, |_| {});
2896
2897 let editor = cx.add_window(|window, cx| {
2898 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2899 build_editor(buffer, window, cx)
2900 });
2901 let del_to_next_word_end = DeleteToNextWordEnd {
2902 ignore_newlines: false,
2903 ignore_brackets: false,
2904 };
2905 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2906 ignore_newlines: true,
2907 ignore_brackets: false,
2908 };
2909
2910 _ = editor.update(cx, |editor, window, cx| {
2911 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2912 s.select_display_ranges([
2913 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2914 ])
2915 });
2916 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2917 assert_eq!(
2918 editor.buffer.read(cx).read(cx).text(),
2919 "one\n two\nthree\n four"
2920 );
2921 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2922 assert_eq!(
2923 editor.buffer.read(cx).read(cx).text(),
2924 "\n two\nthree\n four"
2925 );
2926 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2927 assert_eq!(
2928 editor.buffer.read(cx).read(cx).text(),
2929 "two\nthree\n four"
2930 );
2931 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2932 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2933 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2934 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2935 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2936 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
2937 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2938 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2939 });
2940}
2941
2942#[gpui::test]
2943fn test_newline(cx: &mut TestAppContext) {
2944 init_test(cx, |_| {});
2945
2946 let editor = cx.add_window(|window, cx| {
2947 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2948 build_editor(buffer, window, cx)
2949 });
2950
2951 _ = editor.update(cx, |editor, window, cx| {
2952 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2953 s.select_display_ranges([
2954 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2955 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2956 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2957 ])
2958 });
2959
2960 editor.newline(&Newline, window, cx);
2961 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2962 });
2963}
2964
2965#[gpui::test]
2966fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2967 init_test(cx, |_| {});
2968
2969 let editor = cx.add_window(|window, cx| {
2970 let buffer = MultiBuffer::build_simple(
2971 "
2972 a
2973 b(
2974 X
2975 )
2976 c(
2977 X
2978 )
2979 "
2980 .unindent()
2981 .as_str(),
2982 cx,
2983 );
2984 let mut editor = build_editor(buffer, window, cx);
2985 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2986 s.select_ranges([
2987 Point::new(2, 4)..Point::new(2, 5),
2988 Point::new(5, 4)..Point::new(5, 5),
2989 ])
2990 });
2991 editor
2992 });
2993
2994 _ = editor.update(cx, |editor, window, cx| {
2995 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2996 editor.buffer.update(cx, |buffer, cx| {
2997 buffer.edit(
2998 [
2999 (Point::new(1, 2)..Point::new(3, 0), ""),
3000 (Point::new(4, 2)..Point::new(6, 0), ""),
3001 ],
3002 None,
3003 cx,
3004 );
3005 assert_eq!(
3006 buffer.read(cx).text(),
3007 "
3008 a
3009 b()
3010 c()
3011 "
3012 .unindent()
3013 );
3014 });
3015 assert_eq!(
3016 editor.selections.ranges(cx),
3017 &[
3018 Point::new(1, 2)..Point::new(1, 2),
3019 Point::new(2, 2)..Point::new(2, 2),
3020 ],
3021 );
3022
3023 editor.newline(&Newline, window, cx);
3024 assert_eq!(
3025 editor.text(cx),
3026 "
3027 a
3028 b(
3029 )
3030 c(
3031 )
3032 "
3033 .unindent()
3034 );
3035
3036 // The selections are moved after the inserted newlines
3037 assert_eq!(
3038 editor.selections.ranges(cx),
3039 &[
3040 Point::new(2, 0)..Point::new(2, 0),
3041 Point::new(4, 0)..Point::new(4, 0),
3042 ],
3043 );
3044 });
3045}
3046
3047#[gpui::test]
3048async fn test_newline_above(cx: &mut TestAppContext) {
3049 init_test(cx, |settings| {
3050 settings.defaults.tab_size = NonZeroU32::new(4)
3051 });
3052
3053 let language = Arc::new(
3054 Language::new(
3055 LanguageConfig::default(),
3056 Some(tree_sitter_rust::LANGUAGE.into()),
3057 )
3058 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3059 .unwrap(),
3060 );
3061
3062 let mut cx = EditorTestContext::new(cx).await;
3063 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3064 cx.set_state(indoc! {"
3065 const a: ˇA = (
3066 (ˇ
3067 «const_functionˇ»(ˇ),
3068 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3069 )ˇ
3070 ˇ);ˇ
3071 "});
3072
3073 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3074 cx.assert_editor_state(indoc! {"
3075 ˇ
3076 const a: A = (
3077 ˇ
3078 (
3079 ˇ
3080 ˇ
3081 const_function(),
3082 ˇ
3083 ˇ
3084 ˇ
3085 ˇ
3086 something_else,
3087 ˇ
3088 )
3089 ˇ
3090 ˇ
3091 );
3092 "});
3093}
3094
3095#[gpui::test]
3096async fn test_newline_below(cx: &mut TestAppContext) {
3097 init_test(cx, |settings| {
3098 settings.defaults.tab_size = NonZeroU32::new(4)
3099 });
3100
3101 let language = Arc::new(
3102 Language::new(
3103 LanguageConfig::default(),
3104 Some(tree_sitter_rust::LANGUAGE.into()),
3105 )
3106 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3107 .unwrap(),
3108 );
3109
3110 let mut cx = EditorTestContext::new(cx).await;
3111 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3112 cx.set_state(indoc! {"
3113 const a: ˇA = (
3114 (ˇ
3115 «const_functionˇ»(ˇ),
3116 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3117 )ˇ
3118 ˇ);ˇ
3119 "});
3120
3121 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3122 cx.assert_editor_state(indoc! {"
3123 const a: A = (
3124 ˇ
3125 (
3126 ˇ
3127 const_function(),
3128 ˇ
3129 ˇ
3130 something_else,
3131 ˇ
3132 ˇ
3133 ˇ
3134 ˇ
3135 )
3136 ˇ
3137 );
3138 ˇ
3139 ˇ
3140 "});
3141}
3142
3143#[gpui::test]
3144async fn test_newline_comments(cx: &mut TestAppContext) {
3145 init_test(cx, |settings| {
3146 settings.defaults.tab_size = NonZeroU32::new(4)
3147 });
3148
3149 let language = Arc::new(Language::new(
3150 LanguageConfig {
3151 line_comments: vec!["// ".into()],
3152 ..LanguageConfig::default()
3153 },
3154 None,
3155 ));
3156 {
3157 let mut cx = EditorTestContext::new(cx).await;
3158 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3159 cx.set_state(indoc! {"
3160 // Fooˇ
3161 "});
3162
3163 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3164 cx.assert_editor_state(indoc! {"
3165 // Foo
3166 // ˇ
3167 "});
3168 // Ensure that we add comment prefix when existing line contains space
3169 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3170 cx.assert_editor_state(
3171 indoc! {"
3172 // Foo
3173 //s
3174 // ˇ
3175 "}
3176 .replace("s", " ") // s is used as space placeholder to prevent format on save
3177 .as_str(),
3178 );
3179 // Ensure that we add comment prefix when existing line does not contain space
3180 cx.set_state(indoc! {"
3181 // Foo
3182 //ˇ
3183 "});
3184 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3185 cx.assert_editor_state(indoc! {"
3186 // Foo
3187 //
3188 // ˇ
3189 "});
3190 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3191 cx.set_state(indoc! {"
3192 ˇ// Foo
3193 "});
3194 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3195 cx.assert_editor_state(indoc! {"
3196
3197 ˇ// Foo
3198 "});
3199 }
3200 // Ensure that comment continuations can be disabled.
3201 update_test_language_settings(cx, |settings| {
3202 settings.defaults.extend_comment_on_newline = Some(false);
3203 });
3204 let mut cx = EditorTestContext::new(cx).await;
3205 cx.set_state(indoc! {"
3206 // Fooˇ
3207 "});
3208 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3209 cx.assert_editor_state(indoc! {"
3210 // Foo
3211 ˇ
3212 "});
3213}
3214
3215#[gpui::test]
3216async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3217 init_test(cx, |settings| {
3218 settings.defaults.tab_size = NonZeroU32::new(4)
3219 });
3220
3221 let language = Arc::new(Language::new(
3222 LanguageConfig {
3223 line_comments: vec!["// ".into(), "/// ".into()],
3224 ..LanguageConfig::default()
3225 },
3226 None,
3227 ));
3228 {
3229 let mut cx = EditorTestContext::new(cx).await;
3230 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3231 cx.set_state(indoc! {"
3232 //ˇ
3233 "});
3234 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3235 cx.assert_editor_state(indoc! {"
3236 //
3237 // ˇ
3238 "});
3239
3240 cx.set_state(indoc! {"
3241 ///ˇ
3242 "});
3243 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3244 cx.assert_editor_state(indoc! {"
3245 ///
3246 /// ˇ
3247 "});
3248 }
3249}
3250
3251#[gpui::test]
3252async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3253 init_test(cx, |settings| {
3254 settings.defaults.tab_size = NonZeroU32::new(4)
3255 });
3256
3257 let language = Arc::new(
3258 Language::new(
3259 LanguageConfig {
3260 documentation_comment: Some(language::BlockCommentConfig {
3261 start: "/**".into(),
3262 end: "*/".into(),
3263 prefix: "* ".into(),
3264 tab_size: 1,
3265 }),
3266
3267 ..LanguageConfig::default()
3268 },
3269 Some(tree_sitter_rust::LANGUAGE.into()),
3270 )
3271 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3272 .unwrap(),
3273 );
3274
3275 {
3276 let mut cx = EditorTestContext::new(cx).await;
3277 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3278 cx.set_state(indoc! {"
3279 /**ˇ
3280 "});
3281
3282 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3283 cx.assert_editor_state(indoc! {"
3284 /**
3285 * ˇ
3286 "});
3287 // Ensure that if cursor is before the comment start,
3288 // we do not actually insert a comment prefix.
3289 cx.set_state(indoc! {"
3290 ˇ/**
3291 "});
3292 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3293 cx.assert_editor_state(indoc! {"
3294
3295 ˇ/**
3296 "});
3297 // Ensure that if cursor is between it doesn't add comment prefix.
3298 cx.set_state(indoc! {"
3299 /*ˇ*
3300 "});
3301 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3302 cx.assert_editor_state(indoc! {"
3303 /*
3304 ˇ*
3305 "});
3306 // Ensure that if suffix exists on same line after cursor it adds new line.
3307 cx.set_state(indoc! {"
3308 /**ˇ*/
3309 "});
3310 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3311 cx.assert_editor_state(indoc! {"
3312 /**
3313 * ˇ
3314 */
3315 "});
3316 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3317 cx.set_state(indoc! {"
3318 /**ˇ */
3319 "});
3320 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3321 cx.assert_editor_state(indoc! {"
3322 /**
3323 * ˇ
3324 */
3325 "});
3326 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3327 cx.set_state(indoc! {"
3328 /** ˇ*/
3329 "});
3330 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3331 cx.assert_editor_state(
3332 indoc! {"
3333 /**s
3334 * ˇ
3335 */
3336 "}
3337 .replace("s", " ") // s is used as space placeholder to prevent format on save
3338 .as_str(),
3339 );
3340 // Ensure that delimiter space is preserved when newline on already
3341 // spaced delimiter.
3342 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3343 cx.assert_editor_state(
3344 indoc! {"
3345 /**s
3346 *s
3347 * ˇ
3348 */
3349 "}
3350 .replace("s", " ") // s is used as space placeholder to prevent format on save
3351 .as_str(),
3352 );
3353 // Ensure that delimiter space is preserved when space is not
3354 // on existing delimiter.
3355 cx.set_state(indoc! {"
3356 /**
3357 *ˇ
3358 */
3359 "});
3360 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3361 cx.assert_editor_state(indoc! {"
3362 /**
3363 *
3364 * ˇ
3365 */
3366 "});
3367 // Ensure that if suffix exists on same line after cursor it
3368 // doesn't add extra new line if prefix is not on same line.
3369 cx.set_state(indoc! {"
3370 /**
3371 ˇ*/
3372 "});
3373 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3374 cx.assert_editor_state(indoc! {"
3375 /**
3376
3377 ˇ*/
3378 "});
3379 // Ensure that it detects suffix after existing prefix.
3380 cx.set_state(indoc! {"
3381 /**ˇ/
3382 "});
3383 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3384 cx.assert_editor_state(indoc! {"
3385 /**
3386 ˇ/
3387 "});
3388 // Ensure that if suffix exists on same line before
3389 // cursor it does not add comment prefix.
3390 cx.set_state(indoc! {"
3391 /** */ˇ
3392 "});
3393 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3394 cx.assert_editor_state(indoc! {"
3395 /** */
3396 ˇ
3397 "});
3398 // Ensure that if suffix exists on same line before
3399 // cursor it does not add comment prefix.
3400 cx.set_state(indoc! {"
3401 /**
3402 *
3403 */ˇ
3404 "});
3405 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3406 cx.assert_editor_state(indoc! {"
3407 /**
3408 *
3409 */
3410 ˇ
3411 "});
3412
3413 // Ensure that inline comment followed by code
3414 // doesn't add comment prefix on newline
3415 cx.set_state(indoc! {"
3416 /** */ textˇ
3417 "});
3418 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3419 cx.assert_editor_state(indoc! {"
3420 /** */ text
3421 ˇ
3422 "});
3423
3424 // Ensure that text after comment end tag
3425 // doesn't add comment prefix on newline
3426 cx.set_state(indoc! {"
3427 /**
3428 *
3429 */ˇtext
3430 "});
3431 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3432 cx.assert_editor_state(indoc! {"
3433 /**
3434 *
3435 */
3436 ˇtext
3437 "});
3438
3439 // Ensure if not comment block it doesn't
3440 // add comment prefix on newline
3441 cx.set_state(indoc! {"
3442 * textˇ
3443 "});
3444 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3445 cx.assert_editor_state(indoc! {"
3446 * text
3447 ˇ
3448 "});
3449 }
3450 // Ensure that comment continuations can be disabled.
3451 update_test_language_settings(cx, |settings| {
3452 settings.defaults.extend_comment_on_newline = Some(false);
3453 });
3454 let mut cx = EditorTestContext::new(cx).await;
3455 cx.set_state(indoc! {"
3456 /**ˇ
3457 "});
3458 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3459 cx.assert_editor_state(indoc! {"
3460 /**
3461 ˇ
3462 "});
3463}
3464
3465#[gpui::test]
3466async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3467 init_test(cx, |settings| {
3468 settings.defaults.tab_size = NonZeroU32::new(4)
3469 });
3470
3471 let lua_language = Arc::new(Language::new(
3472 LanguageConfig {
3473 line_comments: vec!["--".into()],
3474 block_comment: Some(language::BlockCommentConfig {
3475 start: "--[[".into(),
3476 prefix: "".into(),
3477 end: "]]".into(),
3478 tab_size: 0,
3479 }),
3480 ..LanguageConfig::default()
3481 },
3482 None,
3483 ));
3484
3485 let mut cx = EditorTestContext::new(cx).await;
3486 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3487
3488 // Line with line comment should extend
3489 cx.set_state(indoc! {"
3490 --ˇ
3491 "});
3492 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3493 cx.assert_editor_state(indoc! {"
3494 --
3495 --ˇ
3496 "});
3497
3498 // Line with block comment that matches line comment should not extend
3499 cx.set_state(indoc! {"
3500 --[[ˇ
3501 "});
3502 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3503 cx.assert_editor_state(indoc! {"
3504 --[[
3505 ˇ
3506 "});
3507}
3508
3509#[gpui::test]
3510fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3511 init_test(cx, |_| {});
3512
3513 let editor = cx.add_window(|window, cx| {
3514 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3515 let mut editor = build_editor(buffer, window, cx);
3516 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3517 s.select_ranges([3..4, 11..12, 19..20])
3518 });
3519 editor
3520 });
3521
3522 _ = editor.update(cx, |editor, window, cx| {
3523 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3524 editor.buffer.update(cx, |buffer, cx| {
3525 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3526 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3527 });
3528 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3529
3530 editor.insert("Z", window, cx);
3531 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3532
3533 // The selections are moved after the inserted characters
3534 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3535 });
3536}
3537
3538#[gpui::test]
3539async fn test_tab(cx: &mut TestAppContext) {
3540 init_test(cx, |settings| {
3541 settings.defaults.tab_size = NonZeroU32::new(3)
3542 });
3543
3544 let mut cx = EditorTestContext::new(cx).await;
3545 cx.set_state(indoc! {"
3546 ˇabˇc
3547 ˇ🏀ˇ🏀ˇefg
3548 dˇ
3549 "});
3550 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3551 cx.assert_editor_state(indoc! {"
3552 ˇab ˇc
3553 ˇ🏀 ˇ🏀 ˇefg
3554 d ˇ
3555 "});
3556
3557 cx.set_state(indoc! {"
3558 a
3559 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3560 "});
3561 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3562 cx.assert_editor_state(indoc! {"
3563 a
3564 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3565 "});
3566}
3567
3568#[gpui::test]
3569async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3570 init_test(cx, |_| {});
3571
3572 let mut cx = EditorTestContext::new(cx).await;
3573 let language = Arc::new(
3574 Language::new(
3575 LanguageConfig::default(),
3576 Some(tree_sitter_rust::LANGUAGE.into()),
3577 )
3578 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3579 .unwrap(),
3580 );
3581 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3582
3583 // test when all cursors are not at suggested indent
3584 // then simply move to their suggested indent location
3585 cx.set_state(indoc! {"
3586 const a: B = (
3587 c(
3588 ˇ
3589 ˇ )
3590 );
3591 "});
3592 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3593 cx.assert_editor_state(indoc! {"
3594 const a: B = (
3595 c(
3596 ˇ
3597 ˇ)
3598 );
3599 "});
3600
3601 // test cursor already at suggested indent not moving when
3602 // other cursors are yet to reach their suggested indents
3603 cx.set_state(indoc! {"
3604 ˇ
3605 const a: B = (
3606 c(
3607 d(
3608 ˇ
3609 )
3610 ˇ
3611 ˇ )
3612 );
3613 "});
3614 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3615 cx.assert_editor_state(indoc! {"
3616 ˇ
3617 const a: B = (
3618 c(
3619 d(
3620 ˇ
3621 )
3622 ˇ
3623 ˇ)
3624 );
3625 "});
3626 // test when all cursors are at suggested indent then tab is inserted
3627 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3628 cx.assert_editor_state(indoc! {"
3629 ˇ
3630 const a: B = (
3631 c(
3632 d(
3633 ˇ
3634 )
3635 ˇ
3636 ˇ)
3637 );
3638 "});
3639
3640 // test when current indent is less than suggested indent,
3641 // we adjust line to match suggested indent and move cursor to it
3642 //
3643 // when no other cursor is at word boundary, all of them should move
3644 cx.set_state(indoc! {"
3645 const a: B = (
3646 c(
3647 d(
3648 ˇ
3649 ˇ )
3650 ˇ )
3651 );
3652 "});
3653 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3654 cx.assert_editor_state(indoc! {"
3655 const a: B = (
3656 c(
3657 d(
3658 ˇ
3659 ˇ)
3660 ˇ)
3661 );
3662 "});
3663
3664 // test when current indent is less than suggested indent,
3665 // we adjust line to match suggested indent and move cursor to it
3666 //
3667 // when some other cursor is at word boundary, it should not move
3668 cx.set_state(indoc! {"
3669 const a: B = (
3670 c(
3671 d(
3672 ˇ
3673 ˇ )
3674 ˇ)
3675 );
3676 "});
3677 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3678 cx.assert_editor_state(indoc! {"
3679 const a: B = (
3680 c(
3681 d(
3682 ˇ
3683 ˇ)
3684 ˇ)
3685 );
3686 "});
3687
3688 // test when current indent is more than suggested indent,
3689 // we just move cursor to current indent instead of suggested indent
3690 //
3691 // when no other cursor is at word boundary, all of them should move
3692 cx.set_state(indoc! {"
3693 const a: B = (
3694 c(
3695 d(
3696 ˇ
3697 ˇ )
3698 ˇ )
3699 );
3700 "});
3701 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3702 cx.assert_editor_state(indoc! {"
3703 const a: B = (
3704 c(
3705 d(
3706 ˇ
3707 ˇ)
3708 ˇ)
3709 );
3710 "});
3711 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3712 cx.assert_editor_state(indoc! {"
3713 const a: B = (
3714 c(
3715 d(
3716 ˇ
3717 ˇ)
3718 ˇ)
3719 );
3720 "});
3721
3722 // test when current indent is more than suggested indent,
3723 // we just move cursor to current indent instead of suggested indent
3724 //
3725 // when some other cursor is at word boundary, it doesn't move
3726 cx.set_state(indoc! {"
3727 const a: B = (
3728 c(
3729 d(
3730 ˇ
3731 ˇ )
3732 ˇ)
3733 );
3734 "});
3735 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3736 cx.assert_editor_state(indoc! {"
3737 const a: B = (
3738 c(
3739 d(
3740 ˇ
3741 ˇ)
3742 ˇ)
3743 );
3744 "});
3745
3746 // handle auto-indent when there are multiple cursors on the same line
3747 cx.set_state(indoc! {"
3748 const a: B = (
3749 c(
3750 ˇ ˇ
3751 ˇ )
3752 );
3753 "});
3754 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3755 cx.assert_editor_state(indoc! {"
3756 const a: B = (
3757 c(
3758 ˇ
3759 ˇ)
3760 );
3761 "});
3762}
3763
3764#[gpui::test]
3765async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3766 init_test(cx, |settings| {
3767 settings.defaults.tab_size = NonZeroU32::new(3)
3768 });
3769
3770 let mut cx = EditorTestContext::new(cx).await;
3771 cx.set_state(indoc! {"
3772 ˇ
3773 \t ˇ
3774 \t ˇ
3775 \t ˇ
3776 \t \t\t \t \t\t \t\t \t \t ˇ
3777 "});
3778
3779 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3780 cx.assert_editor_state(indoc! {"
3781 ˇ
3782 \t ˇ
3783 \t ˇ
3784 \t ˇ
3785 \t \t\t \t \t\t \t\t \t \t ˇ
3786 "});
3787}
3788
3789#[gpui::test]
3790async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3791 init_test(cx, |settings| {
3792 settings.defaults.tab_size = NonZeroU32::new(4)
3793 });
3794
3795 let language = Arc::new(
3796 Language::new(
3797 LanguageConfig::default(),
3798 Some(tree_sitter_rust::LANGUAGE.into()),
3799 )
3800 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3801 .unwrap(),
3802 );
3803
3804 let mut cx = EditorTestContext::new(cx).await;
3805 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3806 cx.set_state(indoc! {"
3807 fn a() {
3808 if b {
3809 \t ˇc
3810 }
3811 }
3812 "});
3813
3814 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3815 cx.assert_editor_state(indoc! {"
3816 fn a() {
3817 if b {
3818 ˇc
3819 }
3820 }
3821 "});
3822}
3823
3824#[gpui::test]
3825async fn test_indent_outdent(cx: &mut TestAppContext) {
3826 init_test(cx, |settings| {
3827 settings.defaults.tab_size = NonZeroU32::new(4);
3828 });
3829
3830 let mut cx = EditorTestContext::new(cx).await;
3831
3832 cx.set_state(indoc! {"
3833 «oneˇ» «twoˇ»
3834 three
3835 four
3836 "});
3837 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3838 cx.assert_editor_state(indoc! {"
3839 «oneˇ» «twoˇ»
3840 three
3841 four
3842 "});
3843
3844 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3845 cx.assert_editor_state(indoc! {"
3846 «oneˇ» «twoˇ»
3847 three
3848 four
3849 "});
3850
3851 // select across line ending
3852 cx.set_state(indoc! {"
3853 one two
3854 t«hree
3855 ˇ» four
3856 "});
3857 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3858 cx.assert_editor_state(indoc! {"
3859 one two
3860 t«hree
3861 ˇ» four
3862 "});
3863
3864 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3865 cx.assert_editor_state(indoc! {"
3866 one two
3867 t«hree
3868 ˇ» four
3869 "});
3870
3871 // Ensure that indenting/outdenting works when the cursor is at column 0.
3872 cx.set_state(indoc! {"
3873 one two
3874 ˇthree
3875 four
3876 "});
3877 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3878 cx.assert_editor_state(indoc! {"
3879 one two
3880 ˇthree
3881 four
3882 "});
3883
3884 cx.set_state(indoc! {"
3885 one two
3886 ˇ three
3887 four
3888 "});
3889 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3890 cx.assert_editor_state(indoc! {"
3891 one two
3892 ˇthree
3893 four
3894 "});
3895}
3896
3897#[gpui::test]
3898async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3899 // This is a regression test for issue #33761
3900 init_test(cx, |_| {});
3901
3902 let mut cx = EditorTestContext::new(cx).await;
3903 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3904 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3905
3906 cx.set_state(
3907 r#"ˇ# ingress:
3908ˇ# api:
3909ˇ# enabled: false
3910ˇ# pathType: Prefix
3911ˇ# console:
3912ˇ# enabled: false
3913ˇ# pathType: Prefix
3914"#,
3915 );
3916
3917 // Press tab to indent all lines
3918 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3919
3920 cx.assert_editor_state(
3921 r#" ˇ# ingress:
3922 ˇ# api:
3923 ˇ# enabled: false
3924 ˇ# pathType: Prefix
3925 ˇ# console:
3926 ˇ# enabled: false
3927 ˇ# pathType: Prefix
3928"#,
3929 );
3930}
3931
3932#[gpui::test]
3933async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3934 // This is a test to make sure our fix for issue #33761 didn't break anything
3935 init_test(cx, |_| {});
3936
3937 let mut cx = EditorTestContext::new(cx).await;
3938 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3939 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3940
3941 cx.set_state(
3942 r#"ˇingress:
3943ˇ api:
3944ˇ enabled: false
3945ˇ pathType: Prefix
3946"#,
3947 );
3948
3949 // Press tab to indent all lines
3950 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3951
3952 cx.assert_editor_state(
3953 r#"ˇingress:
3954 ˇapi:
3955 ˇenabled: false
3956 ˇpathType: Prefix
3957"#,
3958 );
3959}
3960
3961#[gpui::test]
3962async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3963 init_test(cx, |settings| {
3964 settings.defaults.hard_tabs = Some(true);
3965 });
3966
3967 let mut cx = EditorTestContext::new(cx).await;
3968
3969 // select two ranges on one line
3970 cx.set_state(indoc! {"
3971 «oneˇ» «twoˇ»
3972 three
3973 four
3974 "});
3975 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3976 cx.assert_editor_state(indoc! {"
3977 \t«oneˇ» «twoˇ»
3978 three
3979 four
3980 "});
3981 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3982 cx.assert_editor_state(indoc! {"
3983 \t\t«oneˇ» «twoˇ»
3984 three
3985 four
3986 "});
3987 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3988 cx.assert_editor_state(indoc! {"
3989 \t«oneˇ» «twoˇ»
3990 three
3991 four
3992 "});
3993 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3994 cx.assert_editor_state(indoc! {"
3995 «oneˇ» «twoˇ»
3996 three
3997 four
3998 "});
3999
4000 // select across a line ending
4001 cx.set_state(indoc! {"
4002 one two
4003 t«hree
4004 ˇ»four
4005 "});
4006 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4007 cx.assert_editor_state(indoc! {"
4008 one two
4009 \tt«hree
4010 ˇ»four
4011 "});
4012 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4013 cx.assert_editor_state(indoc! {"
4014 one two
4015 \t\tt«hree
4016 ˇ»four
4017 "});
4018 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4019 cx.assert_editor_state(indoc! {"
4020 one two
4021 \tt«hree
4022 ˇ»four
4023 "});
4024 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4025 cx.assert_editor_state(indoc! {"
4026 one two
4027 t«hree
4028 ˇ»four
4029 "});
4030
4031 // Ensure that indenting/outdenting works when the cursor is at column 0.
4032 cx.set_state(indoc! {"
4033 one two
4034 ˇthree
4035 four
4036 "});
4037 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4038 cx.assert_editor_state(indoc! {"
4039 one two
4040 ˇthree
4041 four
4042 "});
4043 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4044 cx.assert_editor_state(indoc! {"
4045 one two
4046 \tˇthree
4047 four
4048 "});
4049 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4050 cx.assert_editor_state(indoc! {"
4051 one two
4052 ˇthree
4053 four
4054 "});
4055}
4056
4057#[gpui::test]
4058fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4059 init_test(cx, |settings| {
4060 settings.languages.0.extend([
4061 (
4062 "TOML".into(),
4063 LanguageSettingsContent {
4064 tab_size: NonZeroU32::new(2),
4065 ..Default::default()
4066 },
4067 ),
4068 (
4069 "Rust".into(),
4070 LanguageSettingsContent {
4071 tab_size: NonZeroU32::new(4),
4072 ..Default::default()
4073 },
4074 ),
4075 ]);
4076 });
4077
4078 let toml_language = Arc::new(Language::new(
4079 LanguageConfig {
4080 name: "TOML".into(),
4081 ..Default::default()
4082 },
4083 None,
4084 ));
4085 let rust_language = Arc::new(Language::new(
4086 LanguageConfig {
4087 name: "Rust".into(),
4088 ..Default::default()
4089 },
4090 None,
4091 ));
4092
4093 let toml_buffer =
4094 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4095 let rust_buffer =
4096 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4097 let multibuffer = cx.new(|cx| {
4098 let mut multibuffer = MultiBuffer::new(ReadWrite);
4099 multibuffer.push_excerpts(
4100 toml_buffer.clone(),
4101 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4102 cx,
4103 );
4104 multibuffer.push_excerpts(
4105 rust_buffer.clone(),
4106 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4107 cx,
4108 );
4109 multibuffer
4110 });
4111
4112 cx.add_window(|window, cx| {
4113 let mut editor = build_editor(multibuffer, window, cx);
4114
4115 assert_eq!(
4116 editor.text(cx),
4117 indoc! {"
4118 a = 1
4119 b = 2
4120
4121 const c: usize = 3;
4122 "}
4123 );
4124
4125 select_ranges(
4126 &mut editor,
4127 indoc! {"
4128 «aˇ» = 1
4129 b = 2
4130
4131 «const c:ˇ» usize = 3;
4132 "},
4133 window,
4134 cx,
4135 );
4136
4137 editor.tab(&Tab, window, cx);
4138 assert_text_with_selections(
4139 &mut editor,
4140 indoc! {"
4141 «aˇ» = 1
4142 b = 2
4143
4144 «const c:ˇ» usize = 3;
4145 "},
4146 cx,
4147 );
4148 editor.backtab(&Backtab, window, cx);
4149 assert_text_with_selections(
4150 &mut editor,
4151 indoc! {"
4152 «aˇ» = 1
4153 b = 2
4154
4155 «const c:ˇ» usize = 3;
4156 "},
4157 cx,
4158 );
4159
4160 editor
4161 });
4162}
4163
4164#[gpui::test]
4165async fn test_backspace(cx: &mut TestAppContext) {
4166 init_test(cx, |_| {});
4167
4168 let mut cx = EditorTestContext::new(cx).await;
4169
4170 // Basic backspace
4171 cx.set_state(indoc! {"
4172 onˇe two three
4173 fou«rˇ» five six
4174 seven «ˇeight nine
4175 »ten
4176 "});
4177 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4178 cx.assert_editor_state(indoc! {"
4179 oˇe two three
4180 fouˇ five six
4181 seven ˇten
4182 "});
4183
4184 // Test backspace inside and around indents
4185 cx.set_state(indoc! {"
4186 zero
4187 ˇone
4188 ˇtwo
4189 ˇ ˇ ˇ three
4190 ˇ ˇ four
4191 "});
4192 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4193 cx.assert_editor_state(indoc! {"
4194 zero
4195 ˇone
4196 ˇtwo
4197 ˇ threeˇ four
4198 "});
4199}
4200
4201#[gpui::test]
4202async fn test_delete(cx: &mut TestAppContext) {
4203 init_test(cx, |_| {});
4204
4205 let mut cx = EditorTestContext::new(cx).await;
4206 cx.set_state(indoc! {"
4207 onˇe two three
4208 fou«rˇ» five six
4209 seven «ˇeight nine
4210 »ten
4211 "});
4212 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4213 cx.assert_editor_state(indoc! {"
4214 onˇ two three
4215 fouˇ five six
4216 seven ˇten
4217 "});
4218}
4219
4220#[gpui::test]
4221fn test_delete_line(cx: &mut TestAppContext) {
4222 init_test(cx, |_| {});
4223
4224 let editor = cx.add_window(|window, cx| {
4225 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4226 build_editor(buffer, window, cx)
4227 });
4228 _ = editor.update(cx, |editor, window, cx| {
4229 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4230 s.select_display_ranges([
4231 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4232 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4233 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4234 ])
4235 });
4236 editor.delete_line(&DeleteLine, window, cx);
4237 assert_eq!(editor.display_text(cx), "ghi");
4238 assert_eq!(
4239 editor.selections.display_ranges(cx),
4240 vec![
4241 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4242 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
4243 ]
4244 );
4245 });
4246
4247 let editor = cx.add_window(|window, cx| {
4248 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4249 build_editor(buffer, window, cx)
4250 });
4251 _ = editor.update(cx, |editor, window, cx| {
4252 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4253 s.select_display_ranges([
4254 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4255 ])
4256 });
4257 editor.delete_line(&DeleteLine, window, cx);
4258 assert_eq!(editor.display_text(cx), "ghi\n");
4259 assert_eq!(
4260 editor.selections.display_ranges(cx),
4261 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4262 );
4263 });
4264}
4265
4266#[gpui::test]
4267fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4268 init_test(cx, |_| {});
4269
4270 cx.add_window(|window, cx| {
4271 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4272 let mut editor = build_editor(buffer.clone(), window, cx);
4273 let buffer = buffer.read(cx).as_singleton().unwrap();
4274
4275 assert_eq!(
4276 editor.selections.ranges::<Point>(cx),
4277 &[Point::new(0, 0)..Point::new(0, 0)]
4278 );
4279
4280 // When on single line, replace newline at end by space
4281 editor.join_lines(&JoinLines, window, cx);
4282 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4283 assert_eq!(
4284 editor.selections.ranges::<Point>(cx),
4285 &[Point::new(0, 3)..Point::new(0, 3)]
4286 );
4287
4288 // When multiple lines are selected, remove newlines that are spanned by the selection
4289 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4290 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4291 });
4292 editor.join_lines(&JoinLines, window, cx);
4293 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4294 assert_eq!(
4295 editor.selections.ranges::<Point>(cx),
4296 &[Point::new(0, 11)..Point::new(0, 11)]
4297 );
4298
4299 // Undo should be transactional
4300 editor.undo(&Undo, window, cx);
4301 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4302 assert_eq!(
4303 editor.selections.ranges::<Point>(cx),
4304 &[Point::new(0, 5)..Point::new(2, 2)]
4305 );
4306
4307 // When joining an empty line don't insert a space
4308 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4309 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4310 });
4311 editor.join_lines(&JoinLines, window, cx);
4312 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4313 assert_eq!(
4314 editor.selections.ranges::<Point>(cx),
4315 [Point::new(2, 3)..Point::new(2, 3)]
4316 );
4317
4318 // We can remove trailing newlines
4319 editor.join_lines(&JoinLines, window, cx);
4320 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4321 assert_eq!(
4322 editor.selections.ranges::<Point>(cx),
4323 [Point::new(2, 3)..Point::new(2, 3)]
4324 );
4325
4326 // We don't blow up on the last line
4327 editor.join_lines(&JoinLines, window, cx);
4328 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4329 assert_eq!(
4330 editor.selections.ranges::<Point>(cx),
4331 [Point::new(2, 3)..Point::new(2, 3)]
4332 );
4333
4334 // reset to test indentation
4335 editor.buffer.update(cx, |buffer, cx| {
4336 buffer.edit(
4337 [
4338 (Point::new(1, 0)..Point::new(1, 2), " "),
4339 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4340 ],
4341 None,
4342 cx,
4343 )
4344 });
4345
4346 // We remove any leading spaces
4347 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4348 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4349 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4350 });
4351 editor.join_lines(&JoinLines, window, cx);
4352 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4353
4354 // We don't insert a space for a line containing only spaces
4355 editor.join_lines(&JoinLines, window, cx);
4356 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4357
4358 // We ignore any leading tabs
4359 editor.join_lines(&JoinLines, window, cx);
4360 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4361
4362 editor
4363 });
4364}
4365
4366#[gpui::test]
4367fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4368 init_test(cx, |_| {});
4369
4370 cx.add_window(|window, cx| {
4371 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4372 let mut editor = build_editor(buffer.clone(), window, cx);
4373 let buffer = buffer.read(cx).as_singleton().unwrap();
4374
4375 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4376 s.select_ranges([
4377 Point::new(0, 2)..Point::new(1, 1),
4378 Point::new(1, 2)..Point::new(1, 2),
4379 Point::new(3, 1)..Point::new(3, 2),
4380 ])
4381 });
4382
4383 editor.join_lines(&JoinLines, window, cx);
4384 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4385
4386 assert_eq!(
4387 editor.selections.ranges::<Point>(cx),
4388 [
4389 Point::new(0, 7)..Point::new(0, 7),
4390 Point::new(1, 3)..Point::new(1, 3)
4391 ]
4392 );
4393 editor
4394 });
4395}
4396
4397#[gpui::test]
4398async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4399 init_test(cx, |_| {});
4400
4401 let mut cx = EditorTestContext::new(cx).await;
4402
4403 let diff_base = r#"
4404 Line 0
4405 Line 1
4406 Line 2
4407 Line 3
4408 "#
4409 .unindent();
4410
4411 cx.set_state(
4412 &r#"
4413 ˇLine 0
4414 Line 1
4415 Line 2
4416 Line 3
4417 "#
4418 .unindent(),
4419 );
4420
4421 cx.set_head_text(&diff_base);
4422 executor.run_until_parked();
4423
4424 // Join lines
4425 cx.update_editor(|editor, window, cx| {
4426 editor.join_lines(&JoinLines, window, cx);
4427 });
4428 executor.run_until_parked();
4429
4430 cx.assert_editor_state(
4431 &r#"
4432 Line 0ˇ Line 1
4433 Line 2
4434 Line 3
4435 "#
4436 .unindent(),
4437 );
4438 // Join again
4439 cx.update_editor(|editor, window, cx| {
4440 editor.join_lines(&JoinLines, window, cx);
4441 });
4442 executor.run_until_parked();
4443
4444 cx.assert_editor_state(
4445 &r#"
4446 Line 0 Line 1ˇ Line 2
4447 Line 3
4448 "#
4449 .unindent(),
4450 );
4451}
4452
4453#[gpui::test]
4454async fn test_custom_newlines_cause_no_false_positive_diffs(
4455 executor: BackgroundExecutor,
4456 cx: &mut TestAppContext,
4457) {
4458 init_test(cx, |_| {});
4459 let mut cx = EditorTestContext::new(cx).await;
4460 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4461 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4462 executor.run_until_parked();
4463
4464 cx.update_editor(|editor, window, cx| {
4465 let snapshot = editor.snapshot(window, cx);
4466 assert_eq!(
4467 snapshot
4468 .buffer_snapshot
4469 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4470 .collect::<Vec<_>>(),
4471 Vec::new(),
4472 "Should not have any diffs for files with custom newlines"
4473 );
4474 });
4475}
4476
4477#[gpui::test]
4478async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4479 init_test(cx, |_| {});
4480
4481 let mut cx = EditorTestContext::new(cx).await;
4482
4483 // Test sort_lines_case_insensitive()
4484 cx.set_state(indoc! {"
4485 «z
4486 y
4487 x
4488 Z
4489 Y
4490 Xˇ»
4491 "});
4492 cx.update_editor(|e, window, cx| {
4493 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4494 });
4495 cx.assert_editor_state(indoc! {"
4496 «x
4497 X
4498 y
4499 Y
4500 z
4501 Zˇ»
4502 "});
4503
4504 // Test sort_lines_by_length()
4505 //
4506 // Demonstrates:
4507 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4508 // - sort is stable
4509 cx.set_state(indoc! {"
4510 «123
4511 æ
4512 12
4513 ∞
4514 1
4515 æˇ»
4516 "});
4517 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4518 cx.assert_editor_state(indoc! {"
4519 «æ
4520 ∞
4521 1
4522 æ
4523 12
4524 123ˇ»
4525 "});
4526
4527 // Test reverse_lines()
4528 cx.set_state(indoc! {"
4529 «5
4530 4
4531 3
4532 2
4533 1ˇ»
4534 "});
4535 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4536 cx.assert_editor_state(indoc! {"
4537 «1
4538 2
4539 3
4540 4
4541 5ˇ»
4542 "});
4543
4544 // Skip testing shuffle_line()
4545
4546 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4547 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4548
4549 // Don't manipulate when cursor is on single line, but expand the selection
4550 cx.set_state(indoc! {"
4551 ddˇdd
4552 ccc
4553 bb
4554 a
4555 "});
4556 cx.update_editor(|e, window, cx| {
4557 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4558 });
4559 cx.assert_editor_state(indoc! {"
4560 «ddddˇ»
4561 ccc
4562 bb
4563 a
4564 "});
4565
4566 // Basic manipulate case
4567 // Start selection moves to column 0
4568 // End of selection shrinks to fit shorter line
4569 cx.set_state(indoc! {"
4570 dd«d
4571 ccc
4572 bb
4573 aaaaaˇ»
4574 "});
4575 cx.update_editor(|e, window, cx| {
4576 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4577 });
4578 cx.assert_editor_state(indoc! {"
4579 «aaaaa
4580 bb
4581 ccc
4582 dddˇ»
4583 "});
4584
4585 // Manipulate case with newlines
4586 cx.set_state(indoc! {"
4587 dd«d
4588 ccc
4589
4590 bb
4591 aaaaa
4592
4593 ˇ»
4594 "});
4595 cx.update_editor(|e, window, cx| {
4596 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4597 });
4598 cx.assert_editor_state(indoc! {"
4599 «
4600
4601 aaaaa
4602 bb
4603 ccc
4604 dddˇ»
4605
4606 "});
4607
4608 // Adding new line
4609 cx.set_state(indoc! {"
4610 aa«a
4611 bbˇ»b
4612 "});
4613 cx.update_editor(|e, window, cx| {
4614 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4615 });
4616 cx.assert_editor_state(indoc! {"
4617 «aaa
4618 bbb
4619 added_lineˇ»
4620 "});
4621
4622 // Removing line
4623 cx.set_state(indoc! {"
4624 aa«a
4625 bbbˇ»
4626 "});
4627 cx.update_editor(|e, window, cx| {
4628 e.manipulate_immutable_lines(window, cx, |lines| {
4629 lines.pop();
4630 })
4631 });
4632 cx.assert_editor_state(indoc! {"
4633 «aaaˇ»
4634 "});
4635
4636 // Removing all lines
4637 cx.set_state(indoc! {"
4638 aa«a
4639 bbbˇ»
4640 "});
4641 cx.update_editor(|e, window, cx| {
4642 e.manipulate_immutable_lines(window, cx, |lines| {
4643 lines.drain(..);
4644 })
4645 });
4646 cx.assert_editor_state(indoc! {"
4647 ˇ
4648 "});
4649}
4650
4651#[gpui::test]
4652async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4653 init_test(cx, |_| {});
4654
4655 let mut cx = EditorTestContext::new(cx).await;
4656
4657 // Consider continuous selection as single selection
4658 cx.set_state(indoc! {"
4659 Aaa«aa
4660 cˇ»c«c
4661 bb
4662 aaaˇ»aa
4663 "});
4664 cx.update_editor(|e, window, cx| {
4665 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4666 });
4667 cx.assert_editor_state(indoc! {"
4668 «Aaaaa
4669 ccc
4670 bb
4671 aaaaaˇ»
4672 "});
4673
4674 cx.set_state(indoc! {"
4675 Aaa«aa
4676 cˇ»c«c
4677 bb
4678 aaaˇ»aa
4679 "});
4680 cx.update_editor(|e, window, cx| {
4681 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4682 });
4683 cx.assert_editor_state(indoc! {"
4684 «Aaaaa
4685 ccc
4686 bbˇ»
4687 "});
4688
4689 // Consider non continuous selection as distinct dedup operations
4690 cx.set_state(indoc! {"
4691 «aaaaa
4692 bb
4693 aaaaa
4694 aaaaaˇ»
4695
4696 aaa«aaˇ»
4697 "});
4698 cx.update_editor(|e, window, cx| {
4699 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4700 });
4701 cx.assert_editor_state(indoc! {"
4702 «aaaaa
4703 bbˇ»
4704
4705 «aaaaaˇ»
4706 "});
4707}
4708
4709#[gpui::test]
4710async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4711 init_test(cx, |_| {});
4712
4713 let mut cx = EditorTestContext::new(cx).await;
4714
4715 cx.set_state(indoc! {"
4716 «Aaa
4717 aAa
4718 Aaaˇ»
4719 "});
4720 cx.update_editor(|e, window, cx| {
4721 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4722 });
4723 cx.assert_editor_state(indoc! {"
4724 «Aaa
4725 aAaˇ»
4726 "});
4727
4728 cx.set_state(indoc! {"
4729 «Aaa
4730 aAa
4731 aaAˇ»
4732 "});
4733 cx.update_editor(|e, window, cx| {
4734 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4735 });
4736 cx.assert_editor_state(indoc! {"
4737 «Aaaˇ»
4738 "});
4739}
4740
4741#[gpui::test]
4742async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4743 init_test(cx, |_| {});
4744
4745 let mut cx = EditorTestContext::new(cx).await;
4746
4747 let js_language = Arc::new(Language::new(
4748 LanguageConfig {
4749 name: "JavaScript".into(),
4750 wrap_characters: Some(language::WrapCharactersConfig {
4751 start_prefix: "<".into(),
4752 start_suffix: ">".into(),
4753 end_prefix: "</".into(),
4754 end_suffix: ">".into(),
4755 }),
4756 ..LanguageConfig::default()
4757 },
4758 None,
4759 ));
4760
4761 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4762
4763 cx.set_state(indoc! {"
4764 «testˇ»
4765 "});
4766 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4767 cx.assert_editor_state(indoc! {"
4768 <«ˇ»>test</«ˇ»>
4769 "});
4770
4771 cx.set_state(indoc! {"
4772 «test
4773 testˇ»
4774 "});
4775 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4776 cx.assert_editor_state(indoc! {"
4777 <«ˇ»>test
4778 test</«ˇ»>
4779 "});
4780
4781 cx.set_state(indoc! {"
4782 teˇst
4783 "});
4784 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4785 cx.assert_editor_state(indoc! {"
4786 te<«ˇ»></«ˇ»>st
4787 "});
4788}
4789
4790#[gpui::test]
4791async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4792 init_test(cx, |_| {});
4793
4794 let mut cx = EditorTestContext::new(cx).await;
4795
4796 let js_language = Arc::new(Language::new(
4797 LanguageConfig {
4798 name: "JavaScript".into(),
4799 wrap_characters: Some(language::WrapCharactersConfig {
4800 start_prefix: "<".into(),
4801 start_suffix: ">".into(),
4802 end_prefix: "</".into(),
4803 end_suffix: ">".into(),
4804 }),
4805 ..LanguageConfig::default()
4806 },
4807 None,
4808 ));
4809
4810 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4811
4812 cx.set_state(indoc! {"
4813 «testˇ»
4814 «testˇ» «testˇ»
4815 «testˇ»
4816 "});
4817 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4818 cx.assert_editor_state(indoc! {"
4819 <«ˇ»>test</«ˇ»>
4820 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4821 <«ˇ»>test</«ˇ»>
4822 "});
4823
4824 cx.set_state(indoc! {"
4825 «test
4826 testˇ»
4827 «test
4828 testˇ»
4829 "});
4830 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4831 cx.assert_editor_state(indoc! {"
4832 <«ˇ»>test
4833 test</«ˇ»>
4834 <«ˇ»>test
4835 test</«ˇ»>
4836 "});
4837}
4838
4839#[gpui::test]
4840async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4841 init_test(cx, |_| {});
4842
4843 let mut cx = EditorTestContext::new(cx).await;
4844
4845 let plaintext_language = Arc::new(Language::new(
4846 LanguageConfig {
4847 name: "Plain Text".into(),
4848 ..LanguageConfig::default()
4849 },
4850 None,
4851 ));
4852
4853 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4854
4855 cx.set_state(indoc! {"
4856 «testˇ»
4857 "});
4858 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4859 cx.assert_editor_state(indoc! {"
4860 «testˇ»
4861 "});
4862}
4863
4864#[gpui::test]
4865async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4866 init_test(cx, |_| {});
4867
4868 let mut cx = EditorTestContext::new(cx).await;
4869
4870 // Manipulate with multiple selections on a single line
4871 cx.set_state(indoc! {"
4872 dd«dd
4873 cˇ»c«c
4874 bb
4875 aaaˇ»aa
4876 "});
4877 cx.update_editor(|e, window, cx| {
4878 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4879 });
4880 cx.assert_editor_state(indoc! {"
4881 «aaaaa
4882 bb
4883 ccc
4884 ddddˇ»
4885 "});
4886
4887 // Manipulate with multiple disjoin selections
4888 cx.set_state(indoc! {"
4889 5«
4890 4
4891 3
4892 2
4893 1ˇ»
4894
4895 dd«dd
4896 ccc
4897 bb
4898 aaaˇ»aa
4899 "});
4900 cx.update_editor(|e, window, cx| {
4901 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4902 });
4903 cx.assert_editor_state(indoc! {"
4904 «1
4905 2
4906 3
4907 4
4908 5ˇ»
4909
4910 «aaaaa
4911 bb
4912 ccc
4913 ddddˇ»
4914 "});
4915
4916 // Adding lines on each selection
4917 cx.set_state(indoc! {"
4918 2«
4919 1ˇ»
4920
4921 bb«bb
4922 aaaˇ»aa
4923 "});
4924 cx.update_editor(|e, window, cx| {
4925 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4926 });
4927 cx.assert_editor_state(indoc! {"
4928 «2
4929 1
4930 added lineˇ»
4931
4932 «bbbb
4933 aaaaa
4934 added lineˇ»
4935 "});
4936
4937 // Removing lines on each selection
4938 cx.set_state(indoc! {"
4939 2«
4940 1ˇ»
4941
4942 bb«bb
4943 aaaˇ»aa
4944 "});
4945 cx.update_editor(|e, window, cx| {
4946 e.manipulate_immutable_lines(window, cx, |lines| {
4947 lines.pop();
4948 })
4949 });
4950 cx.assert_editor_state(indoc! {"
4951 «2ˇ»
4952
4953 «bbbbˇ»
4954 "});
4955}
4956
4957#[gpui::test]
4958async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4959 init_test(cx, |settings| {
4960 settings.defaults.tab_size = NonZeroU32::new(3)
4961 });
4962
4963 let mut cx = EditorTestContext::new(cx).await;
4964
4965 // MULTI SELECTION
4966 // Ln.1 "«" tests empty lines
4967 // Ln.9 tests just leading whitespace
4968 cx.set_state(indoc! {"
4969 «
4970 abc // No indentationˇ»
4971 «\tabc // 1 tabˇ»
4972 \t\tabc « ˇ» // 2 tabs
4973 \t ab«c // Tab followed by space
4974 \tabc // Space followed by tab (3 spaces should be the result)
4975 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4976 abˇ»ˇc ˇ ˇ // Already space indented«
4977 \t
4978 \tabc\tdef // Only the leading tab is manipulatedˇ»
4979 "});
4980 cx.update_editor(|e, window, cx| {
4981 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4982 });
4983 cx.assert_editor_state(
4984 indoc! {"
4985 «
4986 abc // No indentation
4987 abc // 1 tab
4988 abc // 2 tabs
4989 abc // Tab followed by space
4990 abc // Space followed by tab (3 spaces should be the result)
4991 abc // Mixed indentation (tab conversion depends on the column)
4992 abc // Already space indented
4993 ·
4994 abc\tdef // Only the leading tab is manipulatedˇ»
4995 "}
4996 .replace("·", "")
4997 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
4998 );
4999
5000 // Test on just a few lines, the others should remain unchanged
5001 // Only lines (3, 5, 10, 11) should change
5002 cx.set_state(
5003 indoc! {"
5004 ·
5005 abc // No indentation
5006 \tabcˇ // 1 tab
5007 \t\tabc // 2 tabs
5008 \t abcˇ // Tab followed by space
5009 \tabc // Space followed by tab (3 spaces should be the result)
5010 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5011 abc // Already space indented
5012 «\t
5013 \tabc\tdef // Only the leading tab is manipulatedˇ»
5014 "}
5015 .replace("·", "")
5016 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5017 );
5018 cx.update_editor(|e, window, cx| {
5019 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5020 });
5021 cx.assert_editor_state(
5022 indoc! {"
5023 ·
5024 abc // No indentation
5025 « abc // 1 tabˇ»
5026 \t\tabc // 2 tabs
5027 « abc // Tab followed by spaceˇ»
5028 \tabc // Space followed by tab (3 spaces should be the result)
5029 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5030 abc // Already space indented
5031 « ·
5032 abc\tdef // Only the leading tab is manipulatedˇ»
5033 "}
5034 .replace("·", "")
5035 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5036 );
5037
5038 // SINGLE SELECTION
5039 // Ln.1 "«" tests empty lines
5040 // Ln.9 tests just leading whitespace
5041 cx.set_state(indoc! {"
5042 «
5043 abc // No indentation
5044 \tabc // 1 tab
5045 \t\tabc // 2 tabs
5046 \t abc // Tab followed by space
5047 \tabc // Space followed by tab (3 spaces should be the result)
5048 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5049 abc // Already space indented
5050 \t
5051 \tabc\tdef // Only the leading tab is manipulatedˇ»
5052 "});
5053 cx.update_editor(|e, window, cx| {
5054 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5055 });
5056 cx.assert_editor_state(
5057 indoc! {"
5058 «
5059 abc // No indentation
5060 abc // 1 tab
5061 abc // 2 tabs
5062 abc // Tab followed by space
5063 abc // Space followed by tab (3 spaces should be the result)
5064 abc // Mixed indentation (tab conversion depends on the column)
5065 abc // Already space indented
5066 ·
5067 abc\tdef // Only the leading tab is manipulatedˇ»
5068 "}
5069 .replace("·", "")
5070 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5071 );
5072}
5073
5074#[gpui::test]
5075async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5076 init_test(cx, |settings| {
5077 settings.defaults.tab_size = NonZeroU32::new(3)
5078 });
5079
5080 let mut cx = EditorTestContext::new(cx).await;
5081
5082 // MULTI SELECTION
5083 // Ln.1 "«" tests empty lines
5084 // Ln.11 tests just leading whitespace
5085 cx.set_state(indoc! {"
5086 «
5087 abˇ»ˇc // No indentation
5088 abc ˇ ˇ // 1 space (< 3 so dont convert)
5089 abc « // 2 spaces (< 3 so dont convert)
5090 abc // 3 spaces (convert)
5091 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5092 «\tˇ»\t«\tˇ»abc // Already tab indented
5093 «\t abc // Tab followed by space
5094 \tabc // Space followed by tab (should be consumed due to tab)
5095 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5096 \tˇ» «\t
5097 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5098 "});
5099 cx.update_editor(|e, window, cx| {
5100 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5101 });
5102 cx.assert_editor_state(indoc! {"
5103 «
5104 abc // No indentation
5105 abc // 1 space (< 3 so dont convert)
5106 abc // 2 spaces (< 3 so dont convert)
5107 \tabc // 3 spaces (convert)
5108 \t abc // 5 spaces (1 tab + 2 spaces)
5109 \t\t\tabc // Already tab indented
5110 \t abc // Tab followed by space
5111 \tabc // Space followed by tab (should be consumed due to tab)
5112 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5113 \t\t\t
5114 \tabc \t // Only the leading spaces should be convertedˇ»
5115 "});
5116
5117 // Test on just a few lines, the other should remain unchanged
5118 // Only lines (4, 8, 11, 12) should change
5119 cx.set_state(
5120 indoc! {"
5121 ·
5122 abc // No indentation
5123 abc // 1 space (< 3 so dont convert)
5124 abc // 2 spaces (< 3 so dont convert)
5125 « abc // 3 spaces (convert)ˇ»
5126 abc // 5 spaces (1 tab + 2 spaces)
5127 \t\t\tabc // Already tab indented
5128 \t abc // Tab followed by space
5129 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5130 \t\t \tabc // Mixed indentation
5131 \t \t \t \tabc // Mixed indentation
5132 \t \tˇ
5133 « abc \t // Only the leading spaces should be convertedˇ»
5134 "}
5135 .replace("·", "")
5136 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5137 );
5138 cx.update_editor(|e, window, cx| {
5139 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5140 });
5141 cx.assert_editor_state(
5142 indoc! {"
5143 ·
5144 abc // No indentation
5145 abc // 1 space (< 3 so dont convert)
5146 abc // 2 spaces (< 3 so dont convert)
5147 «\tabc // 3 spaces (convert)ˇ»
5148 abc // 5 spaces (1 tab + 2 spaces)
5149 \t\t\tabc // Already tab indented
5150 \t abc // Tab followed by space
5151 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5152 \t\t \tabc // Mixed indentation
5153 \t \t \t \tabc // Mixed indentation
5154 «\t\t\t
5155 \tabc \t // Only the leading spaces should be convertedˇ»
5156 "}
5157 .replace("·", "")
5158 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5159 );
5160
5161 // SINGLE SELECTION
5162 // Ln.1 "«" tests empty lines
5163 // Ln.11 tests just leading whitespace
5164 cx.set_state(indoc! {"
5165 «
5166 abc // No indentation
5167 abc // 1 space (< 3 so dont convert)
5168 abc // 2 spaces (< 3 so dont convert)
5169 abc // 3 spaces (convert)
5170 abc // 5 spaces (1 tab + 2 spaces)
5171 \t\t\tabc // Already tab indented
5172 \t abc // Tab followed by space
5173 \tabc // Space followed by tab (should be consumed due to tab)
5174 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5175 \t \t
5176 abc \t // Only the leading spaces should be convertedˇ»
5177 "});
5178 cx.update_editor(|e, window, cx| {
5179 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5180 });
5181 cx.assert_editor_state(indoc! {"
5182 «
5183 abc // No indentation
5184 abc // 1 space (< 3 so dont convert)
5185 abc // 2 spaces (< 3 so dont convert)
5186 \tabc // 3 spaces (convert)
5187 \t abc // 5 spaces (1 tab + 2 spaces)
5188 \t\t\tabc // Already tab indented
5189 \t abc // Tab followed by space
5190 \tabc // Space followed by tab (should be consumed due to tab)
5191 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5192 \t\t\t
5193 \tabc \t // Only the leading spaces should be convertedˇ»
5194 "});
5195}
5196
5197#[gpui::test]
5198async fn test_toggle_case(cx: &mut TestAppContext) {
5199 init_test(cx, |_| {});
5200
5201 let mut cx = EditorTestContext::new(cx).await;
5202
5203 // If all lower case -> upper case
5204 cx.set_state(indoc! {"
5205 «hello worldˇ»
5206 "});
5207 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5208 cx.assert_editor_state(indoc! {"
5209 «HELLO WORLDˇ»
5210 "});
5211
5212 // If all upper case -> lower case
5213 cx.set_state(indoc! {"
5214 «HELLO WORLDˇ»
5215 "});
5216 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5217 cx.assert_editor_state(indoc! {"
5218 «hello worldˇ»
5219 "});
5220
5221 // If any upper case characters are identified -> lower case
5222 // This matches JetBrains IDEs
5223 cx.set_state(indoc! {"
5224 «hEllo worldˇ»
5225 "});
5226 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5227 cx.assert_editor_state(indoc! {"
5228 «hello worldˇ»
5229 "});
5230}
5231
5232#[gpui::test]
5233async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5234 init_test(cx, |_| {});
5235
5236 let mut cx = EditorTestContext::new(cx).await;
5237
5238 cx.set_state(indoc! {"
5239 «implement-windows-supportˇ»
5240 "});
5241 cx.update_editor(|e, window, cx| {
5242 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5243 });
5244 cx.assert_editor_state(indoc! {"
5245 «Implement windows supportˇ»
5246 "});
5247}
5248
5249#[gpui::test]
5250async fn test_manipulate_text(cx: &mut TestAppContext) {
5251 init_test(cx, |_| {});
5252
5253 let mut cx = EditorTestContext::new(cx).await;
5254
5255 // Test convert_to_upper_case()
5256 cx.set_state(indoc! {"
5257 «hello worldˇ»
5258 "});
5259 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5260 cx.assert_editor_state(indoc! {"
5261 «HELLO WORLDˇ»
5262 "});
5263
5264 // Test convert_to_lower_case()
5265 cx.set_state(indoc! {"
5266 «HELLO WORLDˇ»
5267 "});
5268 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5269 cx.assert_editor_state(indoc! {"
5270 «hello worldˇ»
5271 "});
5272
5273 // Test multiple line, single selection case
5274 cx.set_state(indoc! {"
5275 «The quick brown
5276 fox jumps over
5277 the lazy dogˇ»
5278 "});
5279 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5280 cx.assert_editor_state(indoc! {"
5281 «The Quick Brown
5282 Fox Jumps Over
5283 The Lazy Dogˇ»
5284 "});
5285
5286 // Test multiple line, single selection case
5287 cx.set_state(indoc! {"
5288 «The quick brown
5289 fox jumps over
5290 the lazy dogˇ»
5291 "});
5292 cx.update_editor(|e, window, cx| {
5293 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5294 });
5295 cx.assert_editor_state(indoc! {"
5296 «TheQuickBrown
5297 FoxJumpsOver
5298 TheLazyDogˇ»
5299 "});
5300
5301 // From here on out, test more complex cases of manipulate_text()
5302
5303 // Test no selection case - should affect words cursors are in
5304 // Cursor at beginning, middle, and end of word
5305 cx.set_state(indoc! {"
5306 ˇhello big beauˇtiful worldˇ
5307 "});
5308 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5309 cx.assert_editor_state(indoc! {"
5310 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5311 "});
5312
5313 // Test multiple selections on a single line and across multiple lines
5314 cx.set_state(indoc! {"
5315 «Theˇ» quick «brown
5316 foxˇ» jumps «overˇ»
5317 the «lazyˇ» dog
5318 "});
5319 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5320 cx.assert_editor_state(indoc! {"
5321 «THEˇ» quick «BROWN
5322 FOXˇ» jumps «OVERˇ»
5323 the «LAZYˇ» dog
5324 "});
5325
5326 // Test case where text length grows
5327 cx.set_state(indoc! {"
5328 «tschüߡ»
5329 "});
5330 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5331 cx.assert_editor_state(indoc! {"
5332 «TSCHÜSSˇ»
5333 "});
5334
5335 // Test to make sure we don't crash when text shrinks
5336 cx.set_state(indoc! {"
5337 aaa_bbbˇ
5338 "});
5339 cx.update_editor(|e, window, cx| {
5340 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5341 });
5342 cx.assert_editor_state(indoc! {"
5343 «aaaBbbˇ»
5344 "});
5345
5346 // Test to make sure we all aware of the fact that each word can grow and shrink
5347 // Final selections should be aware of this fact
5348 cx.set_state(indoc! {"
5349 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5350 "});
5351 cx.update_editor(|e, window, cx| {
5352 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5353 });
5354 cx.assert_editor_state(indoc! {"
5355 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5356 "});
5357
5358 cx.set_state(indoc! {"
5359 «hElLo, WoRld!ˇ»
5360 "});
5361 cx.update_editor(|e, window, cx| {
5362 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5363 });
5364 cx.assert_editor_state(indoc! {"
5365 «HeLlO, wOrLD!ˇ»
5366 "});
5367
5368 // Test selections with `line_mode = true`.
5369 cx.update_editor(|editor, _window, _cx| editor.selections.line_mode = true);
5370 cx.set_state(indoc! {"
5371 «The quick brown
5372 fox jumps over
5373 tˇ»he lazy dog
5374 "});
5375 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5376 cx.assert_editor_state(indoc! {"
5377 «THE QUICK BROWN
5378 FOX JUMPS OVER
5379 THE LAZY DOGˇ»
5380 "});
5381}
5382
5383#[gpui::test]
5384fn test_duplicate_line(cx: &mut TestAppContext) {
5385 init_test(cx, |_| {});
5386
5387 let editor = cx.add_window(|window, cx| {
5388 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5389 build_editor(buffer, window, cx)
5390 });
5391 _ = editor.update(cx, |editor, window, cx| {
5392 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5393 s.select_display_ranges([
5394 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5395 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5396 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5397 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5398 ])
5399 });
5400 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5401 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5402 assert_eq!(
5403 editor.selections.display_ranges(cx),
5404 vec![
5405 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5406 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5407 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5408 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5409 ]
5410 );
5411 });
5412
5413 let editor = cx.add_window(|window, cx| {
5414 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5415 build_editor(buffer, window, cx)
5416 });
5417 _ = editor.update(cx, |editor, window, cx| {
5418 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5419 s.select_display_ranges([
5420 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5421 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5422 ])
5423 });
5424 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5425 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5426 assert_eq!(
5427 editor.selections.display_ranges(cx),
5428 vec![
5429 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5430 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5431 ]
5432 );
5433 });
5434
5435 // With `move_upwards` the selections stay in place, except for
5436 // the lines inserted above them
5437 let editor = cx.add_window(|window, cx| {
5438 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5439 build_editor(buffer, window, cx)
5440 });
5441 _ = editor.update(cx, |editor, window, cx| {
5442 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5443 s.select_display_ranges([
5444 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5445 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5446 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5447 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5448 ])
5449 });
5450 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5451 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5452 assert_eq!(
5453 editor.selections.display_ranges(cx),
5454 vec![
5455 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5456 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5457 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5458 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5459 ]
5460 );
5461 });
5462
5463 let editor = cx.add_window(|window, cx| {
5464 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5465 build_editor(buffer, window, cx)
5466 });
5467 _ = editor.update(cx, |editor, window, cx| {
5468 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5469 s.select_display_ranges([
5470 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5471 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5472 ])
5473 });
5474 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5475 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5476 assert_eq!(
5477 editor.selections.display_ranges(cx),
5478 vec![
5479 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5480 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5481 ]
5482 );
5483 });
5484
5485 let editor = cx.add_window(|window, cx| {
5486 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5487 build_editor(buffer, window, cx)
5488 });
5489 _ = editor.update(cx, |editor, window, cx| {
5490 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5491 s.select_display_ranges([
5492 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5493 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5494 ])
5495 });
5496 editor.duplicate_selection(&DuplicateSelection, window, cx);
5497 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5498 assert_eq!(
5499 editor.selections.display_ranges(cx),
5500 vec![
5501 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5502 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5503 ]
5504 );
5505 });
5506}
5507
5508#[gpui::test]
5509fn test_move_line_up_down(cx: &mut TestAppContext) {
5510 init_test(cx, |_| {});
5511
5512 let editor = cx.add_window(|window, cx| {
5513 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5514 build_editor(buffer, window, cx)
5515 });
5516 _ = editor.update(cx, |editor, window, cx| {
5517 editor.fold_creases(
5518 vec![
5519 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5520 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5521 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5522 ],
5523 true,
5524 window,
5525 cx,
5526 );
5527 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5528 s.select_display_ranges([
5529 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5530 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5531 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5532 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5533 ])
5534 });
5535 assert_eq!(
5536 editor.display_text(cx),
5537 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5538 );
5539
5540 editor.move_line_up(&MoveLineUp, window, cx);
5541 assert_eq!(
5542 editor.display_text(cx),
5543 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5544 );
5545 assert_eq!(
5546 editor.selections.display_ranges(cx),
5547 vec![
5548 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5549 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5550 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5551 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5552 ]
5553 );
5554 });
5555
5556 _ = editor.update(cx, |editor, window, cx| {
5557 editor.move_line_down(&MoveLineDown, window, cx);
5558 assert_eq!(
5559 editor.display_text(cx),
5560 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5561 );
5562 assert_eq!(
5563 editor.selections.display_ranges(cx),
5564 vec![
5565 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5566 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5567 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5568 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5569 ]
5570 );
5571 });
5572
5573 _ = editor.update(cx, |editor, window, cx| {
5574 editor.move_line_down(&MoveLineDown, window, cx);
5575 assert_eq!(
5576 editor.display_text(cx),
5577 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5578 );
5579 assert_eq!(
5580 editor.selections.display_ranges(cx),
5581 vec![
5582 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5583 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5584 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5585 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5586 ]
5587 );
5588 });
5589
5590 _ = editor.update(cx, |editor, window, cx| {
5591 editor.move_line_up(&MoveLineUp, window, cx);
5592 assert_eq!(
5593 editor.display_text(cx),
5594 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5595 );
5596 assert_eq!(
5597 editor.selections.display_ranges(cx),
5598 vec![
5599 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5600 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5601 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5602 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5603 ]
5604 );
5605 });
5606}
5607
5608#[gpui::test]
5609fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5610 init_test(cx, |_| {});
5611 let editor = cx.add_window(|window, cx| {
5612 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5613 build_editor(buffer, window, cx)
5614 });
5615 _ = editor.update(cx, |editor, window, cx| {
5616 editor.fold_creases(
5617 vec![Crease::simple(
5618 Point::new(6, 4)..Point::new(7, 4),
5619 FoldPlaceholder::test(),
5620 )],
5621 true,
5622 window,
5623 cx,
5624 );
5625 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5626 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5627 });
5628 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5629 editor.move_line_up(&MoveLineUp, window, cx);
5630 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5631 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5632 });
5633}
5634
5635#[gpui::test]
5636fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5637 init_test(cx, |_| {});
5638
5639 let editor = cx.add_window(|window, cx| {
5640 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5641 build_editor(buffer, window, cx)
5642 });
5643 _ = editor.update(cx, |editor, window, cx| {
5644 let snapshot = editor.buffer.read(cx).snapshot(cx);
5645 editor.insert_blocks(
5646 [BlockProperties {
5647 style: BlockStyle::Fixed,
5648 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5649 height: Some(1),
5650 render: Arc::new(|_| div().into_any()),
5651 priority: 0,
5652 }],
5653 Some(Autoscroll::fit()),
5654 cx,
5655 );
5656 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5657 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5658 });
5659 editor.move_line_down(&MoveLineDown, window, cx);
5660 });
5661}
5662
5663#[gpui::test]
5664async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5665 init_test(cx, |_| {});
5666
5667 let mut cx = EditorTestContext::new(cx).await;
5668 cx.set_state(
5669 &"
5670 ˇzero
5671 one
5672 two
5673 three
5674 four
5675 five
5676 "
5677 .unindent(),
5678 );
5679
5680 // Create a four-line block that replaces three lines of text.
5681 cx.update_editor(|editor, window, cx| {
5682 let snapshot = editor.snapshot(window, cx);
5683 let snapshot = &snapshot.buffer_snapshot;
5684 let placement = BlockPlacement::Replace(
5685 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5686 );
5687 editor.insert_blocks(
5688 [BlockProperties {
5689 placement,
5690 height: Some(4),
5691 style: BlockStyle::Sticky,
5692 render: Arc::new(|_| gpui::div().into_any_element()),
5693 priority: 0,
5694 }],
5695 None,
5696 cx,
5697 );
5698 });
5699
5700 // Move down so that the cursor touches the block.
5701 cx.update_editor(|editor, window, cx| {
5702 editor.move_down(&Default::default(), window, cx);
5703 });
5704 cx.assert_editor_state(
5705 &"
5706 zero
5707 «one
5708 two
5709 threeˇ»
5710 four
5711 five
5712 "
5713 .unindent(),
5714 );
5715
5716 // Move down past the block.
5717 cx.update_editor(|editor, window, cx| {
5718 editor.move_down(&Default::default(), window, cx);
5719 });
5720 cx.assert_editor_state(
5721 &"
5722 zero
5723 one
5724 two
5725 three
5726 ˇfour
5727 five
5728 "
5729 .unindent(),
5730 );
5731}
5732
5733#[gpui::test]
5734fn test_transpose(cx: &mut TestAppContext) {
5735 init_test(cx, |_| {});
5736
5737 _ = cx.add_window(|window, cx| {
5738 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5739 editor.set_style(EditorStyle::default(), window, cx);
5740 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5741 s.select_ranges([1..1])
5742 });
5743 editor.transpose(&Default::default(), window, cx);
5744 assert_eq!(editor.text(cx), "bac");
5745 assert_eq!(editor.selections.ranges(cx), [2..2]);
5746
5747 editor.transpose(&Default::default(), window, cx);
5748 assert_eq!(editor.text(cx), "bca");
5749 assert_eq!(editor.selections.ranges(cx), [3..3]);
5750
5751 editor.transpose(&Default::default(), window, cx);
5752 assert_eq!(editor.text(cx), "bac");
5753 assert_eq!(editor.selections.ranges(cx), [3..3]);
5754
5755 editor
5756 });
5757
5758 _ = cx.add_window(|window, cx| {
5759 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5760 editor.set_style(EditorStyle::default(), window, cx);
5761 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5762 s.select_ranges([3..3])
5763 });
5764 editor.transpose(&Default::default(), window, cx);
5765 assert_eq!(editor.text(cx), "acb\nde");
5766 assert_eq!(editor.selections.ranges(cx), [3..3]);
5767
5768 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5769 s.select_ranges([4..4])
5770 });
5771 editor.transpose(&Default::default(), window, cx);
5772 assert_eq!(editor.text(cx), "acbd\ne");
5773 assert_eq!(editor.selections.ranges(cx), [5..5]);
5774
5775 editor.transpose(&Default::default(), window, cx);
5776 assert_eq!(editor.text(cx), "acbde\n");
5777 assert_eq!(editor.selections.ranges(cx), [6..6]);
5778
5779 editor.transpose(&Default::default(), window, cx);
5780 assert_eq!(editor.text(cx), "acbd\ne");
5781 assert_eq!(editor.selections.ranges(cx), [6..6]);
5782
5783 editor
5784 });
5785
5786 _ = cx.add_window(|window, cx| {
5787 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5788 editor.set_style(EditorStyle::default(), window, cx);
5789 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5790 s.select_ranges([1..1, 2..2, 4..4])
5791 });
5792 editor.transpose(&Default::default(), window, cx);
5793 assert_eq!(editor.text(cx), "bacd\ne");
5794 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5795
5796 editor.transpose(&Default::default(), window, cx);
5797 assert_eq!(editor.text(cx), "bcade\n");
5798 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5799
5800 editor.transpose(&Default::default(), window, cx);
5801 assert_eq!(editor.text(cx), "bcda\ne");
5802 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5803
5804 editor.transpose(&Default::default(), window, cx);
5805 assert_eq!(editor.text(cx), "bcade\n");
5806 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5807
5808 editor.transpose(&Default::default(), window, cx);
5809 assert_eq!(editor.text(cx), "bcaed\n");
5810 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5811
5812 editor
5813 });
5814
5815 _ = cx.add_window(|window, cx| {
5816 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5817 editor.set_style(EditorStyle::default(), window, cx);
5818 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5819 s.select_ranges([4..4])
5820 });
5821 editor.transpose(&Default::default(), window, cx);
5822 assert_eq!(editor.text(cx), "🏀🍐✋");
5823 assert_eq!(editor.selections.ranges(cx), [8..8]);
5824
5825 editor.transpose(&Default::default(), window, cx);
5826 assert_eq!(editor.text(cx), "🏀✋🍐");
5827 assert_eq!(editor.selections.ranges(cx), [11..11]);
5828
5829 editor.transpose(&Default::default(), window, cx);
5830 assert_eq!(editor.text(cx), "🏀🍐✋");
5831 assert_eq!(editor.selections.ranges(cx), [11..11]);
5832
5833 editor
5834 });
5835}
5836
5837#[gpui::test]
5838async fn test_rewrap(cx: &mut TestAppContext) {
5839 init_test(cx, |settings| {
5840 settings.languages.0.extend([
5841 (
5842 "Markdown".into(),
5843 LanguageSettingsContent {
5844 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5845 preferred_line_length: Some(40),
5846 ..Default::default()
5847 },
5848 ),
5849 (
5850 "Plain Text".into(),
5851 LanguageSettingsContent {
5852 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5853 preferred_line_length: Some(40),
5854 ..Default::default()
5855 },
5856 ),
5857 (
5858 "C++".into(),
5859 LanguageSettingsContent {
5860 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5861 preferred_line_length: Some(40),
5862 ..Default::default()
5863 },
5864 ),
5865 (
5866 "Python".into(),
5867 LanguageSettingsContent {
5868 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5869 preferred_line_length: Some(40),
5870 ..Default::default()
5871 },
5872 ),
5873 (
5874 "Rust".into(),
5875 LanguageSettingsContent {
5876 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5877 preferred_line_length: Some(40),
5878 ..Default::default()
5879 },
5880 ),
5881 ])
5882 });
5883
5884 let mut cx = EditorTestContext::new(cx).await;
5885
5886 let cpp_language = Arc::new(Language::new(
5887 LanguageConfig {
5888 name: "C++".into(),
5889 line_comments: vec!["// ".into()],
5890 ..LanguageConfig::default()
5891 },
5892 None,
5893 ));
5894 let python_language = Arc::new(Language::new(
5895 LanguageConfig {
5896 name: "Python".into(),
5897 line_comments: vec!["# ".into()],
5898 ..LanguageConfig::default()
5899 },
5900 None,
5901 ));
5902 let markdown_language = Arc::new(Language::new(
5903 LanguageConfig {
5904 name: "Markdown".into(),
5905 rewrap_prefixes: vec![
5906 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5907 regex::Regex::new("[-*+]\\s+").unwrap(),
5908 ],
5909 ..LanguageConfig::default()
5910 },
5911 None,
5912 ));
5913 let rust_language = Arc::new(
5914 Language::new(
5915 LanguageConfig {
5916 name: "Rust".into(),
5917 line_comments: vec!["// ".into(), "/// ".into()],
5918 ..LanguageConfig::default()
5919 },
5920 Some(tree_sitter_rust::LANGUAGE.into()),
5921 )
5922 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5923 .unwrap(),
5924 );
5925
5926 let plaintext_language = Arc::new(Language::new(
5927 LanguageConfig {
5928 name: "Plain Text".into(),
5929 ..LanguageConfig::default()
5930 },
5931 None,
5932 ));
5933
5934 // Test basic rewrapping of a long line with a cursor
5935 assert_rewrap(
5936 indoc! {"
5937 // ˇThis is a long comment that needs to be wrapped.
5938 "},
5939 indoc! {"
5940 // ˇThis is a long comment that needs to
5941 // be wrapped.
5942 "},
5943 cpp_language.clone(),
5944 &mut cx,
5945 );
5946
5947 // Test rewrapping a full selection
5948 assert_rewrap(
5949 indoc! {"
5950 «// This selected long comment needs to be wrapped.ˇ»"
5951 },
5952 indoc! {"
5953 «// This selected long comment needs to
5954 // be wrapped.ˇ»"
5955 },
5956 cpp_language.clone(),
5957 &mut cx,
5958 );
5959
5960 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5961 assert_rewrap(
5962 indoc! {"
5963 // ˇThis is the first line.
5964 // Thisˇ is the second line.
5965 // This is the thirdˇ line, all part of one paragraph.
5966 "},
5967 indoc! {"
5968 // ˇThis is the first line. Thisˇ is the
5969 // second line. This is the thirdˇ line,
5970 // all part of one paragraph.
5971 "},
5972 cpp_language.clone(),
5973 &mut cx,
5974 );
5975
5976 // Test multiple cursors in different paragraphs trigger separate rewraps
5977 assert_rewrap(
5978 indoc! {"
5979 // ˇThis is the first paragraph, first line.
5980 // ˇThis is the first paragraph, second line.
5981
5982 // ˇThis is the second paragraph, first line.
5983 // ˇThis is the second paragraph, second line.
5984 "},
5985 indoc! {"
5986 // ˇThis is the first paragraph, first
5987 // line. ˇThis is the first paragraph,
5988 // second line.
5989
5990 // ˇThis is the second paragraph, first
5991 // line. ˇThis is the second paragraph,
5992 // second line.
5993 "},
5994 cpp_language.clone(),
5995 &mut cx,
5996 );
5997
5998 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
5999 assert_rewrap(
6000 indoc! {"
6001 «// A regular long long comment to be wrapped.
6002 /// A documentation long comment to be wrapped.ˇ»
6003 "},
6004 indoc! {"
6005 «// A regular long long comment to be
6006 // wrapped.
6007 /// A documentation long comment to be
6008 /// wrapped.ˇ»
6009 "},
6010 rust_language.clone(),
6011 &mut cx,
6012 );
6013
6014 // Test that change in indentation level trigger seperate rewraps
6015 assert_rewrap(
6016 indoc! {"
6017 fn foo() {
6018 «// This is a long comment at the base indent.
6019 // This is a long comment at the next indent.ˇ»
6020 }
6021 "},
6022 indoc! {"
6023 fn foo() {
6024 «// This is a long comment at the
6025 // base indent.
6026 // This is a long comment at the
6027 // next indent.ˇ»
6028 }
6029 "},
6030 rust_language.clone(),
6031 &mut cx,
6032 );
6033
6034 // Test that different comment prefix characters (e.g., '#') are handled correctly
6035 assert_rewrap(
6036 indoc! {"
6037 # ˇThis is a long comment using a pound sign.
6038 "},
6039 indoc! {"
6040 # ˇThis is a long comment using a pound
6041 # sign.
6042 "},
6043 python_language,
6044 &mut cx,
6045 );
6046
6047 // Test rewrapping only affects comments, not code even when selected
6048 assert_rewrap(
6049 indoc! {"
6050 «/// This doc comment is long and should be wrapped.
6051 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6052 "},
6053 indoc! {"
6054 «/// This doc comment is long and should
6055 /// be wrapped.
6056 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6057 "},
6058 rust_language.clone(),
6059 &mut cx,
6060 );
6061
6062 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6063 assert_rewrap(
6064 indoc! {"
6065 # Header
6066
6067 A long long long line of markdown text to wrap.ˇ
6068 "},
6069 indoc! {"
6070 # Header
6071
6072 A long long long line of markdown text
6073 to wrap.ˇ
6074 "},
6075 markdown_language.clone(),
6076 &mut cx,
6077 );
6078
6079 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6080 assert_rewrap(
6081 indoc! {"
6082 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6083 2. This is a numbered list item that is very long and needs to be wrapped properly.
6084 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6085 "},
6086 indoc! {"
6087 «1. This is a numbered list item that is
6088 very long and needs to be wrapped
6089 properly.
6090 2. This is a numbered list item that is
6091 very long and needs to be wrapped
6092 properly.
6093 - This is an unordered list item that is
6094 also very long and should not merge
6095 with the numbered item.ˇ»
6096 "},
6097 markdown_language.clone(),
6098 &mut cx,
6099 );
6100
6101 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6102 assert_rewrap(
6103 indoc! {"
6104 «1. This is a numbered list item that is
6105 very long and needs to be wrapped
6106 properly.
6107 2. This is a numbered list item that is
6108 very long and needs to be wrapped
6109 properly.
6110 - This is an unordered list item that is
6111 also very long and should not merge with
6112 the numbered item.ˇ»
6113 "},
6114 indoc! {"
6115 «1. This is a numbered list item that is
6116 very long and needs to be wrapped
6117 properly.
6118 2. This is a numbered list item that is
6119 very long and needs to be wrapped
6120 properly.
6121 - This is an unordered list item that is
6122 also very long and should not merge
6123 with the numbered item.ˇ»
6124 "},
6125 markdown_language.clone(),
6126 &mut cx,
6127 );
6128
6129 // Test that rewrapping maintain indents even when they already exists.
6130 assert_rewrap(
6131 indoc! {"
6132 «1. This is a numbered list
6133 item that is very long and needs to be wrapped properly.
6134 2. This is a numbered list
6135 item that is very long and needs to be wrapped properly.
6136 - This is an unordered list item that is also very long and
6137 should not merge with the numbered item.ˇ»
6138 "},
6139 indoc! {"
6140 «1. This is a numbered list item that is
6141 very long and needs to be wrapped
6142 properly.
6143 2. This is a numbered list item that is
6144 very long and needs to be wrapped
6145 properly.
6146 - This is an unordered list item that is
6147 also very long and should not merge
6148 with the numbered item.ˇ»
6149 "},
6150 markdown_language,
6151 &mut cx,
6152 );
6153
6154 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6155 assert_rewrap(
6156 indoc! {"
6157 ˇThis is a very long line of plain text that will be wrapped.
6158 "},
6159 indoc! {"
6160 ˇThis is a very long line of plain text
6161 that will be wrapped.
6162 "},
6163 plaintext_language.clone(),
6164 &mut cx,
6165 );
6166
6167 // Test that non-commented code acts as a paragraph boundary within a selection
6168 assert_rewrap(
6169 indoc! {"
6170 «// This is the first long comment block to be wrapped.
6171 fn my_func(a: u32);
6172 // This is the second long comment block to be wrapped.ˇ»
6173 "},
6174 indoc! {"
6175 «// This is the first long comment block
6176 // to be wrapped.
6177 fn my_func(a: u32);
6178 // This is the second long comment block
6179 // to be wrapped.ˇ»
6180 "},
6181 rust_language,
6182 &mut cx,
6183 );
6184
6185 // Test rewrapping multiple selections, including ones with blank lines or tabs
6186 assert_rewrap(
6187 indoc! {"
6188 «ˇThis is a very long line that will be wrapped.
6189
6190 This is another paragraph in the same selection.»
6191
6192 «\tThis is a very long indented line that will be wrapped.ˇ»
6193 "},
6194 indoc! {"
6195 «ˇThis is a very long line that will be
6196 wrapped.
6197
6198 This is another paragraph in the same
6199 selection.»
6200
6201 «\tThis is a very long indented line
6202 \tthat will be wrapped.ˇ»
6203 "},
6204 plaintext_language,
6205 &mut cx,
6206 );
6207
6208 // Test that an empty comment line acts as a paragraph boundary
6209 assert_rewrap(
6210 indoc! {"
6211 // ˇThis is a long comment that will be wrapped.
6212 //
6213 // And this is another long comment that will also be wrapped.ˇ
6214 "},
6215 indoc! {"
6216 // ˇThis is a long comment that will be
6217 // wrapped.
6218 //
6219 // And this is another long comment that
6220 // will also be wrapped.ˇ
6221 "},
6222 cpp_language,
6223 &mut cx,
6224 );
6225
6226 #[track_caller]
6227 fn assert_rewrap(
6228 unwrapped_text: &str,
6229 wrapped_text: &str,
6230 language: Arc<Language>,
6231 cx: &mut EditorTestContext,
6232 ) {
6233 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6234 cx.set_state(unwrapped_text);
6235 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6236 cx.assert_editor_state(wrapped_text);
6237 }
6238}
6239
6240#[gpui::test]
6241async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6242 init_test(cx, |settings| {
6243 settings.languages.0.extend([(
6244 "Rust".into(),
6245 LanguageSettingsContent {
6246 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6247 preferred_line_length: Some(40),
6248 ..Default::default()
6249 },
6250 )])
6251 });
6252
6253 let mut cx = EditorTestContext::new(cx).await;
6254
6255 let rust_lang = Arc::new(
6256 Language::new(
6257 LanguageConfig {
6258 name: "Rust".into(),
6259 line_comments: vec!["// ".into()],
6260 block_comment: Some(BlockCommentConfig {
6261 start: "/*".into(),
6262 end: "*/".into(),
6263 prefix: "* ".into(),
6264 tab_size: 1,
6265 }),
6266 documentation_comment: Some(BlockCommentConfig {
6267 start: "/**".into(),
6268 end: "*/".into(),
6269 prefix: "* ".into(),
6270 tab_size: 1,
6271 }),
6272
6273 ..LanguageConfig::default()
6274 },
6275 Some(tree_sitter_rust::LANGUAGE.into()),
6276 )
6277 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6278 .unwrap(),
6279 );
6280
6281 // regular block comment
6282 assert_rewrap(
6283 indoc! {"
6284 /*
6285 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6286 */
6287 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6288 "},
6289 indoc! {"
6290 /*
6291 *ˇ Lorem ipsum dolor sit amet,
6292 * consectetur adipiscing elit.
6293 */
6294 /*
6295 *ˇ Lorem ipsum dolor sit amet,
6296 * consectetur adipiscing elit.
6297 */
6298 "},
6299 rust_lang.clone(),
6300 &mut cx,
6301 );
6302
6303 // indent is respected
6304 assert_rewrap(
6305 indoc! {"
6306 {}
6307 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6308 "},
6309 indoc! {"
6310 {}
6311 /*
6312 *ˇ Lorem ipsum dolor sit amet,
6313 * consectetur adipiscing elit.
6314 */
6315 "},
6316 rust_lang.clone(),
6317 &mut cx,
6318 );
6319
6320 // short block comments with inline delimiters
6321 assert_rewrap(
6322 indoc! {"
6323 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6324 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6325 */
6326 /*
6327 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6328 "},
6329 indoc! {"
6330 /*
6331 *ˇ Lorem ipsum dolor sit amet,
6332 * consectetur adipiscing elit.
6333 */
6334 /*
6335 *ˇ Lorem ipsum dolor sit amet,
6336 * consectetur adipiscing elit.
6337 */
6338 /*
6339 *ˇ Lorem ipsum dolor sit amet,
6340 * consectetur adipiscing elit.
6341 */
6342 "},
6343 rust_lang.clone(),
6344 &mut cx,
6345 );
6346
6347 // multiline block comment with inline start/end delimiters
6348 assert_rewrap(
6349 indoc! {"
6350 /*ˇ Lorem ipsum dolor sit amet,
6351 * consectetur adipiscing elit. */
6352 "},
6353 indoc! {"
6354 /*
6355 *ˇ Lorem ipsum dolor sit amet,
6356 * consectetur adipiscing elit.
6357 */
6358 "},
6359 rust_lang.clone(),
6360 &mut cx,
6361 );
6362
6363 // block comment rewrap still respects paragraph bounds
6364 assert_rewrap(
6365 indoc! {"
6366 /*
6367 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6368 *
6369 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6370 */
6371 "},
6372 indoc! {"
6373 /*
6374 *ˇ Lorem ipsum dolor sit amet,
6375 * consectetur adipiscing elit.
6376 *
6377 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6378 */
6379 "},
6380 rust_lang.clone(),
6381 &mut cx,
6382 );
6383
6384 // documentation comments
6385 assert_rewrap(
6386 indoc! {"
6387 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6388 /**
6389 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6390 */
6391 "},
6392 indoc! {"
6393 /**
6394 *ˇ Lorem ipsum dolor sit amet,
6395 * consectetur adipiscing elit.
6396 */
6397 /**
6398 *ˇ Lorem ipsum dolor sit amet,
6399 * consectetur adipiscing elit.
6400 */
6401 "},
6402 rust_lang.clone(),
6403 &mut cx,
6404 );
6405
6406 // different, adjacent comments
6407 assert_rewrap(
6408 indoc! {"
6409 /**
6410 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6411 */
6412 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6413 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6414 "},
6415 indoc! {"
6416 /**
6417 *ˇ Lorem ipsum dolor sit amet,
6418 * consectetur adipiscing elit.
6419 */
6420 /*
6421 *ˇ Lorem ipsum dolor sit amet,
6422 * consectetur adipiscing elit.
6423 */
6424 //ˇ Lorem ipsum dolor sit amet,
6425 // consectetur adipiscing elit.
6426 "},
6427 rust_lang.clone(),
6428 &mut cx,
6429 );
6430
6431 // selection w/ single short block comment
6432 assert_rewrap(
6433 indoc! {"
6434 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6435 "},
6436 indoc! {"
6437 «/*
6438 * Lorem ipsum dolor sit amet,
6439 * consectetur adipiscing elit.
6440 */ˇ»
6441 "},
6442 rust_lang.clone(),
6443 &mut cx,
6444 );
6445
6446 // rewrapping a single comment w/ abutting comments
6447 assert_rewrap(
6448 indoc! {"
6449 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6450 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6451 "},
6452 indoc! {"
6453 /*
6454 * ˇLorem ipsum dolor sit amet,
6455 * consectetur adipiscing elit.
6456 */
6457 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6458 "},
6459 rust_lang.clone(),
6460 &mut cx,
6461 );
6462
6463 // selection w/ non-abutting short block comments
6464 assert_rewrap(
6465 indoc! {"
6466 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6467
6468 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6469 "},
6470 indoc! {"
6471 «/*
6472 * Lorem ipsum dolor sit amet,
6473 * consectetur adipiscing elit.
6474 */
6475
6476 /*
6477 * Lorem ipsum dolor sit amet,
6478 * consectetur adipiscing elit.
6479 */ˇ»
6480 "},
6481 rust_lang.clone(),
6482 &mut cx,
6483 );
6484
6485 // selection of multiline block comments
6486 assert_rewrap(
6487 indoc! {"
6488 «/* Lorem ipsum dolor sit amet,
6489 * consectetur adipiscing elit. */ˇ»
6490 "},
6491 indoc! {"
6492 «/*
6493 * Lorem ipsum dolor sit amet,
6494 * consectetur adipiscing elit.
6495 */ˇ»
6496 "},
6497 rust_lang.clone(),
6498 &mut cx,
6499 );
6500
6501 // partial selection of multiline block comments
6502 assert_rewrap(
6503 indoc! {"
6504 «/* Lorem ipsum dolor sit amet,ˇ»
6505 * consectetur adipiscing elit. */
6506 /* Lorem ipsum dolor sit amet,
6507 «* consectetur adipiscing elit. */ˇ»
6508 "},
6509 indoc! {"
6510 «/*
6511 * Lorem ipsum dolor sit amet,ˇ»
6512 * consectetur adipiscing elit. */
6513 /* Lorem ipsum dolor sit amet,
6514 «* consectetur adipiscing elit.
6515 */ˇ»
6516 "},
6517 rust_lang.clone(),
6518 &mut cx,
6519 );
6520
6521 // selection w/ abutting short block comments
6522 // TODO: should not be combined; should rewrap as 2 comments
6523 assert_rewrap(
6524 indoc! {"
6525 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6526 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6527 "},
6528 // desired behavior:
6529 // indoc! {"
6530 // «/*
6531 // * Lorem ipsum dolor sit amet,
6532 // * consectetur adipiscing elit.
6533 // */
6534 // /*
6535 // * Lorem ipsum dolor sit amet,
6536 // * consectetur adipiscing elit.
6537 // */ˇ»
6538 // "},
6539 // actual behaviour:
6540 indoc! {"
6541 «/*
6542 * Lorem ipsum dolor sit amet,
6543 * consectetur adipiscing elit. Lorem
6544 * ipsum dolor sit amet, consectetur
6545 * adipiscing elit.
6546 */ˇ»
6547 "},
6548 rust_lang.clone(),
6549 &mut cx,
6550 );
6551
6552 // TODO: same as above, but with delimiters on separate line
6553 // assert_rewrap(
6554 // indoc! {"
6555 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6556 // */
6557 // /*
6558 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6559 // "},
6560 // // desired:
6561 // // indoc! {"
6562 // // «/*
6563 // // * Lorem ipsum dolor sit amet,
6564 // // * consectetur adipiscing elit.
6565 // // */
6566 // // /*
6567 // // * Lorem ipsum dolor sit amet,
6568 // // * consectetur adipiscing elit.
6569 // // */ˇ»
6570 // // "},
6571 // // actual: (but with trailing w/s on the empty lines)
6572 // indoc! {"
6573 // «/*
6574 // * Lorem ipsum dolor sit amet,
6575 // * consectetur adipiscing elit.
6576 // *
6577 // */
6578 // /*
6579 // *
6580 // * Lorem ipsum dolor sit amet,
6581 // * consectetur adipiscing elit.
6582 // */ˇ»
6583 // "},
6584 // rust_lang.clone(),
6585 // &mut cx,
6586 // );
6587
6588 // TODO these are unhandled edge cases; not correct, just documenting known issues
6589 assert_rewrap(
6590 indoc! {"
6591 /*
6592 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6593 */
6594 /*
6595 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6596 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6597 "},
6598 // desired:
6599 // indoc! {"
6600 // /*
6601 // *ˇ Lorem ipsum dolor sit amet,
6602 // * consectetur adipiscing elit.
6603 // */
6604 // /*
6605 // *ˇ Lorem ipsum dolor sit amet,
6606 // * consectetur adipiscing elit.
6607 // */
6608 // /*
6609 // *ˇ Lorem ipsum dolor sit amet
6610 // */ /* consectetur adipiscing elit. */
6611 // "},
6612 // actual:
6613 indoc! {"
6614 /*
6615 //ˇ Lorem ipsum dolor sit amet,
6616 // consectetur adipiscing elit.
6617 */
6618 /*
6619 * //ˇ Lorem ipsum dolor sit amet,
6620 * consectetur adipiscing elit.
6621 */
6622 /*
6623 *ˇ Lorem ipsum dolor sit amet */ /*
6624 * consectetur adipiscing elit.
6625 */
6626 "},
6627 rust_lang,
6628 &mut cx,
6629 );
6630
6631 #[track_caller]
6632 fn assert_rewrap(
6633 unwrapped_text: &str,
6634 wrapped_text: &str,
6635 language: Arc<Language>,
6636 cx: &mut EditorTestContext,
6637 ) {
6638 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6639 cx.set_state(unwrapped_text);
6640 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6641 cx.assert_editor_state(wrapped_text);
6642 }
6643}
6644
6645#[gpui::test]
6646async fn test_hard_wrap(cx: &mut TestAppContext) {
6647 init_test(cx, |_| {});
6648 let mut cx = EditorTestContext::new(cx).await;
6649
6650 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6651 cx.update_editor(|editor, _, cx| {
6652 editor.set_hard_wrap(Some(14), cx);
6653 });
6654
6655 cx.set_state(indoc!(
6656 "
6657 one two three ˇ
6658 "
6659 ));
6660 cx.simulate_input("four");
6661 cx.run_until_parked();
6662
6663 cx.assert_editor_state(indoc!(
6664 "
6665 one two three
6666 fourˇ
6667 "
6668 ));
6669
6670 cx.update_editor(|editor, window, cx| {
6671 editor.newline(&Default::default(), window, cx);
6672 });
6673 cx.run_until_parked();
6674 cx.assert_editor_state(indoc!(
6675 "
6676 one two three
6677 four
6678 ˇ
6679 "
6680 ));
6681
6682 cx.simulate_input("five");
6683 cx.run_until_parked();
6684 cx.assert_editor_state(indoc!(
6685 "
6686 one two three
6687 four
6688 fiveˇ
6689 "
6690 ));
6691
6692 cx.update_editor(|editor, window, cx| {
6693 editor.newline(&Default::default(), window, cx);
6694 });
6695 cx.run_until_parked();
6696 cx.simulate_input("# ");
6697 cx.run_until_parked();
6698 cx.assert_editor_state(indoc!(
6699 "
6700 one two three
6701 four
6702 five
6703 # ˇ
6704 "
6705 ));
6706
6707 cx.update_editor(|editor, window, cx| {
6708 editor.newline(&Default::default(), window, cx);
6709 });
6710 cx.run_until_parked();
6711 cx.assert_editor_state(indoc!(
6712 "
6713 one two three
6714 four
6715 five
6716 #\x20
6717 #ˇ
6718 "
6719 ));
6720
6721 cx.simulate_input(" 6");
6722 cx.run_until_parked();
6723 cx.assert_editor_state(indoc!(
6724 "
6725 one two three
6726 four
6727 five
6728 #
6729 # 6ˇ
6730 "
6731 ));
6732}
6733
6734#[gpui::test]
6735async fn test_cut_line_ends(cx: &mut TestAppContext) {
6736 init_test(cx, |_| {});
6737
6738 let mut cx = EditorTestContext::new(cx).await;
6739
6740 cx.set_state(indoc! {"
6741 The quick« brownˇ»
6742 fox jumps overˇ
6743 the lazy dog"});
6744 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6745 cx.assert_editor_state(indoc! {"
6746 The quickˇ
6747 ˇthe lazy dog"});
6748
6749 cx.set_state(indoc! {"
6750 The quick« brownˇ»
6751 fox jumps overˇ
6752 the lazy dog"});
6753 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6754 cx.assert_editor_state(indoc! {"
6755 The quickˇ
6756 fox jumps overˇthe lazy dog"});
6757
6758 cx.set_state(indoc! {"
6759 The quick« brownˇ»
6760 fox jumps overˇ
6761 the lazy dog"});
6762 cx.update_editor(|e, window, cx| {
6763 e.cut_to_end_of_line(
6764 &CutToEndOfLine {
6765 stop_at_newlines: true,
6766 },
6767 window,
6768 cx,
6769 )
6770 });
6771 cx.assert_editor_state(indoc! {"
6772 The quickˇ
6773 fox jumps overˇ
6774 the lazy dog"});
6775
6776 cx.set_state(indoc! {"
6777 The quick« brownˇ»
6778 fox jumps overˇ
6779 the lazy dog"});
6780 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6781 cx.assert_editor_state(indoc! {"
6782 The quickˇ
6783 fox jumps overˇthe lazy dog"});
6784}
6785
6786#[gpui::test]
6787async fn test_clipboard(cx: &mut TestAppContext) {
6788 init_test(cx, |_| {});
6789
6790 let mut cx = EditorTestContext::new(cx).await;
6791
6792 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6793 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6794 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6795
6796 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6797 cx.set_state("two ˇfour ˇsix ˇ");
6798 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6799 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6800
6801 // Paste again but with only two cursors. Since the number of cursors doesn't
6802 // match the number of slices in the clipboard, the entire clipboard text
6803 // is pasted at each cursor.
6804 cx.set_state("ˇtwo one✅ four three six five ˇ");
6805 cx.update_editor(|e, window, cx| {
6806 e.handle_input("( ", window, cx);
6807 e.paste(&Paste, window, cx);
6808 e.handle_input(") ", window, cx);
6809 });
6810 cx.assert_editor_state(
6811 &([
6812 "( one✅ ",
6813 "three ",
6814 "five ) ˇtwo one✅ four three six five ( one✅ ",
6815 "three ",
6816 "five ) ˇ",
6817 ]
6818 .join("\n")),
6819 );
6820
6821 // Cut with three selections, one of which is full-line.
6822 cx.set_state(indoc! {"
6823 1«2ˇ»3
6824 4ˇ567
6825 «8ˇ»9"});
6826 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6827 cx.assert_editor_state(indoc! {"
6828 1ˇ3
6829 ˇ9"});
6830
6831 // Paste with three selections, noticing how the copied selection that was full-line
6832 // gets inserted before the second cursor.
6833 cx.set_state(indoc! {"
6834 1ˇ3
6835 9ˇ
6836 «oˇ»ne"});
6837 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6838 cx.assert_editor_state(indoc! {"
6839 12ˇ3
6840 4567
6841 9ˇ
6842 8ˇne"});
6843
6844 // Copy with a single cursor only, which writes the whole line into the clipboard.
6845 cx.set_state(indoc! {"
6846 The quick brown
6847 fox juˇmps over
6848 the lazy dog"});
6849 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6850 assert_eq!(
6851 cx.read_from_clipboard()
6852 .and_then(|item| item.text().as_deref().map(str::to_string)),
6853 Some("fox jumps over\n".to_string())
6854 );
6855
6856 // Paste with three selections, noticing how the copied full-line selection is inserted
6857 // before the empty selections but replaces the selection that is non-empty.
6858 cx.set_state(indoc! {"
6859 Tˇhe quick brown
6860 «foˇ»x jumps over
6861 tˇhe lazy dog"});
6862 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6863 cx.assert_editor_state(indoc! {"
6864 fox jumps over
6865 Tˇhe quick brown
6866 fox jumps over
6867 ˇx jumps over
6868 fox jumps over
6869 tˇhe lazy dog"});
6870}
6871
6872#[gpui::test]
6873async fn test_copy_trim(cx: &mut TestAppContext) {
6874 init_test(cx, |_| {});
6875
6876 let mut cx = EditorTestContext::new(cx).await;
6877 cx.set_state(
6878 r#" «for selection in selections.iter() {
6879 let mut start = selection.start;
6880 let mut end = selection.end;
6881 let is_entire_line = selection.is_empty();
6882 if is_entire_line {
6883 start = Point::new(start.row, 0);ˇ»
6884 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6885 }
6886 "#,
6887 );
6888 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6889 assert_eq!(
6890 cx.read_from_clipboard()
6891 .and_then(|item| item.text().as_deref().map(str::to_string)),
6892 Some(
6893 "for selection in selections.iter() {
6894 let mut start = selection.start;
6895 let mut end = selection.end;
6896 let is_entire_line = selection.is_empty();
6897 if is_entire_line {
6898 start = Point::new(start.row, 0);"
6899 .to_string()
6900 ),
6901 "Regular copying preserves all indentation selected",
6902 );
6903 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6904 assert_eq!(
6905 cx.read_from_clipboard()
6906 .and_then(|item| item.text().as_deref().map(str::to_string)),
6907 Some(
6908 "for selection in selections.iter() {
6909let mut start = selection.start;
6910let mut end = selection.end;
6911let is_entire_line = selection.is_empty();
6912if is_entire_line {
6913 start = Point::new(start.row, 0);"
6914 .to_string()
6915 ),
6916 "Copying with stripping should strip all leading whitespaces"
6917 );
6918
6919 cx.set_state(
6920 r#" « for selection in selections.iter() {
6921 let mut start = selection.start;
6922 let mut end = selection.end;
6923 let is_entire_line = selection.is_empty();
6924 if is_entire_line {
6925 start = Point::new(start.row, 0);ˇ»
6926 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6927 }
6928 "#,
6929 );
6930 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6931 assert_eq!(
6932 cx.read_from_clipboard()
6933 .and_then(|item| item.text().as_deref().map(str::to_string)),
6934 Some(
6935 " for selection in selections.iter() {
6936 let mut start = selection.start;
6937 let mut end = selection.end;
6938 let is_entire_line = selection.is_empty();
6939 if is_entire_line {
6940 start = Point::new(start.row, 0);"
6941 .to_string()
6942 ),
6943 "Regular copying preserves all indentation selected",
6944 );
6945 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6946 assert_eq!(
6947 cx.read_from_clipboard()
6948 .and_then(|item| item.text().as_deref().map(str::to_string)),
6949 Some(
6950 "for selection in selections.iter() {
6951let mut start = selection.start;
6952let mut end = selection.end;
6953let is_entire_line = selection.is_empty();
6954if is_entire_line {
6955 start = Point::new(start.row, 0);"
6956 .to_string()
6957 ),
6958 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6959 );
6960
6961 cx.set_state(
6962 r#" «ˇ for selection in selections.iter() {
6963 let mut start = selection.start;
6964 let mut end = selection.end;
6965 let is_entire_line = selection.is_empty();
6966 if is_entire_line {
6967 start = Point::new(start.row, 0);»
6968 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6969 }
6970 "#,
6971 );
6972 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6973 assert_eq!(
6974 cx.read_from_clipboard()
6975 .and_then(|item| item.text().as_deref().map(str::to_string)),
6976 Some(
6977 " for selection in selections.iter() {
6978 let mut start = selection.start;
6979 let mut end = selection.end;
6980 let is_entire_line = selection.is_empty();
6981 if is_entire_line {
6982 start = Point::new(start.row, 0);"
6983 .to_string()
6984 ),
6985 "Regular copying for reverse selection works the same",
6986 );
6987 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6988 assert_eq!(
6989 cx.read_from_clipboard()
6990 .and_then(|item| item.text().as_deref().map(str::to_string)),
6991 Some(
6992 "for selection in selections.iter() {
6993let mut start = selection.start;
6994let mut end = selection.end;
6995let is_entire_line = selection.is_empty();
6996if is_entire_line {
6997 start = Point::new(start.row, 0);"
6998 .to_string()
6999 ),
7000 "Copying with stripping for reverse selection works the same"
7001 );
7002
7003 cx.set_state(
7004 r#" for selection «in selections.iter() {
7005 let mut start = selection.start;
7006 let mut end = selection.end;
7007 let is_entire_line = selection.is_empty();
7008 if is_entire_line {
7009 start = Point::new(start.row, 0);ˇ»
7010 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7011 }
7012 "#,
7013 );
7014 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7015 assert_eq!(
7016 cx.read_from_clipboard()
7017 .and_then(|item| item.text().as_deref().map(str::to_string)),
7018 Some(
7019 "in selections.iter() {
7020 let mut start = selection.start;
7021 let mut end = selection.end;
7022 let is_entire_line = selection.is_empty();
7023 if is_entire_line {
7024 start = Point::new(start.row, 0);"
7025 .to_string()
7026 ),
7027 "When selecting past the indent, the copying works as usual",
7028 );
7029 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7030 assert_eq!(
7031 cx.read_from_clipboard()
7032 .and_then(|item| item.text().as_deref().map(str::to_string)),
7033 Some(
7034 "in selections.iter() {
7035 let mut start = selection.start;
7036 let mut end = selection.end;
7037 let is_entire_line = selection.is_empty();
7038 if is_entire_line {
7039 start = Point::new(start.row, 0);"
7040 .to_string()
7041 ),
7042 "When selecting past the indent, nothing is trimmed"
7043 );
7044
7045 cx.set_state(
7046 r#" «for selection in selections.iter() {
7047 let mut start = selection.start;
7048
7049 let mut end = selection.end;
7050 let is_entire_line = selection.is_empty();
7051 if is_entire_line {
7052 start = Point::new(start.row, 0);
7053ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7054 }
7055 "#,
7056 );
7057 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7058 assert_eq!(
7059 cx.read_from_clipboard()
7060 .and_then(|item| item.text().as_deref().map(str::to_string)),
7061 Some(
7062 "for selection in selections.iter() {
7063let mut start = selection.start;
7064
7065let mut end = selection.end;
7066let is_entire_line = selection.is_empty();
7067if is_entire_line {
7068 start = Point::new(start.row, 0);
7069"
7070 .to_string()
7071 ),
7072 "Copying with stripping should ignore empty lines"
7073 );
7074}
7075
7076#[gpui::test]
7077async fn test_paste_multiline(cx: &mut TestAppContext) {
7078 init_test(cx, |_| {});
7079
7080 let mut cx = EditorTestContext::new(cx).await;
7081 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7082
7083 // Cut an indented block, without the leading whitespace.
7084 cx.set_state(indoc! {"
7085 const a: B = (
7086 c(),
7087 «d(
7088 e,
7089 f
7090 )ˇ»
7091 );
7092 "});
7093 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7094 cx.assert_editor_state(indoc! {"
7095 const a: B = (
7096 c(),
7097 ˇ
7098 );
7099 "});
7100
7101 // Paste it at the same position.
7102 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7103 cx.assert_editor_state(indoc! {"
7104 const a: B = (
7105 c(),
7106 d(
7107 e,
7108 f
7109 )ˇ
7110 );
7111 "});
7112
7113 // Paste it at a line with a lower indent level.
7114 cx.set_state(indoc! {"
7115 ˇ
7116 const a: B = (
7117 c(),
7118 );
7119 "});
7120 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7121 cx.assert_editor_state(indoc! {"
7122 d(
7123 e,
7124 f
7125 )ˇ
7126 const a: B = (
7127 c(),
7128 );
7129 "});
7130
7131 // Cut an indented block, with the leading whitespace.
7132 cx.set_state(indoc! {"
7133 const a: B = (
7134 c(),
7135 « d(
7136 e,
7137 f
7138 )
7139 ˇ»);
7140 "});
7141 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7142 cx.assert_editor_state(indoc! {"
7143 const a: B = (
7144 c(),
7145 ˇ);
7146 "});
7147
7148 // Paste it at the same position.
7149 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7150 cx.assert_editor_state(indoc! {"
7151 const a: B = (
7152 c(),
7153 d(
7154 e,
7155 f
7156 )
7157 ˇ);
7158 "});
7159
7160 // Paste it at a line with a higher indent level.
7161 cx.set_state(indoc! {"
7162 const a: B = (
7163 c(),
7164 d(
7165 e,
7166 fˇ
7167 )
7168 );
7169 "});
7170 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7171 cx.assert_editor_state(indoc! {"
7172 const a: B = (
7173 c(),
7174 d(
7175 e,
7176 f d(
7177 e,
7178 f
7179 )
7180 ˇ
7181 )
7182 );
7183 "});
7184
7185 // Copy an indented block, starting mid-line
7186 cx.set_state(indoc! {"
7187 const a: B = (
7188 c(),
7189 somethin«g(
7190 e,
7191 f
7192 )ˇ»
7193 );
7194 "});
7195 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7196
7197 // Paste it on a line with a lower indent level
7198 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7199 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7200 cx.assert_editor_state(indoc! {"
7201 const a: B = (
7202 c(),
7203 something(
7204 e,
7205 f
7206 )
7207 );
7208 g(
7209 e,
7210 f
7211 )ˇ"});
7212}
7213
7214#[gpui::test]
7215async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7216 init_test(cx, |_| {});
7217
7218 cx.write_to_clipboard(ClipboardItem::new_string(
7219 " d(\n e\n );\n".into(),
7220 ));
7221
7222 let mut cx = EditorTestContext::new(cx).await;
7223 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7224
7225 cx.set_state(indoc! {"
7226 fn a() {
7227 b();
7228 if c() {
7229 ˇ
7230 }
7231 }
7232 "});
7233
7234 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7235 cx.assert_editor_state(indoc! {"
7236 fn a() {
7237 b();
7238 if c() {
7239 d(
7240 e
7241 );
7242 ˇ
7243 }
7244 }
7245 "});
7246
7247 cx.set_state(indoc! {"
7248 fn a() {
7249 b();
7250 ˇ
7251 }
7252 "});
7253
7254 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7255 cx.assert_editor_state(indoc! {"
7256 fn a() {
7257 b();
7258 d(
7259 e
7260 );
7261 ˇ
7262 }
7263 "});
7264}
7265
7266#[gpui::test]
7267fn test_select_all(cx: &mut TestAppContext) {
7268 init_test(cx, |_| {});
7269
7270 let editor = cx.add_window(|window, cx| {
7271 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7272 build_editor(buffer, window, cx)
7273 });
7274 _ = editor.update(cx, |editor, window, cx| {
7275 editor.select_all(&SelectAll, window, cx);
7276 assert_eq!(
7277 editor.selections.display_ranges(cx),
7278 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7279 );
7280 });
7281}
7282
7283#[gpui::test]
7284fn test_select_line(cx: &mut TestAppContext) {
7285 init_test(cx, |_| {});
7286
7287 let editor = cx.add_window(|window, cx| {
7288 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7289 build_editor(buffer, window, cx)
7290 });
7291 _ = editor.update(cx, |editor, window, cx| {
7292 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7293 s.select_display_ranges([
7294 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7295 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7296 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7297 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7298 ])
7299 });
7300 editor.select_line(&SelectLine, window, cx);
7301 assert_eq!(
7302 editor.selections.display_ranges(cx),
7303 vec![
7304 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7305 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7306 ]
7307 );
7308 });
7309
7310 _ = editor.update(cx, |editor, window, cx| {
7311 editor.select_line(&SelectLine, window, cx);
7312 assert_eq!(
7313 editor.selections.display_ranges(cx),
7314 vec![
7315 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7316 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7317 ]
7318 );
7319 });
7320
7321 _ = editor.update(cx, |editor, window, cx| {
7322 editor.select_line(&SelectLine, window, cx);
7323 assert_eq!(
7324 editor.selections.display_ranges(cx),
7325 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7326 );
7327 });
7328}
7329
7330#[gpui::test]
7331async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7332 init_test(cx, |_| {});
7333 let mut cx = EditorTestContext::new(cx).await;
7334
7335 #[track_caller]
7336 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7337 cx.set_state(initial_state);
7338 cx.update_editor(|e, window, cx| {
7339 e.split_selection_into_lines(&Default::default(), window, cx)
7340 });
7341 cx.assert_editor_state(expected_state);
7342 }
7343
7344 // Selection starts and ends at the middle of lines, left-to-right
7345 test(
7346 &mut cx,
7347 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7348 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7349 );
7350 // Same thing, right-to-left
7351 test(
7352 &mut cx,
7353 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7354 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7355 );
7356
7357 // Whole buffer, left-to-right, last line *doesn't* end with newline
7358 test(
7359 &mut cx,
7360 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7361 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7362 );
7363 // Same thing, right-to-left
7364 test(
7365 &mut cx,
7366 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7367 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7368 );
7369
7370 // Whole buffer, left-to-right, last line ends with newline
7371 test(
7372 &mut cx,
7373 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7374 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7375 );
7376 // Same thing, right-to-left
7377 test(
7378 &mut cx,
7379 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7380 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7381 );
7382
7383 // Starts at the end of a line, ends at the start of another
7384 test(
7385 &mut cx,
7386 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7387 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7388 );
7389}
7390
7391#[gpui::test]
7392async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7393 init_test(cx, |_| {});
7394
7395 let editor = cx.add_window(|window, cx| {
7396 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7397 build_editor(buffer, window, cx)
7398 });
7399
7400 // setup
7401 _ = editor.update(cx, |editor, window, cx| {
7402 editor.fold_creases(
7403 vec![
7404 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7405 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7406 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7407 ],
7408 true,
7409 window,
7410 cx,
7411 );
7412 assert_eq!(
7413 editor.display_text(cx),
7414 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7415 );
7416 });
7417
7418 _ = editor.update(cx, |editor, window, cx| {
7419 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7420 s.select_display_ranges([
7421 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7422 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7423 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7424 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7425 ])
7426 });
7427 editor.split_selection_into_lines(&Default::default(), window, cx);
7428 assert_eq!(
7429 editor.display_text(cx),
7430 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7431 );
7432 });
7433 EditorTestContext::for_editor(editor, cx)
7434 .await
7435 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7436
7437 _ = editor.update(cx, |editor, window, cx| {
7438 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7439 s.select_display_ranges([
7440 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7441 ])
7442 });
7443 editor.split_selection_into_lines(&Default::default(), window, cx);
7444 assert_eq!(
7445 editor.display_text(cx),
7446 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7447 );
7448 assert_eq!(
7449 editor.selections.display_ranges(cx),
7450 [
7451 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7452 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7453 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7454 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7455 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7456 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7457 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7458 ]
7459 );
7460 });
7461 EditorTestContext::for_editor(editor, cx)
7462 .await
7463 .assert_editor_state(
7464 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7465 );
7466}
7467
7468#[gpui::test]
7469async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7470 init_test(cx, |_| {});
7471
7472 let mut cx = EditorTestContext::new(cx).await;
7473
7474 cx.set_state(indoc!(
7475 r#"abc
7476 defˇghi
7477
7478 jk
7479 nlmo
7480 "#
7481 ));
7482
7483 cx.update_editor(|editor, window, cx| {
7484 editor.add_selection_above(&Default::default(), window, cx);
7485 });
7486
7487 cx.assert_editor_state(indoc!(
7488 r#"abcˇ
7489 defˇghi
7490
7491 jk
7492 nlmo
7493 "#
7494 ));
7495
7496 cx.update_editor(|editor, window, cx| {
7497 editor.add_selection_above(&Default::default(), window, cx);
7498 });
7499
7500 cx.assert_editor_state(indoc!(
7501 r#"abcˇ
7502 defˇghi
7503
7504 jk
7505 nlmo
7506 "#
7507 ));
7508
7509 cx.update_editor(|editor, window, cx| {
7510 editor.add_selection_below(&Default::default(), window, cx);
7511 });
7512
7513 cx.assert_editor_state(indoc!(
7514 r#"abc
7515 defˇghi
7516
7517 jk
7518 nlmo
7519 "#
7520 ));
7521
7522 cx.update_editor(|editor, window, cx| {
7523 editor.undo_selection(&Default::default(), window, cx);
7524 });
7525
7526 cx.assert_editor_state(indoc!(
7527 r#"abcˇ
7528 defˇghi
7529
7530 jk
7531 nlmo
7532 "#
7533 ));
7534
7535 cx.update_editor(|editor, window, cx| {
7536 editor.redo_selection(&Default::default(), window, cx);
7537 });
7538
7539 cx.assert_editor_state(indoc!(
7540 r#"abc
7541 defˇghi
7542
7543 jk
7544 nlmo
7545 "#
7546 ));
7547
7548 cx.update_editor(|editor, window, cx| {
7549 editor.add_selection_below(&Default::default(), window, cx);
7550 });
7551
7552 cx.assert_editor_state(indoc!(
7553 r#"abc
7554 defˇghi
7555 ˇ
7556 jk
7557 nlmo
7558 "#
7559 ));
7560
7561 cx.update_editor(|editor, window, cx| {
7562 editor.add_selection_below(&Default::default(), window, cx);
7563 });
7564
7565 cx.assert_editor_state(indoc!(
7566 r#"abc
7567 defˇghi
7568 ˇ
7569 jkˇ
7570 nlmo
7571 "#
7572 ));
7573
7574 cx.update_editor(|editor, window, cx| {
7575 editor.add_selection_below(&Default::default(), window, cx);
7576 });
7577
7578 cx.assert_editor_state(indoc!(
7579 r#"abc
7580 defˇghi
7581 ˇ
7582 jkˇ
7583 nlmˇo
7584 "#
7585 ));
7586
7587 cx.update_editor(|editor, window, cx| {
7588 editor.add_selection_below(&Default::default(), window, cx);
7589 });
7590
7591 cx.assert_editor_state(indoc!(
7592 r#"abc
7593 defˇghi
7594 ˇ
7595 jkˇ
7596 nlmˇo
7597 ˇ"#
7598 ));
7599
7600 // change selections
7601 cx.set_state(indoc!(
7602 r#"abc
7603 def«ˇg»hi
7604
7605 jk
7606 nlmo
7607 "#
7608 ));
7609
7610 cx.update_editor(|editor, window, cx| {
7611 editor.add_selection_below(&Default::default(), window, cx);
7612 });
7613
7614 cx.assert_editor_state(indoc!(
7615 r#"abc
7616 def«ˇg»hi
7617
7618 jk
7619 nlm«ˇo»
7620 "#
7621 ));
7622
7623 cx.update_editor(|editor, window, cx| {
7624 editor.add_selection_below(&Default::default(), window, cx);
7625 });
7626
7627 cx.assert_editor_state(indoc!(
7628 r#"abc
7629 def«ˇg»hi
7630
7631 jk
7632 nlm«ˇo»
7633 "#
7634 ));
7635
7636 cx.update_editor(|editor, window, cx| {
7637 editor.add_selection_above(&Default::default(), window, cx);
7638 });
7639
7640 cx.assert_editor_state(indoc!(
7641 r#"abc
7642 def«ˇg»hi
7643
7644 jk
7645 nlmo
7646 "#
7647 ));
7648
7649 cx.update_editor(|editor, window, cx| {
7650 editor.add_selection_above(&Default::default(), window, cx);
7651 });
7652
7653 cx.assert_editor_state(indoc!(
7654 r#"abc
7655 def«ˇg»hi
7656
7657 jk
7658 nlmo
7659 "#
7660 ));
7661
7662 // Change selections again
7663 cx.set_state(indoc!(
7664 r#"a«bc
7665 defgˇ»hi
7666
7667 jk
7668 nlmo
7669 "#
7670 ));
7671
7672 cx.update_editor(|editor, window, cx| {
7673 editor.add_selection_below(&Default::default(), window, cx);
7674 });
7675
7676 cx.assert_editor_state(indoc!(
7677 r#"a«bcˇ»
7678 d«efgˇ»hi
7679
7680 j«kˇ»
7681 nlmo
7682 "#
7683 ));
7684
7685 cx.update_editor(|editor, window, cx| {
7686 editor.add_selection_below(&Default::default(), window, cx);
7687 });
7688 cx.assert_editor_state(indoc!(
7689 r#"a«bcˇ»
7690 d«efgˇ»hi
7691
7692 j«kˇ»
7693 n«lmoˇ»
7694 "#
7695 ));
7696 cx.update_editor(|editor, window, cx| {
7697 editor.add_selection_above(&Default::default(), window, cx);
7698 });
7699
7700 cx.assert_editor_state(indoc!(
7701 r#"a«bcˇ»
7702 d«efgˇ»hi
7703
7704 j«kˇ»
7705 nlmo
7706 "#
7707 ));
7708
7709 // Change selections again
7710 cx.set_state(indoc!(
7711 r#"abc
7712 d«ˇefghi
7713
7714 jk
7715 nlm»o
7716 "#
7717 ));
7718
7719 cx.update_editor(|editor, window, cx| {
7720 editor.add_selection_above(&Default::default(), window, cx);
7721 });
7722
7723 cx.assert_editor_state(indoc!(
7724 r#"a«ˇbc»
7725 d«ˇef»ghi
7726
7727 j«ˇk»
7728 n«ˇlm»o
7729 "#
7730 ));
7731
7732 cx.update_editor(|editor, window, cx| {
7733 editor.add_selection_below(&Default::default(), window, cx);
7734 });
7735
7736 cx.assert_editor_state(indoc!(
7737 r#"abc
7738 d«ˇef»ghi
7739
7740 j«ˇk»
7741 n«ˇlm»o
7742 "#
7743 ));
7744}
7745
7746#[gpui::test]
7747async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7748 init_test(cx, |_| {});
7749 let mut cx = EditorTestContext::new(cx).await;
7750
7751 cx.set_state(indoc!(
7752 r#"line onˇe
7753 liˇne two
7754 line three
7755 line four"#
7756 ));
7757
7758 cx.update_editor(|editor, window, cx| {
7759 editor.add_selection_below(&Default::default(), window, cx);
7760 });
7761
7762 // test multiple cursors expand in the same direction
7763 cx.assert_editor_state(indoc!(
7764 r#"line onˇe
7765 liˇne twˇo
7766 liˇne three
7767 line four"#
7768 ));
7769
7770 cx.update_editor(|editor, window, cx| {
7771 editor.add_selection_below(&Default::default(), window, cx);
7772 });
7773
7774 cx.update_editor(|editor, window, cx| {
7775 editor.add_selection_below(&Default::default(), window, cx);
7776 });
7777
7778 // test multiple cursors expand below overflow
7779 cx.assert_editor_state(indoc!(
7780 r#"line onˇe
7781 liˇne twˇo
7782 liˇne thˇree
7783 liˇne foˇur"#
7784 ));
7785
7786 cx.update_editor(|editor, window, cx| {
7787 editor.add_selection_above(&Default::default(), window, cx);
7788 });
7789
7790 // test multiple cursors retrieves back correctly
7791 cx.assert_editor_state(indoc!(
7792 r#"line onˇe
7793 liˇne twˇo
7794 liˇne thˇree
7795 line four"#
7796 ));
7797
7798 cx.update_editor(|editor, window, cx| {
7799 editor.add_selection_above(&Default::default(), window, cx);
7800 });
7801
7802 cx.update_editor(|editor, window, cx| {
7803 editor.add_selection_above(&Default::default(), window, cx);
7804 });
7805
7806 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7807 cx.assert_editor_state(indoc!(
7808 r#"liˇne onˇe
7809 liˇne two
7810 line three
7811 line four"#
7812 ));
7813
7814 cx.update_editor(|editor, window, cx| {
7815 editor.undo_selection(&Default::default(), window, cx);
7816 });
7817
7818 // test undo
7819 cx.assert_editor_state(indoc!(
7820 r#"line onˇe
7821 liˇne twˇo
7822 line three
7823 line four"#
7824 ));
7825
7826 cx.update_editor(|editor, window, cx| {
7827 editor.redo_selection(&Default::default(), window, cx);
7828 });
7829
7830 // test redo
7831 cx.assert_editor_state(indoc!(
7832 r#"liˇne onˇe
7833 liˇne two
7834 line three
7835 line four"#
7836 ));
7837
7838 cx.set_state(indoc!(
7839 r#"abcd
7840 ef«ghˇ»
7841 ijkl
7842 «mˇ»nop"#
7843 ));
7844
7845 cx.update_editor(|editor, window, cx| {
7846 editor.add_selection_above(&Default::default(), window, cx);
7847 });
7848
7849 // test multiple selections expand in the same direction
7850 cx.assert_editor_state(indoc!(
7851 r#"ab«cdˇ»
7852 ef«ghˇ»
7853 «iˇ»jkl
7854 «mˇ»nop"#
7855 ));
7856
7857 cx.update_editor(|editor, window, cx| {
7858 editor.add_selection_above(&Default::default(), window, cx);
7859 });
7860
7861 // test multiple selection upward overflow
7862 cx.assert_editor_state(indoc!(
7863 r#"ab«cdˇ»
7864 «eˇ»f«ghˇ»
7865 «iˇ»jkl
7866 «mˇ»nop"#
7867 ));
7868
7869 cx.update_editor(|editor, window, cx| {
7870 editor.add_selection_below(&Default::default(), window, cx);
7871 });
7872
7873 // test multiple selection retrieves back correctly
7874 cx.assert_editor_state(indoc!(
7875 r#"abcd
7876 ef«ghˇ»
7877 «iˇ»jkl
7878 «mˇ»nop"#
7879 ));
7880
7881 cx.update_editor(|editor, window, cx| {
7882 editor.add_selection_below(&Default::default(), window, cx);
7883 });
7884
7885 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7886 cx.assert_editor_state(indoc!(
7887 r#"abcd
7888 ef«ghˇ»
7889 ij«klˇ»
7890 «mˇ»nop"#
7891 ));
7892
7893 cx.update_editor(|editor, window, cx| {
7894 editor.undo_selection(&Default::default(), window, cx);
7895 });
7896
7897 // test undo
7898 cx.assert_editor_state(indoc!(
7899 r#"abcd
7900 ef«ghˇ»
7901 «iˇ»jkl
7902 «mˇ»nop"#
7903 ));
7904
7905 cx.update_editor(|editor, window, cx| {
7906 editor.redo_selection(&Default::default(), window, cx);
7907 });
7908
7909 // test redo
7910 cx.assert_editor_state(indoc!(
7911 r#"abcd
7912 ef«ghˇ»
7913 ij«klˇ»
7914 «mˇ»nop"#
7915 ));
7916}
7917
7918#[gpui::test]
7919async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7920 init_test(cx, |_| {});
7921 let mut cx = EditorTestContext::new(cx).await;
7922
7923 cx.set_state(indoc!(
7924 r#"line onˇe
7925 liˇne two
7926 line three
7927 line four"#
7928 ));
7929
7930 cx.update_editor(|editor, window, cx| {
7931 editor.add_selection_below(&Default::default(), window, cx);
7932 editor.add_selection_below(&Default::default(), window, cx);
7933 editor.add_selection_below(&Default::default(), window, cx);
7934 });
7935
7936 // initial state with two multi cursor groups
7937 cx.assert_editor_state(indoc!(
7938 r#"line onˇe
7939 liˇne twˇo
7940 liˇne thˇree
7941 liˇne foˇur"#
7942 ));
7943
7944 // add single cursor in middle - simulate opt click
7945 cx.update_editor(|editor, window, cx| {
7946 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7947 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7948 editor.end_selection(window, cx);
7949 });
7950
7951 cx.assert_editor_state(indoc!(
7952 r#"line onˇe
7953 liˇne twˇo
7954 liˇneˇ thˇree
7955 liˇne foˇur"#
7956 ));
7957
7958 cx.update_editor(|editor, window, cx| {
7959 editor.add_selection_above(&Default::default(), window, cx);
7960 });
7961
7962 // test new added selection expands above and existing selection shrinks
7963 cx.assert_editor_state(indoc!(
7964 r#"line onˇe
7965 liˇneˇ twˇo
7966 liˇneˇ thˇree
7967 line four"#
7968 ));
7969
7970 cx.update_editor(|editor, window, cx| {
7971 editor.add_selection_above(&Default::default(), window, cx);
7972 });
7973
7974 // test new added selection expands above and existing selection shrinks
7975 cx.assert_editor_state(indoc!(
7976 r#"lineˇ onˇe
7977 liˇneˇ twˇo
7978 lineˇ three
7979 line four"#
7980 ));
7981
7982 // intial state with two selection groups
7983 cx.set_state(indoc!(
7984 r#"abcd
7985 ef«ghˇ»
7986 ijkl
7987 «mˇ»nop"#
7988 ));
7989
7990 cx.update_editor(|editor, window, cx| {
7991 editor.add_selection_above(&Default::default(), window, cx);
7992 editor.add_selection_above(&Default::default(), window, cx);
7993 });
7994
7995 cx.assert_editor_state(indoc!(
7996 r#"ab«cdˇ»
7997 «eˇ»f«ghˇ»
7998 «iˇ»jkl
7999 «mˇ»nop"#
8000 ));
8001
8002 // add single selection in middle - simulate opt drag
8003 cx.update_editor(|editor, window, cx| {
8004 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8005 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8006 editor.update_selection(
8007 DisplayPoint::new(DisplayRow(2), 4),
8008 0,
8009 gpui::Point::<f32>::default(),
8010 window,
8011 cx,
8012 );
8013 editor.end_selection(window, cx);
8014 });
8015
8016 cx.assert_editor_state(indoc!(
8017 r#"ab«cdˇ»
8018 «eˇ»f«ghˇ»
8019 «iˇ»jk«lˇ»
8020 «mˇ»nop"#
8021 ));
8022
8023 cx.update_editor(|editor, window, cx| {
8024 editor.add_selection_below(&Default::default(), window, cx);
8025 });
8026
8027 // test new added selection expands below, others shrinks from above
8028 cx.assert_editor_state(indoc!(
8029 r#"abcd
8030 ef«ghˇ»
8031 «iˇ»jk«lˇ»
8032 «mˇ»no«pˇ»"#
8033 ));
8034}
8035
8036#[gpui::test]
8037async fn test_select_next(cx: &mut TestAppContext) {
8038 init_test(cx, |_| {});
8039
8040 let mut cx = EditorTestContext::new(cx).await;
8041 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8042
8043 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8044 .unwrap();
8045 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8046
8047 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8048 .unwrap();
8049 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8050
8051 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8052 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8053
8054 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8055 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8056
8057 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8058 .unwrap();
8059 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8060
8061 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8062 .unwrap();
8063 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8064
8065 // Test selection direction should be preserved
8066 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8067
8068 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8069 .unwrap();
8070 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8071}
8072
8073#[gpui::test]
8074async fn test_select_all_matches(cx: &mut TestAppContext) {
8075 init_test(cx, |_| {});
8076
8077 let mut cx = EditorTestContext::new(cx).await;
8078
8079 // Test caret-only selections
8080 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8081 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8082 .unwrap();
8083 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8084
8085 // Test left-to-right selections
8086 cx.set_state("abc\n«abcˇ»\nabc");
8087 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8088 .unwrap();
8089 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8090
8091 // Test right-to-left selections
8092 cx.set_state("abc\n«ˇabc»\nabc");
8093 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8094 .unwrap();
8095 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8096
8097 // Test selecting whitespace with caret selection
8098 cx.set_state("abc\nˇ abc\nabc");
8099 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8100 .unwrap();
8101 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8102
8103 // Test selecting whitespace with left-to-right selection
8104 cx.set_state("abc\n«ˇ »abc\nabc");
8105 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8106 .unwrap();
8107 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8108
8109 // Test no matches with right-to-left selection
8110 cx.set_state("abc\n« ˇ»abc\nabc");
8111 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8112 .unwrap();
8113 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8114
8115 // Test with a single word and clip_at_line_ends=true (#29823)
8116 cx.set_state("aˇbc");
8117 cx.update_editor(|e, window, cx| {
8118 e.set_clip_at_line_ends(true, cx);
8119 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8120 e.set_clip_at_line_ends(false, cx);
8121 });
8122 cx.assert_editor_state("«abcˇ»");
8123}
8124
8125#[gpui::test]
8126async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8127 init_test(cx, |_| {});
8128
8129 let mut cx = EditorTestContext::new(cx).await;
8130
8131 let large_body_1 = "\nd".repeat(200);
8132 let large_body_2 = "\ne".repeat(200);
8133
8134 cx.set_state(&format!(
8135 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8136 ));
8137 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8138 let scroll_position = editor.scroll_position(cx);
8139 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8140 scroll_position
8141 });
8142
8143 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8144 .unwrap();
8145 cx.assert_editor_state(&format!(
8146 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8147 ));
8148 let scroll_position_after_selection =
8149 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8150 assert_eq!(
8151 initial_scroll_position, scroll_position_after_selection,
8152 "Scroll position should not change after selecting all matches"
8153 );
8154}
8155
8156#[gpui::test]
8157async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8158 init_test(cx, |_| {});
8159
8160 let mut cx = EditorLspTestContext::new_rust(
8161 lsp::ServerCapabilities {
8162 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8163 ..Default::default()
8164 },
8165 cx,
8166 )
8167 .await;
8168
8169 cx.set_state(indoc! {"
8170 line 1
8171 line 2
8172 linˇe 3
8173 line 4
8174 line 5
8175 "});
8176
8177 // Make an edit
8178 cx.update_editor(|editor, window, cx| {
8179 editor.handle_input("X", window, cx);
8180 });
8181
8182 // Move cursor to a different position
8183 cx.update_editor(|editor, window, cx| {
8184 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8185 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8186 });
8187 });
8188
8189 cx.assert_editor_state(indoc! {"
8190 line 1
8191 line 2
8192 linXe 3
8193 line 4
8194 liˇne 5
8195 "});
8196
8197 cx.lsp
8198 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8199 Ok(Some(vec![lsp::TextEdit::new(
8200 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8201 "PREFIX ".to_string(),
8202 )]))
8203 });
8204
8205 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8206 .unwrap()
8207 .await
8208 .unwrap();
8209
8210 cx.assert_editor_state(indoc! {"
8211 PREFIX line 1
8212 line 2
8213 linXe 3
8214 line 4
8215 liˇne 5
8216 "});
8217
8218 // Undo formatting
8219 cx.update_editor(|editor, window, cx| {
8220 editor.undo(&Default::default(), window, cx);
8221 });
8222
8223 // Verify cursor moved back to position after edit
8224 cx.assert_editor_state(indoc! {"
8225 line 1
8226 line 2
8227 linXˇe 3
8228 line 4
8229 line 5
8230 "});
8231}
8232
8233#[gpui::test]
8234async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8235 init_test(cx, |_| {});
8236
8237 let mut cx = EditorTestContext::new(cx).await;
8238
8239 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8240 cx.update_editor(|editor, window, cx| {
8241 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8242 });
8243
8244 cx.set_state(indoc! {"
8245 line 1
8246 line 2
8247 linˇe 3
8248 line 4
8249 line 5
8250 line 6
8251 line 7
8252 line 8
8253 line 9
8254 line 10
8255 "});
8256
8257 let snapshot = cx.buffer_snapshot();
8258 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8259
8260 cx.update(|_, cx| {
8261 provider.update(cx, |provider, _| {
8262 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
8263 id: None,
8264 edits: vec![(edit_position..edit_position, "X".into())],
8265 edit_preview: None,
8266 }))
8267 })
8268 });
8269
8270 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8271 cx.update_editor(|editor, window, cx| {
8272 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8273 });
8274
8275 cx.assert_editor_state(indoc! {"
8276 line 1
8277 line 2
8278 lineXˇ 3
8279 line 4
8280 line 5
8281 line 6
8282 line 7
8283 line 8
8284 line 9
8285 line 10
8286 "});
8287
8288 cx.update_editor(|editor, window, cx| {
8289 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8290 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8291 });
8292 });
8293
8294 cx.assert_editor_state(indoc! {"
8295 line 1
8296 line 2
8297 lineX 3
8298 line 4
8299 line 5
8300 line 6
8301 line 7
8302 line 8
8303 line 9
8304 liˇne 10
8305 "});
8306
8307 cx.update_editor(|editor, window, cx| {
8308 editor.undo(&Default::default(), window, cx);
8309 });
8310
8311 cx.assert_editor_state(indoc! {"
8312 line 1
8313 line 2
8314 lineˇ 3
8315 line 4
8316 line 5
8317 line 6
8318 line 7
8319 line 8
8320 line 9
8321 line 10
8322 "});
8323}
8324
8325#[gpui::test]
8326async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8327 init_test(cx, |_| {});
8328
8329 let mut cx = EditorTestContext::new(cx).await;
8330 cx.set_state(
8331 r#"let foo = 2;
8332lˇet foo = 2;
8333let fooˇ = 2;
8334let foo = 2;
8335let foo = ˇ2;"#,
8336 );
8337
8338 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8339 .unwrap();
8340 cx.assert_editor_state(
8341 r#"let foo = 2;
8342«letˇ» foo = 2;
8343let «fooˇ» = 2;
8344let foo = 2;
8345let foo = «2ˇ»;"#,
8346 );
8347
8348 // noop for multiple selections with different contents
8349 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8350 .unwrap();
8351 cx.assert_editor_state(
8352 r#"let foo = 2;
8353«letˇ» foo = 2;
8354let «fooˇ» = 2;
8355let foo = 2;
8356let foo = «2ˇ»;"#,
8357 );
8358
8359 // Test last selection direction should be preserved
8360 cx.set_state(
8361 r#"let foo = 2;
8362let foo = 2;
8363let «fooˇ» = 2;
8364let «ˇfoo» = 2;
8365let foo = 2;"#,
8366 );
8367
8368 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8369 .unwrap();
8370 cx.assert_editor_state(
8371 r#"let foo = 2;
8372let foo = 2;
8373let «fooˇ» = 2;
8374let «ˇfoo» = 2;
8375let «ˇfoo» = 2;"#,
8376 );
8377}
8378
8379#[gpui::test]
8380async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8381 init_test(cx, |_| {});
8382
8383 let mut cx =
8384 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8385
8386 cx.assert_editor_state(indoc! {"
8387 ˇbbb
8388 ccc
8389
8390 bbb
8391 ccc
8392 "});
8393 cx.dispatch_action(SelectPrevious::default());
8394 cx.assert_editor_state(indoc! {"
8395 «bbbˇ»
8396 ccc
8397
8398 bbb
8399 ccc
8400 "});
8401 cx.dispatch_action(SelectPrevious::default());
8402 cx.assert_editor_state(indoc! {"
8403 «bbbˇ»
8404 ccc
8405
8406 «bbbˇ»
8407 ccc
8408 "});
8409}
8410
8411#[gpui::test]
8412async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8413 init_test(cx, |_| {});
8414
8415 let mut cx = EditorTestContext::new(cx).await;
8416 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8417
8418 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8419 .unwrap();
8420 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8421
8422 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8423 .unwrap();
8424 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8425
8426 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8427 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8428
8429 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8430 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8431
8432 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8433 .unwrap();
8434 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8435
8436 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8437 .unwrap();
8438 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8439}
8440
8441#[gpui::test]
8442async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8443 init_test(cx, |_| {});
8444
8445 let mut cx = EditorTestContext::new(cx).await;
8446 cx.set_state("aˇ");
8447
8448 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8449 .unwrap();
8450 cx.assert_editor_state("«aˇ»");
8451 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8452 .unwrap();
8453 cx.assert_editor_state("«aˇ»");
8454}
8455
8456#[gpui::test]
8457async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8458 init_test(cx, |_| {});
8459
8460 let mut cx = EditorTestContext::new(cx).await;
8461 cx.set_state(
8462 r#"let foo = 2;
8463lˇet foo = 2;
8464let fooˇ = 2;
8465let foo = 2;
8466let foo = ˇ2;"#,
8467 );
8468
8469 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8470 .unwrap();
8471 cx.assert_editor_state(
8472 r#"let foo = 2;
8473«letˇ» foo = 2;
8474let «fooˇ» = 2;
8475let foo = 2;
8476let foo = «2ˇ»;"#,
8477 );
8478
8479 // noop for multiple selections with different contents
8480 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8481 .unwrap();
8482 cx.assert_editor_state(
8483 r#"let foo = 2;
8484«letˇ» foo = 2;
8485let «fooˇ» = 2;
8486let foo = 2;
8487let foo = «2ˇ»;"#,
8488 );
8489}
8490
8491#[gpui::test]
8492async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8493 init_test(cx, |_| {});
8494
8495 let mut cx = EditorTestContext::new(cx).await;
8496 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8497
8498 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8499 .unwrap();
8500 // selection direction is preserved
8501 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8502
8503 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8504 .unwrap();
8505 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8506
8507 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8508 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8509
8510 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8511 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8512
8513 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8514 .unwrap();
8515 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8516
8517 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8518 .unwrap();
8519 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8520}
8521
8522#[gpui::test]
8523async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8524 init_test(cx, |_| {});
8525
8526 let language = Arc::new(Language::new(
8527 LanguageConfig::default(),
8528 Some(tree_sitter_rust::LANGUAGE.into()),
8529 ));
8530
8531 let text = r#"
8532 use mod1::mod2::{mod3, mod4};
8533
8534 fn fn_1(param1: bool, param2: &str) {
8535 let var1 = "text";
8536 }
8537 "#
8538 .unindent();
8539
8540 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8541 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8542 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8543
8544 editor
8545 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8546 .await;
8547
8548 editor.update_in(cx, |editor, window, cx| {
8549 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8550 s.select_display_ranges([
8551 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8552 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8553 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8554 ]);
8555 });
8556 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8557 });
8558 editor.update(cx, |editor, cx| {
8559 assert_text_with_selections(
8560 editor,
8561 indoc! {r#"
8562 use mod1::mod2::{mod3, «mod4ˇ»};
8563
8564 fn fn_1«ˇ(param1: bool, param2: &str)» {
8565 let var1 = "«ˇtext»";
8566 }
8567 "#},
8568 cx,
8569 );
8570 });
8571
8572 editor.update_in(cx, |editor, window, cx| {
8573 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8574 });
8575 editor.update(cx, |editor, cx| {
8576 assert_text_with_selections(
8577 editor,
8578 indoc! {r#"
8579 use mod1::mod2::«{mod3, mod4}ˇ»;
8580
8581 «ˇfn fn_1(param1: bool, param2: &str) {
8582 let var1 = "text";
8583 }»
8584 "#},
8585 cx,
8586 );
8587 });
8588
8589 editor.update_in(cx, |editor, window, cx| {
8590 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8591 });
8592 assert_eq!(
8593 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8594 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8595 );
8596
8597 // Trying to expand the selected syntax node one more time has no effect.
8598 editor.update_in(cx, |editor, window, cx| {
8599 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8600 });
8601 assert_eq!(
8602 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8603 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8604 );
8605
8606 editor.update_in(cx, |editor, window, cx| {
8607 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8608 });
8609 editor.update(cx, |editor, cx| {
8610 assert_text_with_selections(
8611 editor,
8612 indoc! {r#"
8613 use mod1::mod2::«{mod3, mod4}ˇ»;
8614
8615 «ˇfn fn_1(param1: bool, param2: &str) {
8616 let var1 = "text";
8617 }»
8618 "#},
8619 cx,
8620 );
8621 });
8622
8623 editor.update_in(cx, |editor, window, cx| {
8624 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8625 });
8626 editor.update(cx, |editor, cx| {
8627 assert_text_with_selections(
8628 editor,
8629 indoc! {r#"
8630 use mod1::mod2::{mod3, «mod4ˇ»};
8631
8632 fn fn_1«ˇ(param1: bool, param2: &str)» {
8633 let var1 = "«ˇtext»";
8634 }
8635 "#},
8636 cx,
8637 );
8638 });
8639
8640 editor.update_in(cx, |editor, window, cx| {
8641 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8642 });
8643 editor.update(cx, |editor, cx| {
8644 assert_text_with_selections(
8645 editor,
8646 indoc! {r#"
8647 use mod1::mod2::{mod3, moˇd4};
8648
8649 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8650 let var1 = "teˇxt";
8651 }
8652 "#},
8653 cx,
8654 );
8655 });
8656
8657 // Trying to shrink the selected syntax node one more time has no effect.
8658 editor.update_in(cx, |editor, window, cx| {
8659 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8660 });
8661 editor.update_in(cx, |editor, _, cx| {
8662 assert_text_with_selections(
8663 editor,
8664 indoc! {r#"
8665 use mod1::mod2::{mod3, moˇd4};
8666
8667 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8668 let var1 = "teˇxt";
8669 }
8670 "#},
8671 cx,
8672 );
8673 });
8674
8675 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8676 // a fold.
8677 editor.update_in(cx, |editor, window, cx| {
8678 editor.fold_creases(
8679 vec![
8680 Crease::simple(
8681 Point::new(0, 21)..Point::new(0, 24),
8682 FoldPlaceholder::test(),
8683 ),
8684 Crease::simple(
8685 Point::new(3, 20)..Point::new(3, 22),
8686 FoldPlaceholder::test(),
8687 ),
8688 ],
8689 true,
8690 window,
8691 cx,
8692 );
8693 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8694 });
8695 editor.update(cx, |editor, cx| {
8696 assert_text_with_selections(
8697 editor,
8698 indoc! {r#"
8699 use mod1::mod2::«{mod3, mod4}ˇ»;
8700
8701 fn fn_1«ˇ(param1: bool, param2: &str)» {
8702 let var1 = "«ˇtext»";
8703 }
8704 "#},
8705 cx,
8706 );
8707 });
8708}
8709
8710#[gpui::test]
8711async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8712 init_test(cx, |_| {});
8713
8714 let language = Arc::new(Language::new(
8715 LanguageConfig::default(),
8716 Some(tree_sitter_rust::LANGUAGE.into()),
8717 ));
8718
8719 let text = "let a = 2;";
8720
8721 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8722 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8723 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8724
8725 editor
8726 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8727 .await;
8728
8729 // Test case 1: Cursor at end of word
8730 editor.update_in(cx, |editor, window, cx| {
8731 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8732 s.select_display_ranges([
8733 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8734 ]);
8735 });
8736 });
8737 editor.update(cx, |editor, cx| {
8738 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8739 });
8740 editor.update_in(cx, |editor, window, cx| {
8741 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8742 });
8743 editor.update(cx, |editor, cx| {
8744 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8745 });
8746 editor.update_in(cx, |editor, window, cx| {
8747 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8748 });
8749 editor.update(cx, |editor, cx| {
8750 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8751 });
8752
8753 // Test case 2: Cursor at end of statement
8754 editor.update_in(cx, |editor, window, cx| {
8755 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8756 s.select_display_ranges([
8757 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8758 ]);
8759 });
8760 });
8761 editor.update(cx, |editor, cx| {
8762 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8763 });
8764 editor.update_in(cx, |editor, window, cx| {
8765 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8766 });
8767 editor.update(cx, |editor, cx| {
8768 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8769 });
8770}
8771
8772#[gpui::test]
8773async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8774 init_test(cx, |_| {});
8775
8776 let language = Arc::new(Language::new(
8777 LanguageConfig {
8778 name: "JavaScript".into(),
8779 ..Default::default()
8780 },
8781 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8782 ));
8783
8784 let text = r#"
8785 let a = {
8786 key: "value",
8787 };
8788 "#
8789 .unindent();
8790
8791 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8792 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8793 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8794
8795 editor
8796 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8797 .await;
8798
8799 // Test case 1: Cursor after '{'
8800 editor.update_in(cx, |editor, window, cx| {
8801 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8802 s.select_display_ranges([
8803 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8804 ]);
8805 });
8806 });
8807 editor.update(cx, |editor, cx| {
8808 assert_text_with_selections(
8809 editor,
8810 indoc! {r#"
8811 let a = {ˇ
8812 key: "value",
8813 };
8814 "#},
8815 cx,
8816 );
8817 });
8818 editor.update_in(cx, |editor, window, cx| {
8819 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8820 });
8821 editor.update(cx, |editor, cx| {
8822 assert_text_with_selections(
8823 editor,
8824 indoc! {r#"
8825 let a = «ˇ{
8826 key: "value",
8827 }»;
8828 "#},
8829 cx,
8830 );
8831 });
8832
8833 // Test case 2: Cursor after ':'
8834 editor.update_in(cx, |editor, window, cx| {
8835 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8836 s.select_display_ranges([
8837 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8838 ]);
8839 });
8840 });
8841 editor.update(cx, |editor, cx| {
8842 assert_text_with_selections(
8843 editor,
8844 indoc! {r#"
8845 let a = {
8846 key:ˇ "value",
8847 };
8848 "#},
8849 cx,
8850 );
8851 });
8852 editor.update_in(cx, |editor, window, cx| {
8853 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8854 });
8855 editor.update(cx, |editor, cx| {
8856 assert_text_with_selections(
8857 editor,
8858 indoc! {r#"
8859 let a = {
8860 «ˇkey: "value"»,
8861 };
8862 "#},
8863 cx,
8864 );
8865 });
8866 editor.update_in(cx, |editor, window, cx| {
8867 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8868 });
8869 editor.update(cx, |editor, cx| {
8870 assert_text_with_selections(
8871 editor,
8872 indoc! {r#"
8873 let a = «ˇ{
8874 key: "value",
8875 }»;
8876 "#},
8877 cx,
8878 );
8879 });
8880
8881 // Test case 3: Cursor after ','
8882 editor.update_in(cx, |editor, window, cx| {
8883 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8884 s.select_display_ranges([
8885 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
8886 ]);
8887 });
8888 });
8889 editor.update(cx, |editor, cx| {
8890 assert_text_with_selections(
8891 editor,
8892 indoc! {r#"
8893 let a = {
8894 key: "value",ˇ
8895 };
8896 "#},
8897 cx,
8898 );
8899 });
8900 editor.update_in(cx, |editor, window, cx| {
8901 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8902 });
8903 editor.update(cx, |editor, cx| {
8904 assert_text_with_selections(
8905 editor,
8906 indoc! {r#"
8907 let a = «ˇ{
8908 key: "value",
8909 }»;
8910 "#},
8911 cx,
8912 );
8913 });
8914
8915 // Test case 4: Cursor after ';'
8916 editor.update_in(cx, |editor, window, cx| {
8917 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8918 s.select_display_ranges([
8919 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
8920 ]);
8921 });
8922 });
8923 editor.update(cx, |editor, cx| {
8924 assert_text_with_selections(
8925 editor,
8926 indoc! {r#"
8927 let a = {
8928 key: "value",
8929 };ˇ
8930 "#},
8931 cx,
8932 );
8933 });
8934 editor.update_in(cx, |editor, window, cx| {
8935 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8936 });
8937 editor.update(cx, |editor, cx| {
8938 assert_text_with_selections(
8939 editor,
8940 indoc! {r#"
8941 «ˇlet a = {
8942 key: "value",
8943 };
8944 »"#},
8945 cx,
8946 );
8947 });
8948}
8949
8950#[gpui::test]
8951async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8952 init_test(cx, |_| {});
8953
8954 let language = Arc::new(Language::new(
8955 LanguageConfig::default(),
8956 Some(tree_sitter_rust::LANGUAGE.into()),
8957 ));
8958
8959 let text = r#"
8960 use mod1::mod2::{mod3, mod4};
8961
8962 fn fn_1(param1: bool, param2: &str) {
8963 let var1 = "hello world";
8964 }
8965 "#
8966 .unindent();
8967
8968 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8969 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8970 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8971
8972 editor
8973 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8974 .await;
8975
8976 // Test 1: Cursor on a letter of a string word
8977 editor.update_in(cx, |editor, window, cx| {
8978 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8979 s.select_display_ranges([
8980 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8981 ]);
8982 });
8983 });
8984 editor.update_in(cx, |editor, window, cx| {
8985 assert_text_with_selections(
8986 editor,
8987 indoc! {r#"
8988 use mod1::mod2::{mod3, mod4};
8989
8990 fn fn_1(param1: bool, param2: &str) {
8991 let var1 = "hˇello world";
8992 }
8993 "#},
8994 cx,
8995 );
8996 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8997 assert_text_with_selections(
8998 editor,
8999 indoc! {r#"
9000 use mod1::mod2::{mod3, mod4};
9001
9002 fn fn_1(param1: bool, param2: &str) {
9003 let var1 = "«ˇhello» world";
9004 }
9005 "#},
9006 cx,
9007 );
9008 });
9009
9010 // Test 2: Partial selection within a word
9011 editor.update_in(cx, |editor, window, cx| {
9012 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9013 s.select_display_ranges([
9014 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9015 ]);
9016 });
9017 });
9018 editor.update_in(cx, |editor, window, cx| {
9019 assert_text_with_selections(
9020 editor,
9021 indoc! {r#"
9022 use mod1::mod2::{mod3, mod4};
9023
9024 fn fn_1(param1: bool, param2: &str) {
9025 let var1 = "h«elˇ»lo world";
9026 }
9027 "#},
9028 cx,
9029 );
9030 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9031 assert_text_with_selections(
9032 editor,
9033 indoc! {r#"
9034 use mod1::mod2::{mod3, mod4};
9035
9036 fn fn_1(param1: bool, param2: &str) {
9037 let var1 = "«ˇhello» world";
9038 }
9039 "#},
9040 cx,
9041 );
9042 });
9043
9044 // Test 3: Complete word already selected
9045 editor.update_in(cx, |editor, window, cx| {
9046 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9047 s.select_display_ranges([
9048 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9049 ]);
9050 });
9051 });
9052 editor.update_in(cx, |editor, window, cx| {
9053 assert_text_with_selections(
9054 editor,
9055 indoc! {r#"
9056 use mod1::mod2::{mod3, mod4};
9057
9058 fn fn_1(param1: bool, param2: &str) {
9059 let var1 = "«helloˇ» world";
9060 }
9061 "#},
9062 cx,
9063 );
9064 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9065 assert_text_with_selections(
9066 editor,
9067 indoc! {r#"
9068 use mod1::mod2::{mod3, mod4};
9069
9070 fn fn_1(param1: bool, param2: &str) {
9071 let var1 = "«hello worldˇ»";
9072 }
9073 "#},
9074 cx,
9075 );
9076 });
9077
9078 // Test 4: Selection spanning across words
9079 editor.update_in(cx, |editor, window, cx| {
9080 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9081 s.select_display_ranges([
9082 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9083 ]);
9084 });
9085 });
9086 editor.update_in(cx, |editor, window, cx| {
9087 assert_text_with_selections(
9088 editor,
9089 indoc! {r#"
9090 use mod1::mod2::{mod3, mod4};
9091
9092 fn fn_1(param1: bool, param2: &str) {
9093 let var1 = "hel«lo woˇ»rld";
9094 }
9095 "#},
9096 cx,
9097 );
9098 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9099 assert_text_with_selections(
9100 editor,
9101 indoc! {r#"
9102 use mod1::mod2::{mod3, mod4};
9103
9104 fn fn_1(param1: bool, param2: &str) {
9105 let var1 = "«ˇhello world»";
9106 }
9107 "#},
9108 cx,
9109 );
9110 });
9111
9112 // Test 5: Expansion beyond string
9113 editor.update_in(cx, |editor, window, cx| {
9114 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9115 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9116 assert_text_with_selections(
9117 editor,
9118 indoc! {r#"
9119 use mod1::mod2::{mod3, mod4};
9120
9121 fn fn_1(param1: bool, param2: &str) {
9122 «ˇlet var1 = "hello world";»
9123 }
9124 "#},
9125 cx,
9126 );
9127 });
9128}
9129
9130#[gpui::test]
9131async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9132 init_test(cx, |_| {});
9133
9134 let mut cx = EditorTestContext::new(cx).await;
9135
9136 let language = Arc::new(Language::new(
9137 LanguageConfig::default(),
9138 Some(tree_sitter_rust::LANGUAGE.into()),
9139 ));
9140
9141 cx.update_buffer(|buffer, cx| {
9142 buffer.set_language(Some(language), cx);
9143 });
9144
9145 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9146 cx.update_editor(|editor, window, cx| {
9147 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9148 });
9149
9150 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9151}
9152
9153#[gpui::test]
9154async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9155 init_test(cx, |_| {});
9156
9157 let base_text = r#"
9158 impl A {
9159 // this is an uncommitted comment
9160
9161 fn b() {
9162 c();
9163 }
9164
9165 // this is another uncommitted comment
9166
9167 fn d() {
9168 // e
9169 // f
9170 }
9171 }
9172
9173 fn g() {
9174 // h
9175 }
9176 "#
9177 .unindent();
9178
9179 let text = r#"
9180 ˇimpl A {
9181
9182 fn b() {
9183 c();
9184 }
9185
9186 fn d() {
9187 // e
9188 // f
9189 }
9190 }
9191
9192 fn g() {
9193 // h
9194 }
9195 "#
9196 .unindent();
9197
9198 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9199 cx.set_state(&text);
9200 cx.set_head_text(&base_text);
9201 cx.update_editor(|editor, window, cx| {
9202 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9203 });
9204
9205 cx.assert_state_with_diff(
9206 "
9207 ˇimpl A {
9208 - // this is an uncommitted comment
9209
9210 fn b() {
9211 c();
9212 }
9213
9214 - // this is another uncommitted comment
9215 -
9216 fn d() {
9217 // e
9218 // f
9219 }
9220 }
9221
9222 fn g() {
9223 // h
9224 }
9225 "
9226 .unindent(),
9227 );
9228
9229 let expected_display_text = "
9230 impl A {
9231 // this is an uncommitted comment
9232
9233 fn b() {
9234 ⋯
9235 }
9236
9237 // this is another uncommitted comment
9238
9239 fn d() {
9240 ⋯
9241 }
9242 }
9243
9244 fn g() {
9245 ⋯
9246 }
9247 "
9248 .unindent();
9249
9250 cx.update_editor(|editor, window, cx| {
9251 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9252 assert_eq!(editor.display_text(cx), expected_display_text);
9253 });
9254}
9255
9256#[gpui::test]
9257async fn test_autoindent(cx: &mut TestAppContext) {
9258 init_test(cx, |_| {});
9259
9260 let language = Arc::new(
9261 Language::new(
9262 LanguageConfig {
9263 brackets: BracketPairConfig {
9264 pairs: vec![
9265 BracketPair {
9266 start: "{".to_string(),
9267 end: "}".to_string(),
9268 close: false,
9269 surround: false,
9270 newline: true,
9271 },
9272 BracketPair {
9273 start: "(".to_string(),
9274 end: ")".to_string(),
9275 close: false,
9276 surround: false,
9277 newline: true,
9278 },
9279 ],
9280 ..Default::default()
9281 },
9282 ..Default::default()
9283 },
9284 Some(tree_sitter_rust::LANGUAGE.into()),
9285 )
9286 .with_indents_query(
9287 r#"
9288 (_ "(" ")" @end) @indent
9289 (_ "{" "}" @end) @indent
9290 "#,
9291 )
9292 .unwrap(),
9293 );
9294
9295 let text = "fn a() {}";
9296
9297 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9298 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9299 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9300 editor
9301 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9302 .await;
9303
9304 editor.update_in(cx, |editor, window, cx| {
9305 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9306 s.select_ranges([5..5, 8..8, 9..9])
9307 });
9308 editor.newline(&Newline, window, cx);
9309 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9310 assert_eq!(
9311 editor.selections.ranges(cx),
9312 &[
9313 Point::new(1, 4)..Point::new(1, 4),
9314 Point::new(3, 4)..Point::new(3, 4),
9315 Point::new(5, 0)..Point::new(5, 0)
9316 ]
9317 );
9318 });
9319}
9320
9321#[gpui::test]
9322async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9323 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9324
9325 let language = Arc::new(
9326 Language::new(
9327 LanguageConfig {
9328 brackets: BracketPairConfig {
9329 pairs: vec![
9330 BracketPair {
9331 start: "{".to_string(),
9332 end: "}".to_string(),
9333 close: false,
9334 surround: false,
9335 newline: true,
9336 },
9337 BracketPair {
9338 start: "(".to_string(),
9339 end: ")".to_string(),
9340 close: false,
9341 surround: false,
9342 newline: true,
9343 },
9344 ],
9345 ..Default::default()
9346 },
9347 ..Default::default()
9348 },
9349 Some(tree_sitter_rust::LANGUAGE.into()),
9350 )
9351 .with_indents_query(
9352 r#"
9353 (_ "(" ")" @end) @indent
9354 (_ "{" "}" @end) @indent
9355 "#,
9356 )
9357 .unwrap(),
9358 );
9359
9360 let text = "fn a() {}";
9361
9362 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9363 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9364 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9365 editor
9366 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9367 .await;
9368
9369 editor.update_in(cx, |editor, window, cx| {
9370 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9371 s.select_ranges([5..5, 8..8, 9..9])
9372 });
9373 editor.newline(&Newline, window, cx);
9374 assert_eq!(
9375 editor.text(cx),
9376 indoc!(
9377 "
9378 fn a(
9379
9380 ) {
9381
9382 }
9383 "
9384 )
9385 );
9386 assert_eq!(
9387 editor.selections.ranges(cx),
9388 &[
9389 Point::new(1, 0)..Point::new(1, 0),
9390 Point::new(3, 0)..Point::new(3, 0),
9391 Point::new(5, 0)..Point::new(5, 0)
9392 ]
9393 );
9394 });
9395}
9396
9397#[gpui::test]
9398async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9399 init_test(cx, |settings| {
9400 settings.defaults.auto_indent = Some(true);
9401 settings.languages.0.insert(
9402 "python".into(),
9403 LanguageSettingsContent {
9404 auto_indent: Some(false),
9405 ..Default::default()
9406 },
9407 );
9408 });
9409
9410 let mut cx = EditorTestContext::new(cx).await;
9411
9412 let injected_language = Arc::new(
9413 Language::new(
9414 LanguageConfig {
9415 brackets: BracketPairConfig {
9416 pairs: vec![
9417 BracketPair {
9418 start: "{".to_string(),
9419 end: "}".to_string(),
9420 close: false,
9421 surround: false,
9422 newline: true,
9423 },
9424 BracketPair {
9425 start: "(".to_string(),
9426 end: ")".to_string(),
9427 close: true,
9428 surround: false,
9429 newline: true,
9430 },
9431 ],
9432 ..Default::default()
9433 },
9434 name: "python".into(),
9435 ..Default::default()
9436 },
9437 Some(tree_sitter_python::LANGUAGE.into()),
9438 )
9439 .with_indents_query(
9440 r#"
9441 (_ "(" ")" @end) @indent
9442 (_ "{" "}" @end) @indent
9443 "#,
9444 )
9445 .unwrap(),
9446 );
9447
9448 let language = Arc::new(
9449 Language::new(
9450 LanguageConfig {
9451 brackets: BracketPairConfig {
9452 pairs: vec![
9453 BracketPair {
9454 start: "{".to_string(),
9455 end: "}".to_string(),
9456 close: false,
9457 surround: false,
9458 newline: true,
9459 },
9460 BracketPair {
9461 start: "(".to_string(),
9462 end: ")".to_string(),
9463 close: true,
9464 surround: false,
9465 newline: true,
9466 },
9467 ],
9468 ..Default::default()
9469 },
9470 name: LanguageName::new("rust"),
9471 ..Default::default()
9472 },
9473 Some(tree_sitter_rust::LANGUAGE.into()),
9474 )
9475 .with_indents_query(
9476 r#"
9477 (_ "(" ")" @end) @indent
9478 (_ "{" "}" @end) @indent
9479 "#,
9480 )
9481 .unwrap()
9482 .with_injection_query(
9483 r#"
9484 (macro_invocation
9485 macro: (identifier) @_macro_name
9486 (token_tree) @injection.content
9487 (#set! injection.language "python"))
9488 "#,
9489 )
9490 .unwrap(),
9491 );
9492
9493 cx.language_registry().add(injected_language);
9494 cx.language_registry().add(language.clone());
9495
9496 cx.update_buffer(|buffer, cx| {
9497 buffer.set_language(Some(language), cx);
9498 });
9499
9500 cx.set_state(r#"struct A {ˇ}"#);
9501
9502 cx.update_editor(|editor, window, cx| {
9503 editor.newline(&Default::default(), window, cx);
9504 });
9505
9506 cx.assert_editor_state(indoc!(
9507 "struct A {
9508 ˇ
9509 }"
9510 ));
9511
9512 cx.set_state(r#"select_biased!(ˇ)"#);
9513
9514 cx.update_editor(|editor, window, cx| {
9515 editor.newline(&Default::default(), window, cx);
9516 editor.handle_input("def ", window, cx);
9517 editor.handle_input("(", window, cx);
9518 editor.newline(&Default::default(), window, cx);
9519 editor.handle_input("a", window, cx);
9520 });
9521
9522 cx.assert_editor_state(indoc!(
9523 "select_biased!(
9524 def (
9525 aˇ
9526 )
9527 )"
9528 ));
9529}
9530
9531#[gpui::test]
9532async fn test_autoindent_selections(cx: &mut TestAppContext) {
9533 init_test(cx, |_| {});
9534
9535 {
9536 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9537 cx.set_state(indoc! {"
9538 impl A {
9539
9540 fn b() {}
9541
9542 «fn c() {
9543
9544 }ˇ»
9545 }
9546 "});
9547
9548 cx.update_editor(|editor, window, cx| {
9549 editor.autoindent(&Default::default(), window, cx);
9550 });
9551
9552 cx.assert_editor_state(indoc! {"
9553 impl A {
9554
9555 fn b() {}
9556
9557 «fn c() {
9558
9559 }ˇ»
9560 }
9561 "});
9562 }
9563
9564 {
9565 let mut cx = EditorTestContext::new_multibuffer(
9566 cx,
9567 [indoc! { "
9568 impl A {
9569 «
9570 // a
9571 fn b(){}
9572 »
9573 «
9574 }
9575 fn c(){}
9576 »
9577 "}],
9578 );
9579
9580 let buffer = cx.update_editor(|editor, _, cx| {
9581 let buffer = editor.buffer().update(cx, |buffer, _| {
9582 buffer.all_buffers().iter().next().unwrap().clone()
9583 });
9584 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9585 buffer
9586 });
9587
9588 cx.run_until_parked();
9589 cx.update_editor(|editor, window, cx| {
9590 editor.select_all(&Default::default(), window, cx);
9591 editor.autoindent(&Default::default(), window, cx)
9592 });
9593 cx.run_until_parked();
9594
9595 cx.update(|_, cx| {
9596 assert_eq!(
9597 buffer.read(cx).text(),
9598 indoc! { "
9599 impl A {
9600
9601 // a
9602 fn b(){}
9603
9604
9605 }
9606 fn c(){}
9607
9608 " }
9609 )
9610 });
9611 }
9612}
9613
9614#[gpui::test]
9615async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9616 init_test(cx, |_| {});
9617
9618 let mut cx = EditorTestContext::new(cx).await;
9619
9620 let language = Arc::new(Language::new(
9621 LanguageConfig {
9622 brackets: BracketPairConfig {
9623 pairs: vec![
9624 BracketPair {
9625 start: "{".to_string(),
9626 end: "}".to_string(),
9627 close: true,
9628 surround: true,
9629 newline: true,
9630 },
9631 BracketPair {
9632 start: "(".to_string(),
9633 end: ")".to_string(),
9634 close: true,
9635 surround: true,
9636 newline: true,
9637 },
9638 BracketPair {
9639 start: "/*".to_string(),
9640 end: " */".to_string(),
9641 close: true,
9642 surround: true,
9643 newline: true,
9644 },
9645 BracketPair {
9646 start: "[".to_string(),
9647 end: "]".to_string(),
9648 close: false,
9649 surround: false,
9650 newline: true,
9651 },
9652 BracketPair {
9653 start: "\"".to_string(),
9654 end: "\"".to_string(),
9655 close: true,
9656 surround: true,
9657 newline: false,
9658 },
9659 BracketPair {
9660 start: "<".to_string(),
9661 end: ">".to_string(),
9662 close: false,
9663 surround: true,
9664 newline: true,
9665 },
9666 ],
9667 ..Default::default()
9668 },
9669 autoclose_before: "})]".to_string(),
9670 ..Default::default()
9671 },
9672 Some(tree_sitter_rust::LANGUAGE.into()),
9673 ));
9674
9675 cx.language_registry().add(language.clone());
9676 cx.update_buffer(|buffer, cx| {
9677 buffer.set_language(Some(language), cx);
9678 });
9679
9680 cx.set_state(
9681 &r#"
9682 🏀ˇ
9683 εˇ
9684 ❤️ˇ
9685 "#
9686 .unindent(),
9687 );
9688
9689 // autoclose multiple nested brackets at multiple cursors
9690 cx.update_editor(|editor, window, cx| {
9691 editor.handle_input("{", window, cx);
9692 editor.handle_input("{", window, cx);
9693 editor.handle_input("{", window, cx);
9694 });
9695 cx.assert_editor_state(
9696 &"
9697 🏀{{{ˇ}}}
9698 ε{{{ˇ}}}
9699 ❤️{{{ˇ}}}
9700 "
9701 .unindent(),
9702 );
9703
9704 // insert a different closing bracket
9705 cx.update_editor(|editor, window, cx| {
9706 editor.handle_input(")", window, cx);
9707 });
9708 cx.assert_editor_state(
9709 &"
9710 🏀{{{)ˇ}}}
9711 ε{{{)ˇ}}}
9712 ❤️{{{)ˇ}}}
9713 "
9714 .unindent(),
9715 );
9716
9717 // skip over the auto-closed brackets when typing a closing bracket
9718 cx.update_editor(|editor, window, cx| {
9719 editor.move_right(&MoveRight, window, cx);
9720 editor.handle_input("}", window, cx);
9721 editor.handle_input("}", window, cx);
9722 editor.handle_input("}", window, cx);
9723 });
9724 cx.assert_editor_state(
9725 &"
9726 🏀{{{)}}}}ˇ
9727 ε{{{)}}}}ˇ
9728 ❤️{{{)}}}}ˇ
9729 "
9730 .unindent(),
9731 );
9732
9733 // autoclose multi-character pairs
9734 cx.set_state(
9735 &"
9736 ˇ
9737 ˇ
9738 "
9739 .unindent(),
9740 );
9741 cx.update_editor(|editor, window, cx| {
9742 editor.handle_input("/", window, cx);
9743 editor.handle_input("*", window, cx);
9744 });
9745 cx.assert_editor_state(
9746 &"
9747 /*ˇ */
9748 /*ˇ */
9749 "
9750 .unindent(),
9751 );
9752
9753 // one cursor autocloses a multi-character pair, one cursor
9754 // does not autoclose.
9755 cx.set_state(
9756 &"
9757 /ˇ
9758 ˇ
9759 "
9760 .unindent(),
9761 );
9762 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9763 cx.assert_editor_state(
9764 &"
9765 /*ˇ */
9766 *ˇ
9767 "
9768 .unindent(),
9769 );
9770
9771 // Don't autoclose if the next character isn't whitespace and isn't
9772 // listed in the language's "autoclose_before" section.
9773 cx.set_state("ˇa b");
9774 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9775 cx.assert_editor_state("{ˇa b");
9776
9777 // Don't autoclose if `close` is false for the bracket pair
9778 cx.set_state("ˇ");
9779 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9780 cx.assert_editor_state("[ˇ");
9781
9782 // Surround with brackets if text is selected
9783 cx.set_state("«aˇ» b");
9784 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9785 cx.assert_editor_state("{«aˇ»} b");
9786
9787 // Autoclose when not immediately after a word character
9788 cx.set_state("a ˇ");
9789 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9790 cx.assert_editor_state("a \"ˇ\"");
9791
9792 // Autoclose pair where the start and end characters are the same
9793 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9794 cx.assert_editor_state("a \"\"ˇ");
9795
9796 // Don't autoclose when immediately after a word character
9797 cx.set_state("aˇ");
9798 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9799 cx.assert_editor_state("a\"ˇ");
9800
9801 // Do autoclose when after a non-word character
9802 cx.set_state("{ˇ");
9803 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9804 cx.assert_editor_state("{\"ˇ\"");
9805
9806 // Non identical pairs autoclose regardless of preceding character
9807 cx.set_state("aˇ");
9808 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9809 cx.assert_editor_state("a{ˇ}");
9810
9811 // Don't autoclose pair if autoclose is disabled
9812 cx.set_state("ˇ");
9813 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9814 cx.assert_editor_state("<ˇ");
9815
9816 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9817 cx.set_state("«aˇ» b");
9818 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9819 cx.assert_editor_state("<«aˇ»> b");
9820}
9821
9822#[gpui::test]
9823async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9824 init_test(cx, |settings| {
9825 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9826 });
9827
9828 let mut cx = EditorTestContext::new(cx).await;
9829
9830 let language = Arc::new(Language::new(
9831 LanguageConfig {
9832 brackets: BracketPairConfig {
9833 pairs: vec![
9834 BracketPair {
9835 start: "{".to_string(),
9836 end: "}".to_string(),
9837 close: true,
9838 surround: true,
9839 newline: true,
9840 },
9841 BracketPair {
9842 start: "(".to_string(),
9843 end: ")".to_string(),
9844 close: true,
9845 surround: true,
9846 newline: true,
9847 },
9848 BracketPair {
9849 start: "[".to_string(),
9850 end: "]".to_string(),
9851 close: false,
9852 surround: false,
9853 newline: true,
9854 },
9855 ],
9856 ..Default::default()
9857 },
9858 autoclose_before: "})]".to_string(),
9859 ..Default::default()
9860 },
9861 Some(tree_sitter_rust::LANGUAGE.into()),
9862 ));
9863
9864 cx.language_registry().add(language.clone());
9865 cx.update_buffer(|buffer, cx| {
9866 buffer.set_language(Some(language), cx);
9867 });
9868
9869 cx.set_state(
9870 &"
9871 ˇ
9872 ˇ
9873 ˇ
9874 "
9875 .unindent(),
9876 );
9877
9878 // ensure only matching closing brackets are skipped over
9879 cx.update_editor(|editor, window, cx| {
9880 editor.handle_input("}", window, cx);
9881 editor.move_left(&MoveLeft, window, cx);
9882 editor.handle_input(")", window, cx);
9883 editor.move_left(&MoveLeft, window, cx);
9884 });
9885 cx.assert_editor_state(
9886 &"
9887 ˇ)}
9888 ˇ)}
9889 ˇ)}
9890 "
9891 .unindent(),
9892 );
9893
9894 // skip-over closing brackets at multiple cursors
9895 cx.update_editor(|editor, window, cx| {
9896 editor.handle_input(")", window, cx);
9897 editor.handle_input("}", window, cx);
9898 });
9899 cx.assert_editor_state(
9900 &"
9901 )}ˇ
9902 )}ˇ
9903 )}ˇ
9904 "
9905 .unindent(),
9906 );
9907
9908 // ignore non-close brackets
9909 cx.update_editor(|editor, window, cx| {
9910 editor.handle_input("]", window, cx);
9911 editor.move_left(&MoveLeft, window, cx);
9912 editor.handle_input("]", window, cx);
9913 });
9914 cx.assert_editor_state(
9915 &"
9916 )}]ˇ]
9917 )}]ˇ]
9918 )}]ˇ]
9919 "
9920 .unindent(),
9921 );
9922}
9923
9924#[gpui::test]
9925async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9926 init_test(cx, |_| {});
9927
9928 let mut cx = EditorTestContext::new(cx).await;
9929
9930 let html_language = Arc::new(
9931 Language::new(
9932 LanguageConfig {
9933 name: "HTML".into(),
9934 brackets: BracketPairConfig {
9935 pairs: vec![
9936 BracketPair {
9937 start: "<".into(),
9938 end: ">".into(),
9939 close: true,
9940 ..Default::default()
9941 },
9942 BracketPair {
9943 start: "{".into(),
9944 end: "}".into(),
9945 close: true,
9946 ..Default::default()
9947 },
9948 BracketPair {
9949 start: "(".into(),
9950 end: ")".into(),
9951 close: true,
9952 ..Default::default()
9953 },
9954 ],
9955 ..Default::default()
9956 },
9957 autoclose_before: "})]>".into(),
9958 ..Default::default()
9959 },
9960 Some(tree_sitter_html::LANGUAGE.into()),
9961 )
9962 .with_injection_query(
9963 r#"
9964 (script_element
9965 (raw_text) @injection.content
9966 (#set! injection.language "javascript"))
9967 "#,
9968 )
9969 .unwrap(),
9970 );
9971
9972 let javascript_language = Arc::new(Language::new(
9973 LanguageConfig {
9974 name: "JavaScript".into(),
9975 brackets: BracketPairConfig {
9976 pairs: vec![
9977 BracketPair {
9978 start: "/*".into(),
9979 end: " */".into(),
9980 close: true,
9981 ..Default::default()
9982 },
9983 BracketPair {
9984 start: "{".into(),
9985 end: "}".into(),
9986 close: true,
9987 ..Default::default()
9988 },
9989 BracketPair {
9990 start: "(".into(),
9991 end: ")".into(),
9992 close: true,
9993 ..Default::default()
9994 },
9995 ],
9996 ..Default::default()
9997 },
9998 autoclose_before: "})]>".into(),
9999 ..Default::default()
10000 },
10001 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10002 ));
10003
10004 cx.language_registry().add(html_language.clone());
10005 cx.language_registry().add(javascript_language);
10006 cx.executor().run_until_parked();
10007
10008 cx.update_buffer(|buffer, cx| {
10009 buffer.set_language(Some(html_language), cx);
10010 });
10011
10012 cx.set_state(
10013 &r#"
10014 <body>ˇ
10015 <script>
10016 var x = 1;ˇ
10017 </script>
10018 </body>ˇ
10019 "#
10020 .unindent(),
10021 );
10022
10023 // Precondition: different languages are active at different locations.
10024 cx.update_editor(|editor, window, cx| {
10025 let snapshot = editor.snapshot(window, cx);
10026 let cursors = editor.selections.ranges::<usize>(cx);
10027 let languages = cursors
10028 .iter()
10029 .map(|c| snapshot.language_at(c.start).unwrap().name())
10030 .collect::<Vec<_>>();
10031 assert_eq!(
10032 languages,
10033 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10034 );
10035 });
10036
10037 // Angle brackets autoclose in HTML, but not JavaScript.
10038 cx.update_editor(|editor, window, cx| {
10039 editor.handle_input("<", window, cx);
10040 editor.handle_input("a", window, cx);
10041 });
10042 cx.assert_editor_state(
10043 &r#"
10044 <body><aˇ>
10045 <script>
10046 var x = 1;<aˇ
10047 </script>
10048 </body><aˇ>
10049 "#
10050 .unindent(),
10051 );
10052
10053 // Curly braces and parens autoclose in both HTML and JavaScript.
10054 cx.update_editor(|editor, window, cx| {
10055 editor.handle_input(" b=", window, cx);
10056 editor.handle_input("{", window, cx);
10057 editor.handle_input("c", window, cx);
10058 editor.handle_input("(", window, cx);
10059 });
10060 cx.assert_editor_state(
10061 &r#"
10062 <body><a b={c(ˇ)}>
10063 <script>
10064 var x = 1;<a b={c(ˇ)}
10065 </script>
10066 </body><a b={c(ˇ)}>
10067 "#
10068 .unindent(),
10069 );
10070
10071 // Brackets that were already autoclosed are skipped.
10072 cx.update_editor(|editor, window, cx| {
10073 editor.handle_input(")", window, cx);
10074 editor.handle_input("d", window, cx);
10075 editor.handle_input("}", window, cx);
10076 });
10077 cx.assert_editor_state(
10078 &r#"
10079 <body><a b={c()d}ˇ>
10080 <script>
10081 var x = 1;<a b={c()d}ˇ
10082 </script>
10083 </body><a b={c()d}ˇ>
10084 "#
10085 .unindent(),
10086 );
10087 cx.update_editor(|editor, window, cx| {
10088 editor.handle_input(">", window, cx);
10089 });
10090 cx.assert_editor_state(
10091 &r#"
10092 <body><a b={c()d}>ˇ
10093 <script>
10094 var x = 1;<a b={c()d}>ˇ
10095 </script>
10096 </body><a b={c()d}>ˇ
10097 "#
10098 .unindent(),
10099 );
10100
10101 // Reset
10102 cx.set_state(
10103 &r#"
10104 <body>ˇ
10105 <script>
10106 var x = 1;ˇ
10107 </script>
10108 </body>ˇ
10109 "#
10110 .unindent(),
10111 );
10112
10113 cx.update_editor(|editor, window, cx| {
10114 editor.handle_input("<", window, cx);
10115 });
10116 cx.assert_editor_state(
10117 &r#"
10118 <body><ˇ>
10119 <script>
10120 var x = 1;<ˇ
10121 </script>
10122 </body><ˇ>
10123 "#
10124 .unindent(),
10125 );
10126
10127 // When backspacing, the closing angle brackets are removed.
10128 cx.update_editor(|editor, window, cx| {
10129 editor.backspace(&Backspace, window, cx);
10130 });
10131 cx.assert_editor_state(
10132 &r#"
10133 <body>ˇ
10134 <script>
10135 var x = 1;ˇ
10136 </script>
10137 </body>ˇ
10138 "#
10139 .unindent(),
10140 );
10141
10142 // Block comments autoclose in JavaScript, but not HTML.
10143 cx.update_editor(|editor, window, cx| {
10144 editor.handle_input("/", window, cx);
10145 editor.handle_input("*", window, cx);
10146 });
10147 cx.assert_editor_state(
10148 &r#"
10149 <body>/*ˇ
10150 <script>
10151 var x = 1;/*ˇ */
10152 </script>
10153 </body>/*ˇ
10154 "#
10155 .unindent(),
10156 );
10157}
10158
10159#[gpui::test]
10160async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10161 init_test(cx, |_| {});
10162
10163 let mut cx = EditorTestContext::new(cx).await;
10164
10165 let rust_language = Arc::new(
10166 Language::new(
10167 LanguageConfig {
10168 name: "Rust".into(),
10169 brackets: serde_json::from_value(json!([
10170 { "start": "{", "end": "}", "close": true, "newline": true },
10171 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10172 ]))
10173 .unwrap(),
10174 autoclose_before: "})]>".into(),
10175 ..Default::default()
10176 },
10177 Some(tree_sitter_rust::LANGUAGE.into()),
10178 )
10179 .with_override_query("(string_literal) @string")
10180 .unwrap(),
10181 );
10182
10183 cx.language_registry().add(rust_language.clone());
10184 cx.update_buffer(|buffer, cx| {
10185 buffer.set_language(Some(rust_language), cx);
10186 });
10187
10188 cx.set_state(
10189 &r#"
10190 let x = ˇ
10191 "#
10192 .unindent(),
10193 );
10194
10195 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10196 cx.update_editor(|editor, window, cx| {
10197 editor.handle_input("\"", window, cx);
10198 });
10199 cx.assert_editor_state(
10200 &r#"
10201 let x = "ˇ"
10202 "#
10203 .unindent(),
10204 );
10205
10206 // Inserting another quotation mark. The cursor moves across the existing
10207 // automatically-inserted quotation mark.
10208 cx.update_editor(|editor, window, cx| {
10209 editor.handle_input("\"", window, cx);
10210 });
10211 cx.assert_editor_state(
10212 &r#"
10213 let x = ""ˇ
10214 "#
10215 .unindent(),
10216 );
10217
10218 // Reset
10219 cx.set_state(
10220 &r#"
10221 let x = ˇ
10222 "#
10223 .unindent(),
10224 );
10225
10226 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10227 cx.update_editor(|editor, window, cx| {
10228 editor.handle_input("\"", window, cx);
10229 editor.handle_input(" ", window, cx);
10230 editor.move_left(&Default::default(), window, cx);
10231 editor.handle_input("\\", window, cx);
10232 editor.handle_input("\"", window, cx);
10233 });
10234 cx.assert_editor_state(
10235 &r#"
10236 let x = "\"ˇ "
10237 "#
10238 .unindent(),
10239 );
10240
10241 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10242 // mark. Nothing is inserted.
10243 cx.update_editor(|editor, window, cx| {
10244 editor.move_right(&Default::default(), window, cx);
10245 editor.handle_input("\"", window, cx);
10246 });
10247 cx.assert_editor_state(
10248 &r#"
10249 let x = "\" "ˇ
10250 "#
10251 .unindent(),
10252 );
10253}
10254
10255#[gpui::test]
10256async fn test_surround_with_pair(cx: &mut TestAppContext) {
10257 init_test(cx, |_| {});
10258
10259 let language = Arc::new(Language::new(
10260 LanguageConfig {
10261 brackets: BracketPairConfig {
10262 pairs: vec![
10263 BracketPair {
10264 start: "{".to_string(),
10265 end: "}".to_string(),
10266 close: true,
10267 surround: true,
10268 newline: true,
10269 },
10270 BracketPair {
10271 start: "/* ".to_string(),
10272 end: "*/".to_string(),
10273 close: true,
10274 surround: true,
10275 ..Default::default()
10276 },
10277 ],
10278 ..Default::default()
10279 },
10280 ..Default::default()
10281 },
10282 Some(tree_sitter_rust::LANGUAGE.into()),
10283 ));
10284
10285 let text = r#"
10286 a
10287 b
10288 c
10289 "#
10290 .unindent();
10291
10292 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10293 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10294 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10295 editor
10296 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10297 .await;
10298
10299 editor.update_in(cx, |editor, window, cx| {
10300 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10301 s.select_display_ranges([
10302 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10303 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10304 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10305 ])
10306 });
10307
10308 editor.handle_input("{", window, cx);
10309 editor.handle_input("{", window, cx);
10310 editor.handle_input("{", window, cx);
10311 assert_eq!(
10312 editor.text(cx),
10313 "
10314 {{{a}}}
10315 {{{b}}}
10316 {{{c}}}
10317 "
10318 .unindent()
10319 );
10320 assert_eq!(
10321 editor.selections.display_ranges(cx),
10322 [
10323 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10324 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10325 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10326 ]
10327 );
10328
10329 editor.undo(&Undo, window, cx);
10330 editor.undo(&Undo, window, cx);
10331 editor.undo(&Undo, window, cx);
10332 assert_eq!(
10333 editor.text(cx),
10334 "
10335 a
10336 b
10337 c
10338 "
10339 .unindent()
10340 );
10341 assert_eq!(
10342 editor.selections.display_ranges(cx),
10343 [
10344 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10345 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10346 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10347 ]
10348 );
10349
10350 // Ensure inserting the first character of a multi-byte bracket pair
10351 // doesn't surround the selections with the bracket.
10352 editor.handle_input("/", window, cx);
10353 assert_eq!(
10354 editor.text(cx),
10355 "
10356 /
10357 /
10358 /
10359 "
10360 .unindent()
10361 );
10362 assert_eq!(
10363 editor.selections.display_ranges(cx),
10364 [
10365 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10366 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10367 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10368 ]
10369 );
10370
10371 editor.undo(&Undo, window, cx);
10372 assert_eq!(
10373 editor.text(cx),
10374 "
10375 a
10376 b
10377 c
10378 "
10379 .unindent()
10380 );
10381 assert_eq!(
10382 editor.selections.display_ranges(cx),
10383 [
10384 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10385 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10386 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10387 ]
10388 );
10389
10390 // Ensure inserting the last character of a multi-byte bracket pair
10391 // doesn't surround the selections with the bracket.
10392 editor.handle_input("*", window, cx);
10393 assert_eq!(
10394 editor.text(cx),
10395 "
10396 *
10397 *
10398 *
10399 "
10400 .unindent()
10401 );
10402 assert_eq!(
10403 editor.selections.display_ranges(cx),
10404 [
10405 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10406 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10407 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10408 ]
10409 );
10410 });
10411}
10412
10413#[gpui::test]
10414async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10415 init_test(cx, |_| {});
10416
10417 let language = Arc::new(Language::new(
10418 LanguageConfig {
10419 brackets: BracketPairConfig {
10420 pairs: vec![BracketPair {
10421 start: "{".to_string(),
10422 end: "}".to_string(),
10423 close: true,
10424 surround: true,
10425 newline: true,
10426 }],
10427 ..Default::default()
10428 },
10429 autoclose_before: "}".to_string(),
10430 ..Default::default()
10431 },
10432 Some(tree_sitter_rust::LANGUAGE.into()),
10433 ));
10434
10435 let text = r#"
10436 a
10437 b
10438 c
10439 "#
10440 .unindent();
10441
10442 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10443 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10444 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10445 editor
10446 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10447 .await;
10448
10449 editor.update_in(cx, |editor, window, cx| {
10450 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10451 s.select_ranges([
10452 Point::new(0, 1)..Point::new(0, 1),
10453 Point::new(1, 1)..Point::new(1, 1),
10454 Point::new(2, 1)..Point::new(2, 1),
10455 ])
10456 });
10457
10458 editor.handle_input("{", window, cx);
10459 editor.handle_input("{", window, cx);
10460 editor.handle_input("_", window, cx);
10461 assert_eq!(
10462 editor.text(cx),
10463 "
10464 a{{_}}
10465 b{{_}}
10466 c{{_}}
10467 "
10468 .unindent()
10469 );
10470 assert_eq!(
10471 editor.selections.ranges::<Point>(cx),
10472 [
10473 Point::new(0, 4)..Point::new(0, 4),
10474 Point::new(1, 4)..Point::new(1, 4),
10475 Point::new(2, 4)..Point::new(2, 4)
10476 ]
10477 );
10478
10479 editor.backspace(&Default::default(), window, cx);
10480 editor.backspace(&Default::default(), window, cx);
10481 assert_eq!(
10482 editor.text(cx),
10483 "
10484 a{}
10485 b{}
10486 c{}
10487 "
10488 .unindent()
10489 );
10490 assert_eq!(
10491 editor.selections.ranges::<Point>(cx),
10492 [
10493 Point::new(0, 2)..Point::new(0, 2),
10494 Point::new(1, 2)..Point::new(1, 2),
10495 Point::new(2, 2)..Point::new(2, 2)
10496 ]
10497 );
10498
10499 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10500 assert_eq!(
10501 editor.text(cx),
10502 "
10503 a
10504 b
10505 c
10506 "
10507 .unindent()
10508 );
10509 assert_eq!(
10510 editor.selections.ranges::<Point>(cx),
10511 [
10512 Point::new(0, 1)..Point::new(0, 1),
10513 Point::new(1, 1)..Point::new(1, 1),
10514 Point::new(2, 1)..Point::new(2, 1)
10515 ]
10516 );
10517 });
10518}
10519
10520#[gpui::test]
10521async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10522 init_test(cx, |settings| {
10523 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10524 });
10525
10526 let mut cx = EditorTestContext::new(cx).await;
10527
10528 let language = Arc::new(Language::new(
10529 LanguageConfig {
10530 brackets: BracketPairConfig {
10531 pairs: vec![
10532 BracketPair {
10533 start: "{".to_string(),
10534 end: "}".to_string(),
10535 close: true,
10536 surround: true,
10537 newline: true,
10538 },
10539 BracketPair {
10540 start: "(".to_string(),
10541 end: ")".to_string(),
10542 close: true,
10543 surround: true,
10544 newline: true,
10545 },
10546 BracketPair {
10547 start: "[".to_string(),
10548 end: "]".to_string(),
10549 close: false,
10550 surround: true,
10551 newline: true,
10552 },
10553 ],
10554 ..Default::default()
10555 },
10556 autoclose_before: "})]".to_string(),
10557 ..Default::default()
10558 },
10559 Some(tree_sitter_rust::LANGUAGE.into()),
10560 ));
10561
10562 cx.language_registry().add(language.clone());
10563 cx.update_buffer(|buffer, cx| {
10564 buffer.set_language(Some(language), cx);
10565 });
10566
10567 cx.set_state(
10568 &"
10569 {(ˇ)}
10570 [[ˇ]]
10571 {(ˇ)}
10572 "
10573 .unindent(),
10574 );
10575
10576 cx.update_editor(|editor, window, cx| {
10577 editor.backspace(&Default::default(), window, cx);
10578 editor.backspace(&Default::default(), window, cx);
10579 });
10580
10581 cx.assert_editor_state(
10582 &"
10583 ˇ
10584 ˇ]]
10585 ˇ
10586 "
10587 .unindent(),
10588 );
10589
10590 cx.update_editor(|editor, window, cx| {
10591 editor.handle_input("{", window, cx);
10592 editor.handle_input("{", window, cx);
10593 editor.move_right(&MoveRight, window, cx);
10594 editor.move_right(&MoveRight, window, cx);
10595 editor.move_left(&MoveLeft, window, cx);
10596 editor.move_left(&MoveLeft, window, cx);
10597 editor.backspace(&Default::default(), window, cx);
10598 });
10599
10600 cx.assert_editor_state(
10601 &"
10602 {ˇ}
10603 {ˇ}]]
10604 {ˇ}
10605 "
10606 .unindent(),
10607 );
10608
10609 cx.update_editor(|editor, window, cx| {
10610 editor.backspace(&Default::default(), window, cx);
10611 });
10612
10613 cx.assert_editor_state(
10614 &"
10615 ˇ
10616 ˇ]]
10617 ˇ
10618 "
10619 .unindent(),
10620 );
10621}
10622
10623#[gpui::test]
10624async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10625 init_test(cx, |_| {});
10626
10627 let language = Arc::new(Language::new(
10628 LanguageConfig::default(),
10629 Some(tree_sitter_rust::LANGUAGE.into()),
10630 ));
10631
10632 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10633 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10634 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10635 editor
10636 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10637 .await;
10638
10639 editor.update_in(cx, |editor, window, cx| {
10640 editor.set_auto_replace_emoji_shortcode(true);
10641
10642 editor.handle_input("Hello ", window, cx);
10643 editor.handle_input(":wave", window, cx);
10644 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10645
10646 editor.handle_input(":", window, cx);
10647 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10648
10649 editor.handle_input(" :smile", window, cx);
10650 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10651
10652 editor.handle_input(":", window, cx);
10653 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10654
10655 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10656 editor.handle_input(":wave", window, cx);
10657 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10658
10659 editor.handle_input(":", window, cx);
10660 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10661
10662 editor.handle_input(":1", window, cx);
10663 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10664
10665 editor.handle_input(":", window, cx);
10666 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10667
10668 // Ensure shortcode does not get replaced when it is part of a word
10669 editor.handle_input(" Test:wave", window, cx);
10670 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10671
10672 editor.handle_input(":", window, cx);
10673 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10674
10675 editor.set_auto_replace_emoji_shortcode(false);
10676
10677 // Ensure shortcode does not get replaced when auto replace is off
10678 editor.handle_input(" :wave", window, cx);
10679 assert_eq!(
10680 editor.text(cx),
10681 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10682 );
10683
10684 editor.handle_input(":", window, cx);
10685 assert_eq!(
10686 editor.text(cx),
10687 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10688 );
10689 });
10690}
10691
10692#[gpui::test]
10693async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10694 init_test(cx, |_| {});
10695
10696 let (text, insertion_ranges) = marked_text_ranges(
10697 indoc! {"
10698 ˇ
10699 "},
10700 false,
10701 );
10702
10703 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10704 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10705
10706 _ = editor.update_in(cx, |editor, window, cx| {
10707 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10708
10709 editor
10710 .insert_snippet(&insertion_ranges, snippet, window, cx)
10711 .unwrap();
10712
10713 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10714 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10715 assert_eq!(editor.text(cx), expected_text);
10716 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10717 }
10718
10719 assert(
10720 editor,
10721 cx,
10722 indoc! {"
10723 type «» =•
10724 "},
10725 );
10726
10727 assert!(editor.context_menu_visible(), "There should be a matches");
10728 });
10729}
10730
10731#[gpui::test]
10732async fn test_snippets(cx: &mut TestAppContext) {
10733 init_test(cx, |_| {});
10734
10735 let mut cx = EditorTestContext::new(cx).await;
10736
10737 cx.set_state(indoc! {"
10738 a.ˇ b
10739 a.ˇ b
10740 a.ˇ b
10741 "});
10742
10743 cx.update_editor(|editor, window, cx| {
10744 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10745 let insertion_ranges = editor
10746 .selections
10747 .all(cx)
10748 .iter()
10749 .map(|s| s.range())
10750 .collect::<Vec<_>>();
10751 editor
10752 .insert_snippet(&insertion_ranges, snippet, window, cx)
10753 .unwrap();
10754 });
10755
10756 cx.assert_editor_state(indoc! {"
10757 a.f(«oneˇ», two, «threeˇ») b
10758 a.f(«oneˇ», two, «threeˇ») b
10759 a.f(«oneˇ», two, «threeˇ») b
10760 "});
10761
10762 // Can't move earlier than the first tab stop
10763 cx.update_editor(|editor, window, cx| {
10764 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10765 });
10766 cx.assert_editor_state(indoc! {"
10767 a.f(«oneˇ», two, «threeˇ») b
10768 a.f(«oneˇ», two, «threeˇ») b
10769 a.f(«oneˇ», two, «threeˇ») b
10770 "});
10771
10772 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10773 cx.assert_editor_state(indoc! {"
10774 a.f(one, «twoˇ», three) b
10775 a.f(one, «twoˇ», three) b
10776 a.f(one, «twoˇ», three) b
10777 "});
10778
10779 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10780 cx.assert_editor_state(indoc! {"
10781 a.f(«oneˇ», two, «threeˇ») b
10782 a.f(«oneˇ», two, «threeˇ») b
10783 a.f(«oneˇ», two, «threeˇ») b
10784 "});
10785
10786 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10787 cx.assert_editor_state(indoc! {"
10788 a.f(one, «twoˇ», three) b
10789 a.f(one, «twoˇ», three) b
10790 a.f(one, «twoˇ», three) b
10791 "});
10792 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10793 cx.assert_editor_state(indoc! {"
10794 a.f(one, two, three)ˇ b
10795 a.f(one, two, three)ˇ b
10796 a.f(one, two, three)ˇ b
10797 "});
10798
10799 // As soon as the last tab stop is reached, snippet state is gone
10800 cx.update_editor(|editor, window, cx| {
10801 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10802 });
10803 cx.assert_editor_state(indoc! {"
10804 a.f(one, two, three)ˇ b
10805 a.f(one, two, three)ˇ b
10806 a.f(one, two, three)ˇ b
10807 "});
10808}
10809
10810#[gpui::test]
10811async fn test_snippet_indentation(cx: &mut TestAppContext) {
10812 init_test(cx, |_| {});
10813
10814 let mut cx = EditorTestContext::new(cx).await;
10815
10816 cx.update_editor(|editor, window, cx| {
10817 let snippet = Snippet::parse(indoc! {"
10818 /*
10819 * Multiline comment with leading indentation
10820 *
10821 * $1
10822 */
10823 $0"})
10824 .unwrap();
10825 let insertion_ranges = editor
10826 .selections
10827 .all(cx)
10828 .iter()
10829 .map(|s| s.range())
10830 .collect::<Vec<_>>();
10831 editor
10832 .insert_snippet(&insertion_ranges, snippet, window, cx)
10833 .unwrap();
10834 });
10835
10836 cx.assert_editor_state(indoc! {"
10837 /*
10838 * Multiline comment with leading indentation
10839 *
10840 * ˇ
10841 */
10842 "});
10843
10844 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10845 cx.assert_editor_state(indoc! {"
10846 /*
10847 * Multiline comment with leading indentation
10848 *
10849 *•
10850 */
10851 ˇ"});
10852}
10853
10854#[gpui::test]
10855async fn test_document_format_during_save(cx: &mut TestAppContext) {
10856 init_test(cx, |_| {});
10857
10858 let fs = FakeFs::new(cx.executor());
10859 fs.insert_file(path!("/file.rs"), Default::default()).await;
10860
10861 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10862
10863 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10864 language_registry.add(rust_lang());
10865 let mut fake_servers = language_registry.register_fake_lsp(
10866 "Rust",
10867 FakeLspAdapter {
10868 capabilities: lsp::ServerCapabilities {
10869 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10870 ..Default::default()
10871 },
10872 ..Default::default()
10873 },
10874 );
10875
10876 let buffer = project
10877 .update(cx, |project, cx| {
10878 project.open_local_buffer(path!("/file.rs"), cx)
10879 })
10880 .await
10881 .unwrap();
10882
10883 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10884 let (editor, cx) = cx.add_window_view(|window, cx| {
10885 build_editor_with_project(project.clone(), buffer, window, cx)
10886 });
10887 editor.update_in(cx, |editor, window, cx| {
10888 editor.set_text("one\ntwo\nthree\n", window, cx)
10889 });
10890 assert!(cx.read(|cx| editor.is_dirty(cx)));
10891
10892 cx.executor().start_waiting();
10893 let fake_server = fake_servers.next().await.unwrap();
10894
10895 {
10896 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10897 move |params, _| async move {
10898 assert_eq!(
10899 params.text_document.uri,
10900 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10901 );
10902 assert_eq!(params.options.tab_size, 4);
10903 Ok(Some(vec![lsp::TextEdit::new(
10904 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10905 ", ".to_string(),
10906 )]))
10907 },
10908 );
10909 let save = editor
10910 .update_in(cx, |editor, window, cx| {
10911 editor.save(
10912 SaveOptions {
10913 format: true,
10914 autosave: false,
10915 },
10916 project.clone(),
10917 window,
10918 cx,
10919 )
10920 })
10921 .unwrap();
10922 cx.executor().start_waiting();
10923 save.await;
10924
10925 assert_eq!(
10926 editor.update(cx, |editor, cx| editor.text(cx)),
10927 "one, two\nthree\n"
10928 );
10929 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10930 }
10931
10932 {
10933 editor.update_in(cx, |editor, window, cx| {
10934 editor.set_text("one\ntwo\nthree\n", window, cx)
10935 });
10936 assert!(cx.read(|cx| editor.is_dirty(cx)));
10937
10938 // Ensure we can still save even if formatting hangs.
10939 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10940 move |params, _| async move {
10941 assert_eq!(
10942 params.text_document.uri,
10943 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10944 );
10945 futures::future::pending::<()>().await;
10946 unreachable!()
10947 },
10948 );
10949 let save = editor
10950 .update_in(cx, |editor, window, cx| {
10951 editor.save(
10952 SaveOptions {
10953 format: true,
10954 autosave: false,
10955 },
10956 project.clone(),
10957 window,
10958 cx,
10959 )
10960 })
10961 .unwrap();
10962 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
10963 cx.executor().start_waiting();
10964 save.await;
10965 assert_eq!(
10966 editor.update(cx, |editor, cx| editor.text(cx)),
10967 "one\ntwo\nthree\n"
10968 );
10969 }
10970
10971 // Set rust language override and assert overridden tabsize is sent to language server
10972 update_test_language_settings(cx, |settings| {
10973 settings.languages.0.insert(
10974 "Rust".into(),
10975 LanguageSettingsContent {
10976 tab_size: NonZeroU32::new(8),
10977 ..Default::default()
10978 },
10979 );
10980 });
10981
10982 {
10983 editor.update_in(cx, |editor, window, cx| {
10984 editor.set_text("somehting_new\n", window, cx)
10985 });
10986 assert!(cx.read(|cx| editor.is_dirty(cx)));
10987 let _formatting_request_signal = fake_server
10988 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
10989 assert_eq!(
10990 params.text_document.uri,
10991 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10992 );
10993 assert_eq!(params.options.tab_size, 8);
10994 Ok(Some(vec![]))
10995 });
10996 let save = editor
10997 .update_in(cx, |editor, window, cx| {
10998 editor.save(
10999 SaveOptions {
11000 format: true,
11001 autosave: false,
11002 },
11003 project.clone(),
11004 window,
11005 cx,
11006 )
11007 })
11008 .unwrap();
11009 cx.executor().start_waiting();
11010 save.await;
11011 }
11012}
11013
11014#[gpui::test]
11015async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11016 init_test(cx, |settings| {
11017 settings.defaults.ensure_final_newline_on_save = Some(false);
11018 });
11019
11020 let fs = FakeFs::new(cx.executor());
11021 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11022
11023 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11024
11025 let buffer = project
11026 .update(cx, |project, cx| {
11027 project.open_local_buffer(path!("/file.txt"), cx)
11028 })
11029 .await
11030 .unwrap();
11031
11032 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11033 let (editor, cx) = cx.add_window_view(|window, cx| {
11034 build_editor_with_project(project.clone(), buffer, window, cx)
11035 });
11036 editor.update_in(cx, |editor, window, cx| {
11037 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11038 s.select_ranges([0..0])
11039 });
11040 });
11041 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11042
11043 editor.update_in(cx, |editor, window, cx| {
11044 editor.handle_input("\n", window, cx)
11045 });
11046 cx.run_until_parked();
11047 save(&editor, &project, cx).await;
11048 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11049
11050 editor.update_in(cx, |editor, window, cx| {
11051 editor.undo(&Default::default(), window, cx);
11052 });
11053 save(&editor, &project, cx).await;
11054 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11055
11056 editor.update_in(cx, |editor, window, cx| {
11057 editor.redo(&Default::default(), window, cx);
11058 });
11059 cx.run_until_parked();
11060 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11061
11062 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11063 let save = editor
11064 .update_in(cx, |editor, window, cx| {
11065 editor.save(
11066 SaveOptions {
11067 format: true,
11068 autosave: false,
11069 },
11070 project.clone(),
11071 window,
11072 cx,
11073 )
11074 })
11075 .unwrap();
11076 cx.executor().start_waiting();
11077 save.await;
11078 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11079 }
11080}
11081
11082#[gpui::test]
11083async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11084 init_test(cx, |_| {});
11085
11086 let cols = 4;
11087 let rows = 10;
11088 let sample_text_1 = sample_text(rows, cols, 'a');
11089 assert_eq!(
11090 sample_text_1,
11091 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11092 );
11093 let sample_text_2 = sample_text(rows, cols, 'l');
11094 assert_eq!(
11095 sample_text_2,
11096 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11097 );
11098 let sample_text_3 = sample_text(rows, cols, 'v');
11099 assert_eq!(
11100 sample_text_3,
11101 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11102 );
11103
11104 let fs = FakeFs::new(cx.executor());
11105 fs.insert_tree(
11106 path!("/a"),
11107 json!({
11108 "main.rs": sample_text_1,
11109 "other.rs": sample_text_2,
11110 "lib.rs": sample_text_3,
11111 }),
11112 )
11113 .await;
11114
11115 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11116 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11117 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11118
11119 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11120 language_registry.add(rust_lang());
11121 let mut fake_servers = language_registry.register_fake_lsp(
11122 "Rust",
11123 FakeLspAdapter {
11124 capabilities: lsp::ServerCapabilities {
11125 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11126 ..Default::default()
11127 },
11128 ..Default::default()
11129 },
11130 );
11131
11132 let worktree = project.update(cx, |project, cx| {
11133 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11134 assert_eq!(worktrees.len(), 1);
11135 worktrees.pop().unwrap()
11136 });
11137 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11138
11139 let buffer_1 = project
11140 .update(cx, |project, cx| {
11141 project.open_buffer((worktree_id, "main.rs"), cx)
11142 })
11143 .await
11144 .unwrap();
11145 let buffer_2 = project
11146 .update(cx, |project, cx| {
11147 project.open_buffer((worktree_id, "other.rs"), cx)
11148 })
11149 .await
11150 .unwrap();
11151 let buffer_3 = project
11152 .update(cx, |project, cx| {
11153 project.open_buffer((worktree_id, "lib.rs"), cx)
11154 })
11155 .await
11156 .unwrap();
11157
11158 let multi_buffer = cx.new(|cx| {
11159 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11160 multi_buffer.push_excerpts(
11161 buffer_1.clone(),
11162 [
11163 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11164 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11165 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11166 ],
11167 cx,
11168 );
11169 multi_buffer.push_excerpts(
11170 buffer_2.clone(),
11171 [
11172 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11173 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11174 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11175 ],
11176 cx,
11177 );
11178 multi_buffer.push_excerpts(
11179 buffer_3.clone(),
11180 [
11181 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11182 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11183 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11184 ],
11185 cx,
11186 );
11187 multi_buffer
11188 });
11189 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11190 Editor::new(
11191 EditorMode::full(),
11192 multi_buffer,
11193 Some(project.clone()),
11194 window,
11195 cx,
11196 )
11197 });
11198
11199 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11200 editor.change_selections(
11201 SelectionEffects::scroll(Autoscroll::Next),
11202 window,
11203 cx,
11204 |s| s.select_ranges(Some(1..2)),
11205 );
11206 editor.insert("|one|two|three|", window, cx);
11207 });
11208 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11209 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11210 editor.change_selections(
11211 SelectionEffects::scroll(Autoscroll::Next),
11212 window,
11213 cx,
11214 |s| s.select_ranges(Some(60..70)),
11215 );
11216 editor.insert("|four|five|six|", window, cx);
11217 });
11218 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11219
11220 // First two buffers should be edited, but not the third one.
11221 assert_eq!(
11222 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11223 "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}",
11224 );
11225 buffer_1.update(cx, |buffer, _| {
11226 assert!(buffer.is_dirty());
11227 assert_eq!(
11228 buffer.text(),
11229 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11230 )
11231 });
11232 buffer_2.update(cx, |buffer, _| {
11233 assert!(buffer.is_dirty());
11234 assert_eq!(
11235 buffer.text(),
11236 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11237 )
11238 });
11239 buffer_3.update(cx, |buffer, _| {
11240 assert!(!buffer.is_dirty());
11241 assert_eq!(buffer.text(), sample_text_3,)
11242 });
11243 cx.executor().run_until_parked();
11244
11245 cx.executor().start_waiting();
11246 let save = multi_buffer_editor
11247 .update_in(cx, |editor, window, cx| {
11248 editor.save(
11249 SaveOptions {
11250 format: true,
11251 autosave: false,
11252 },
11253 project.clone(),
11254 window,
11255 cx,
11256 )
11257 })
11258 .unwrap();
11259
11260 let fake_server = fake_servers.next().await.unwrap();
11261 fake_server
11262 .server
11263 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11264 Ok(Some(vec![lsp::TextEdit::new(
11265 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11266 format!("[{} formatted]", params.text_document.uri),
11267 )]))
11268 })
11269 .detach();
11270 save.await;
11271
11272 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11273 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11274 assert_eq!(
11275 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11276 uri!(
11277 "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}"
11278 ),
11279 );
11280 buffer_1.update(cx, |buffer, _| {
11281 assert!(!buffer.is_dirty());
11282 assert_eq!(
11283 buffer.text(),
11284 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11285 )
11286 });
11287 buffer_2.update(cx, |buffer, _| {
11288 assert!(!buffer.is_dirty());
11289 assert_eq!(
11290 buffer.text(),
11291 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11292 )
11293 });
11294 buffer_3.update(cx, |buffer, _| {
11295 assert!(!buffer.is_dirty());
11296 assert_eq!(buffer.text(), sample_text_3,)
11297 });
11298}
11299
11300#[gpui::test]
11301async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11302 init_test(cx, |_| {});
11303
11304 let fs = FakeFs::new(cx.executor());
11305 fs.insert_tree(
11306 path!("/dir"),
11307 json!({
11308 "file1.rs": "fn main() { println!(\"hello\"); }",
11309 "file2.rs": "fn test() { println!(\"test\"); }",
11310 "file3.rs": "fn other() { println!(\"other\"); }\n",
11311 }),
11312 )
11313 .await;
11314
11315 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11316 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11317 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11318
11319 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11320 language_registry.add(rust_lang());
11321
11322 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11323 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11324
11325 // Open three buffers
11326 let buffer_1 = project
11327 .update(cx, |project, cx| {
11328 project.open_buffer((worktree_id, "file1.rs"), cx)
11329 })
11330 .await
11331 .unwrap();
11332 let buffer_2 = project
11333 .update(cx, |project, cx| {
11334 project.open_buffer((worktree_id, "file2.rs"), cx)
11335 })
11336 .await
11337 .unwrap();
11338 let buffer_3 = project
11339 .update(cx, |project, cx| {
11340 project.open_buffer((worktree_id, "file3.rs"), cx)
11341 })
11342 .await
11343 .unwrap();
11344
11345 // Create a multi-buffer with all three buffers
11346 let multi_buffer = cx.new(|cx| {
11347 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11348 multi_buffer.push_excerpts(
11349 buffer_1.clone(),
11350 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11351 cx,
11352 );
11353 multi_buffer.push_excerpts(
11354 buffer_2.clone(),
11355 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11356 cx,
11357 );
11358 multi_buffer.push_excerpts(
11359 buffer_3.clone(),
11360 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11361 cx,
11362 );
11363 multi_buffer
11364 });
11365
11366 let editor = cx.new_window_entity(|window, cx| {
11367 Editor::new(
11368 EditorMode::full(),
11369 multi_buffer,
11370 Some(project.clone()),
11371 window,
11372 cx,
11373 )
11374 });
11375
11376 // Edit only the first buffer
11377 editor.update_in(cx, |editor, window, cx| {
11378 editor.change_selections(
11379 SelectionEffects::scroll(Autoscroll::Next),
11380 window,
11381 cx,
11382 |s| s.select_ranges(Some(10..10)),
11383 );
11384 editor.insert("// edited", window, cx);
11385 });
11386
11387 // Verify that only buffer 1 is dirty
11388 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11389 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11390 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11391
11392 // Get write counts after file creation (files were created with initial content)
11393 // We expect each file to have been written once during creation
11394 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11395 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11396 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11397
11398 // Perform autosave
11399 let save_task = editor.update_in(cx, |editor, window, cx| {
11400 editor.save(
11401 SaveOptions {
11402 format: true,
11403 autosave: true,
11404 },
11405 project.clone(),
11406 window,
11407 cx,
11408 )
11409 });
11410 save_task.await.unwrap();
11411
11412 // Only the dirty buffer should have been saved
11413 assert_eq!(
11414 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11415 1,
11416 "Buffer 1 was dirty, so it should have been written once during autosave"
11417 );
11418 assert_eq!(
11419 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11420 0,
11421 "Buffer 2 was clean, so it should not have been written during autosave"
11422 );
11423 assert_eq!(
11424 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11425 0,
11426 "Buffer 3 was clean, so it should not have been written during autosave"
11427 );
11428
11429 // Verify buffer states after autosave
11430 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11431 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11432 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11433
11434 // Now perform a manual save (format = true)
11435 let save_task = editor.update_in(cx, |editor, window, cx| {
11436 editor.save(
11437 SaveOptions {
11438 format: true,
11439 autosave: false,
11440 },
11441 project.clone(),
11442 window,
11443 cx,
11444 )
11445 });
11446 save_task.await.unwrap();
11447
11448 // During manual save, clean buffers don't get written to disk
11449 // They just get did_save called for language server notifications
11450 assert_eq!(
11451 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11452 1,
11453 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11454 );
11455 assert_eq!(
11456 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11457 0,
11458 "Buffer 2 should not have been written at all"
11459 );
11460 assert_eq!(
11461 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11462 0,
11463 "Buffer 3 should not have been written at all"
11464 );
11465}
11466
11467async fn setup_range_format_test(
11468 cx: &mut TestAppContext,
11469) -> (
11470 Entity<Project>,
11471 Entity<Editor>,
11472 &mut gpui::VisualTestContext,
11473 lsp::FakeLanguageServer,
11474) {
11475 init_test(cx, |_| {});
11476
11477 let fs = FakeFs::new(cx.executor());
11478 fs.insert_file(path!("/file.rs"), Default::default()).await;
11479
11480 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11481
11482 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11483 language_registry.add(rust_lang());
11484 let mut fake_servers = language_registry.register_fake_lsp(
11485 "Rust",
11486 FakeLspAdapter {
11487 capabilities: lsp::ServerCapabilities {
11488 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11489 ..lsp::ServerCapabilities::default()
11490 },
11491 ..FakeLspAdapter::default()
11492 },
11493 );
11494
11495 let buffer = project
11496 .update(cx, |project, cx| {
11497 project.open_local_buffer(path!("/file.rs"), cx)
11498 })
11499 .await
11500 .unwrap();
11501
11502 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11503 let (editor, cx) = cx.add_window_view(|window, cx| {
11504 build_editor_with_project(project.clone(), buffer, window, cx)
11505 });
11506
11507 cx.executor().start_waiting();
11508 let fake_server = fake_servers.next().await.unwrap();
11509
11510 (project, editor, cx, fake_server)
11511}
11512
11513#[gpui::test]
11514async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11515 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11516
11517 editor.update_in(cx, |editor, window, cx| {
11518 editor.set_text("one\ntwo\nthree\n", window, cx)
11519 });
11520 assert!(cx.read(|cx| editor.is_dirty(cx)));
11521
11522 let save = editor
11523 .update_in(cx, |editor, window, cx| {
11524 editor.save(
11525 SaveOptions {
11526 format: true,
11527 autosave: false,
11528 },
11529 project.clone(),
11530 window,
11531 cx,
11532 )
11533 })
11534 .unwrap();
11535 fake_server
11536 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11537 assert_eq!(
11538 params.text_document.uri,
11539 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11540 );
11541 assert_eq!(params.options.tab_size, 4);
11542 Ok(Some(vec![lsp::TextEdit::new(
11543 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11544 ", ".to_string(),
11545 )]))
11546 })
11547 .next()
11548 .await;
11549 cx.executor().start_waiting();
11550 save.await;
11551 assert_eq!(
11552 editor.update(cx, |editor, cx| editor.text(cx)),
11553 "one, two\nthree\n"
11554 );
11555 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11556}
11557
11558#[gpui::test]
11559async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11560 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11561
11562 editor.update_in(cx, |editor, window, cx| {
11563 editor.set_text("one\ntwo\nthree\n", window, cx)
11564 });
11565 assert!(cx.read(|cx| editor.is_dirty(cx)));
11566
11567 // Test that save still works when formatting hangs
11568 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11569 move |params, _| async move {
11570 assert_eq!(
11571 params.text_document.uri,
11572 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11573 );
11574 futures::future::pending::<()>().await;
11575 unreachable!()
11576 },
11577 );
11578 let save = editor
11579 .update_in(cx, |editor, window, cx| {
11580 editor.save(
11581 SaveOptions {
11582 format: true,
11583 autosave: false,
11584 },
11585 project.clone(),
11586 window,
11587 cx,
11588 )
11589 })
11590 .unwrap();
11591 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11592 cx.executor().start_waiting();
11593 save.await;
11594 assert_eq!(
11595 editor.update(cx, |editor, cx| editor.text(cx)),
11596 "one\ntwo\nthree\n"
11597 );
11598 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11599}
11600
11601#[gpui::test]
11602async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11603 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11604
11605 // Buffer starts clean, no formatting should be requested
11606 let save = editor
11607 .update_in(cx, |editor, window, cx| {
11608 editor.save(
11609 SaveOptions {
11610 format: false,
11611 autosave: false,
11612 },
11613 project.clone(),
11614 window,
11615 cx,
11616 )
11617 })
11618 .unwrap();
11619 let _pending_format_request = fake_server
11620 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11621 panic!("Should not be invoked");
11622 })
11623 .next();
11624 cx.executor().start_waiting();
11625 save.await;
11626 cx.run_until_parked();
11627}
11628
11629#[gpui::test]
11630async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11631 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11632
11633 // Set Rust language override and assert overridden tabsize is sent to language server
11634 update_test_language_settings(cx, |settings| {
11635 settings.languages.0.insert(
11636 "Rust".into(),
11637 LanguageSettingsContent {
11638 tab_size: NonZeroU32::new(8),
11639 ..Default::default()
11640 },
11641 );
11642 });
11643
11644 editor.update_in(cx, |editor, window, cx| {
11645 editor.set_text("something_new\n", window, cx)
11646 });
11647 assert!(cx.read(|cx| editor.is_dirty(cx)));
11648 let save = editor
11649 .update_in(cx, |editor, window, cx| {
11650 editor.save(
11651 SaveOptions {
11652 format: true,
11653 autosave: false,
11654 },
11655 project.clone(),
11656 window,
11657 cx,
11658 )
11659 })
11660 .unwrap();
11661 fake_server
11662 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11663 assert_eq!(
11664 params.text_document.uri,
11665 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11666 );
11667 assert_eq!(params.options.tab_size, 8);
11668 Ok(Some(Vec::new()))
11669 })
11670 .next()
11671 .await;
11672 save.await;
11673}
11674
11675#[gpui::test]
11676async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11677 init_test(cx, |settings| {
11678 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11679 Formatter::LanguageServer { name: None },
11680 )))
11681 });
11682
11683 let fs = FakeFs::new(cx.executor());
11684 fs.insert_file(path!("/file.rs"), Default::default()).await;
11685
11686 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11687
11688 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11689 language_registry.add(Arc::new(Language::new(
11690 LanguageConfig {
11691 name: "Rust".into(),
11692 matcher: LanguageMatcher {
11693 path_suffixes: vec!["rs".to_string()],
11694 ..Default::default()
11695 },
11696 ..LanguageConfig::default()
11697 },
11698 Some(tree_sitter_rust::LANGUAGE.into()),
11699 )));
11700 update_test_language_settings(cx, |settings| {
11701 // Enable Prettier formatting for the same buffer, and ensure
11702 // LSP is called instead of Prettier.
11703 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11704 });
11705 let mut fake_servers = language_registry.register_fake_lsp(
11706 "Rust",
11707 FakeLspAdapter {
11708 capabilities: lsp::ServerCapabilities {
11709 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11710 ..Default::default()
11711 },
11712 ..Default::default()
11713 },
11714 );
11715
11716 let buffer = project
11717 .update(cx, |project, cx| {
11718 project.open_local_buffer(path!("/file.rs"), cx)
11719 })
11720 .await
11721 .unwrap();
11722
11723 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11724 let (editor, cx) = cx.add_window_view(|window, cx| {
11725 build_editor_with_project(project.clone(), buffer, window, cx)
11726 });
11727 editor.update_in(cx, |editor, window, cx| {
11728 editor.set_text("one\ntwo\nthree\n", window, cx)
11729 });
11730
11731 cx.executor().start_waiting();
11732 let fake_server = fake_servers.next().await.unwrap();
11733
11734 let format = editor
11735 .update_in(cx, |editor, window, cx| {
11736 editor.perform_format(
11737 project.clone(),
11738 FormatTrigger::Manual,
11739 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11740 window,
11741 cx,
11742 )
11743 })
11744 .unwrap();
11745 fake_server
11746 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11747 assert_eq!(
11748 params.text_document.uri,
11749 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11750 );
11751 assert_eq!(params.options.tab_size, 4);
11752 Ok(Some(vec![lsp::TextEdit::new(
11753 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11754 ", ".to_string(),
11755 )]))
11756 })
11757 .next()
11758 .await;
11759 cx.executor().start_waiting();
11760 format.await;
11761 assert_eq!(
11762 editor.update(cx, |editor, cx| editor.text(cx)),
11763 "one, two\nthree\n"
11764 );
11765
11766 editor.update_in(cx, |editor, window, cx| {
11767 editor.set_text("one\ntwo\nthree\n", window, cx)
11768 });
11769 // Ensure we don't lock if formatting hangs.
11770 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11771 move |params, _| async move {
11772 assert_eq!(
11773 params.text_document.uri,
11774 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11775 );
11776 futures::future::pending::<()>().await;
11777 unreachable!()
11778 },
11779 );
11780 let format = editor
11781 .update_in(cx, |editor, window, cx| {
11782 editor.perform_format(
11783 project,
11784 FormatTrigger::Manual,
11785 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11786 window,
11787 cx,
11788 )
11789 })
11790 .unwrap();
11791 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11792 cx.executor().start_waiting();
11793 format.await;
11794 assert_eq!(
11795 editor.update(cx, |editor, cx| editor.text(cx)),
11796 "one\ntwo\nthree\n"
11797 );
11798}
11799
11800#[gpui::test]
11801async fn test_multiple_formatters(cx: &mut TestAppContext) {
11802 init_test(cx, |settings| {
11803 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11804 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11805 Formatter::LanguageServer { name: None },
11806 Formatter::CodeActions(
11807 [
11808 ("code-action-1".into(), true),
11809 ("code-action-2".into(), true),
11810 ]
11811 .into_iter()
11812 .collect(),
11813 ),
11814 ])))
11815 });
11816
11817 let fs = FakeFs::new(cx.executor());
11818 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11819 .await;
11820
11821 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11822 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11823 language_registry.add(rust_lang());
11824
11825 let mut fake_servers = language_registry.register_fake_lsp(
11826 "Rust",
11827 FakeLspAdapter {
11828 capabilities: lsp::ServerCapabilities {
11829 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11830 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11831 commands: vec!["the-command-for-code-action-1".into()],
11832 ..Default::default()
11833 }),
11834 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11835 ..Default::default()
11836 },
11837 ..Default::default()
11838 },
11839 );
11840
11841 let buffer = project
11842 .update(cx, |project, cx| {
11843 project.open_local_buffer(path!("/file.rs"), cx)
11844 })
11845 .await
11846 .unwrap();
11847
11848 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11849 let (editor, cx) = cx.add_window_view(|window, cx| {
11850 build_editor_with_project(project.clone(), buffer, window, cx)
11851 });
11852
11853 cx.executor().start_waiting();
11854
11855 let fake_server = fake_servers.next().await.unwrap();
11856 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11857 move |_params, _| async move {
11858 Ok(Some(vec![lsp::TextEdit::new(
11859 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11860 "applied-formatting\n".to_string(),
11861 )]))
11862 },
11863 );
11864 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11865 move |params, _| async move {
11866 assert_eq!(
11867 params.context.only,
11868 Some(vec!["code-action-1".into(), "code-action-2".into()])
11869 );
11870 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11871 Ok(Some(vec![
11872 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11873 kind: Some("code-action-1".into()),
11874 edit: Some(lsp::WorkspaceEdit::new(
11875 [(
11876 uri.clone(),
11877 vec![lsp::TextEdit::new(
11878 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11879 "applied-code-action-1-edit\n".to_string(),
11880 )],
11881 )]
11882 .into_iter()
11883 .collect(),
11884 )),
11885 command: Some(lsp::Command {
11886 command: "the-command-for-code-action-1".into(),
11887 ..Default::default()
11888 }),
11889 ..Default::default()
11890 }),
11891 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11892 kind: Some("code-action-2".into()),
11893 edit: Some(lsp::WorkspaceEdit::new(
11894 [(
11895 uri,
11896 vec![lsp::TextEdit::new(
11897 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11898 "applied-code-action-2-edit\n".to_string(),
11899 )],
11900 )]
11901 .into_iter()
11902 .collect(),
11903 )),
11904 ..Default::default()
11905 }),
11906 ]))
11907 },
11908 );
11909
11910 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11911 move |params, _| async move { Ok(params) }
11912 });
11913
11914 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11915 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11916 let fake = fake_server.clone();
11917 let lock = command_lock.clone();
11918 move |params, _| {
11919 assert_eq!(params.command, "the-command-for-code-action-1");
11920 let fake = fake.clone();
11921 let lock = lock.clone();
11922 async move {
11923 lock.lock().await;
11924 fake.server
11925 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11926 label: None,
11927 edit: lsp::WorkspaceEdit {
11928 changes: Some(
11929 [(
11930 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11931 vec![lsp::TextEdit {
11932 range: lsp::Range::new(
11933 lsp::Position::new(0, 0),
11934 lsp::Position::new(0, 0),
11935 ),
11936 new_text: "applied-code-action-1-command\n".into(),
11937 }],
11938 )]
11939 .into_iter()
11940 .collect(),
11941 ),
11942 ..Default::default()
11943 },
11944 })
11945 .await
11946 .into_response()
11947 .unwrap();
11948 Ok(Some(json!(null)))
11949 }
11950 }
11951 });
11952
11953 cx.executor().start_waiting();
11954 editor
11955 .update_in(cx, |editor, window, cx| {
11956 editor.perform_format(
11957 project.clone(),
11958 FormatTrigger::Manual,
11959 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11960 window,
11961 cx,
11962 )
11963 })
11964 .unwrap()
11965 .await;
11966 editor.update(cx, |editor, cx| {
11967 assert_eq!(
11968 editor.text(cx),
11969 r#"
11970 applied-code-action-2-edit
11971 applied-code-action-1-command
11972 applied-code-action-1-edit
11973 applied-formatting
11974 one
11975 two
11976 three
11977 "#
11978 .unindent()
11979 );
11980 });
11981
11982 editor.update_in(cx, |editor, window, cx| {
11983 editor.undo(&Default::default(), window, cx);
11984 assert_eq!(editor.text(cx), "one \ntwo \nthree");
11985 });
11986
11987 // Perform a manual edit while waiting for an LSP command
11988 // that's being run as part of a formatting code action.
11989 let lock_guard = command_lock.lock().await;
11990 let format = editor
11991 .update_in(cx, |editor, window, cx| {
11992 editor.perform_format(
11993 project.clone(),
11994 FormatTrigger::Manual,
11995 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11996 window,
11997 cx,
11998 )
11999 })
12000 .unwrap();
12001 cx.run_until_parked();
12002 editor.update(cx, |editor, cx| {
12003 assert_eq!(
12004 editor.text(cx),
12005 r#"
12006 applied-code-action-1-edit
12007 applied-formatting
12008 one
12009 two
12010 three
12011 "#
12012 .unindent()
12013 );
12014
12015 editor.buffer.update(cx, |buffer, cx| {
12016 let ix = buffer.len(cx);
12017 buffer.edit([(ix..ix, "edited\n")], None, cx);
12018 });
12019 });
12020
12021 // Allow the LSP command to proceed. Because the buffer was edited,
12022 // the second code action will not be run.
12023 drop(lock_guard);
12024 format.await;
12025 editor.update_in(cx, |editor, window, cx| {
12026 assert_eq!(
12027 editor.text(cx),
12028 r#"
12029 applied-code-action-1-command
12030 applied-code-action-1-edit
12031 applied-formatting
12032 one
12033 two
12034 three
12035 edited
12036 "#
12037 .unindent()
12038 );
12039
12040 // The manual edit is undone first, because it is the last thing the user did
12041 // (even though the command completed afterwards).
12042 editor.undo(&Default::default(), window, cx);
12043 assert_eq!(
12044 editor.text(cx),
12045 r#"
12046 applied-code-action-1-command
12047 applied-code-action-1-edit
12048 applied-formatting
12049 one
12050 two
12051 three
12052 "#
12053 .unindent()
12054 );
12055
12056 // All the formatting (including the command, which completed after the manual edit)
12057 // is undone together.
12058 editor.undo(&Default::default(), window, cx);
12059 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12060 });
12061}
12062
12063#[gpui::test]
12064async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12065 init_test(cx, |settings| {
12066 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12067 Formatter::LanguageServer { name: None },
12068 ])))
12069 });
12070
12071 let fs = FakeFs::new(cx.executor());
12072 fs.insert_file(path!("/file.ts"), Default::default()).await;
12073
12074 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12075
12076 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12077 language_registry.add(Arc::new(Language::new(
12078 LanguageConfig {
12079 name: "TypeScript".into(),
12080 matcher: LanguageMatcher {
12081 path_suffixes: vec!["ts".to_string()],
12082 ..Default::default()
12083 },
12084 ..LanguageConfig::default()
12085 },
12086 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12087 )));
12088 update_test_language_settings(cx, |settings| {
12089 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12090 });
12091 let mut fake_servers = language_registry.register_fake_lsp(
12092 "TypeScript",
12093 FakeLspAdapter {
12094 capabilities: lsp::ServerCapabilities {
12095 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12096 ..Default::default()
12097 },
12098 ..Default::default()
12099 },
12100 );
12101
12102 let buffer = project
12103 .update(cx, |project, cx| {
12104 project.open_local_buffer(path!("/file.ts"), cx)
12105 })
12106 .await
12107 .unwrap();
12108
12109 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12110 let (editor, cx) = cx.add_window_view(|window, cx| {
12111 build_editor_with_project(project.clone(), buffer, window, cx)
12112 });
12113 editor.update_in(cx, |editor, window, cx| {
12114 editor.set_text(
12115 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12116 window,
12117 cx,
12118 )
12119 });
12120
12121 cx.executor().start_waiting();
12122 let fake_server = fake_servers.next().await.unwrap();
12123
12124 let format = editor
12125 .update_in(cx, |editor, window, cx| {
12126 editor.perform_code_action_kind(
12127 project.clone(),
12128 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12129 window,
12130 cx,
12131 )
12132 })
12133 .unwrap();
12134 fake_server
12135 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12136 assert_eq!(
12137 params.text_document.uri,
12138 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12139 );
12140 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12141 lsp::CodeAction {
12142 title: "Organize Imports".to_string(),
12143 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12144 edit: Some(lsp::WorkspaceEdit {
12145 changes: Some(
12146 [(
12147 params.text_document.uri.clone(),
12148 vec![lsp::TextEdit::new(
12149 lsp::Range::new(
12150 lsp::Position::new(1, 0),
12151 lsp::Position::new(2, 0),
12152 ),
12153 "".to_string(),
12154 )],
12155 )]
12156 .into_iter()
12157 .collect(),
12158 ),
12159 ..Default::default()
12160 }),
12161 ..Default::default()
12162 },
12163 )]))
12164 })
12165 .next()
12166 .await;
12167 cx.executor().start_waiting();
12168 format.await;
12169 assert_eq!(
12170 editor.update(cx, |editor, cx| editor.text(cx)),
12171 "import { a } from 'module';\n\nconst x = a;\n"
12172 );
12173
12174 editor.update_in(cx, |editor, window, cx| {
12175 editor.set_text(
12176 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12177 window,
12178 cx,
12179 )
12180 });
12181 // Ensure we don't lock if code action hangs.
12182 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12183 move |params, _| async move {
12184 assert_eq!(
12185 params.text_document.uri,
12186 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12187 );
12188 futures::future::pending::<()>().await;
12189 unreachable!()
12190 },
12191 );
12192 let format = editor
12193 .update_in(cx, |editor, window, cx| {
12194 editor.perform_code_action_kind(
12195 project,
12196 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12197 window,
12198 cx,
12199 )
12200 })
12201 .unwrap();
12202 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12203 cx.executor().start_waiting();
12204 format.await;
12205 assert_eq!(
12206 editor.update(cx, |editor, cx| editor.text(cx)),
12207 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12208 );
12209}
12210
12211#[gpui::test]
12212async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12213 init_test(cx, |_| {});
12214
12215 let mut cx = EditorLspTestContext::new_rust(
12216 lsp::ServerCapabilities {
12217 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12218 ..Default::default()
12219 },
12220 cx,
12221 )
12222 .await;
12223
12224 cx.set_state(indoc! {"
12225 one.twoˇ
12226 "});
12227
12228 // The format request takes a long time. When it completes, it inserts
12229 // a newline and an indent before the `.`
12230 cx.lsp
12231 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12232 let executor = cx.background_executor().clone();
12233 async move {
12234 executor.timer(Duration::from_millis(100)).await;
12235 Ok(Some(vec![lsp::TextEdit {
12236 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12237 new_text: "\n ".into(),
12238 }]))
12239 }
12240 });
12241
12242 // Submit a format request.
12243 let format_1 = cx
12244 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12245 .unwrap();
12246 cx.executor().run_until_parked();
12247
12248 // Submit a second format request.
12249 let format_2 = cx
12250 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12251 .unwrap();
12252 cx.executor().run_until_parked();
12253
12254 // Wait for both format requests to complete
12255 cx.executor().advance_clock(Duration::from_millis(200));
12256 cx.executor().start_waiting();
12257 format_1.await.unwrap();
12258 cx.executor().start_waiting();
12259 format_2.await.unwrap();
12260
12261 // The formatting edits only happens once.
12262 cx.assert_editor_state(indoc! {"
12263 one
12264 .twoˇ
12265 "});
12266}
12267
12268#[gpui::test]
12269async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12270 init_test(cx, |settings| {
12271 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12272 });
12273
12274 let mut cx = EditorLspTestContext::new_rust(
12275 lsp::ServerCapabilities {
12276 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12277 ..Default::default()
12278 },
12279 cx,
12280 )
12281 .await;
12282
12283 // Set up a buffer white some trailing whitespace and no trailing newline.
12284 cx.set_state(
12285 &[
12286 "one ", //
12287 "twoˇ", //
12288 "three ", //
12289 "four", //
12290 ]
12291 .join("\n"),
12292 );
12293
12294 // Submit a format request.
12295 let format = cx
12296 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12297 .unwrap();
12298
12299 // Record which buffer changes have been sent to the language server
12300 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12301 cx.lsp
12302 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12303 let buffer_changes = buffer_changes.clone();
12304 move |params, _| {
12305 buffer_changes.lock().extend(
12306 params
12307 .content_changes
12308 .into_iter()
12309 .map(|e| (e.range.unwrap(), e.text)),
12310 );
12311 }
12312 });
12313
12314 // Handle formatting requests to the language server.
12315 cx.lsp
12316 .set_request_handler::<lsp::request::Formatting, _, _>({
12317 let buffer_changes = buffer_changes.clone();
12318 move |_, _| {
12319 // When formatting is requested, trailing whitespace has already been stripped,
12320 // and the trailing newline has already been added.
12321 assert_eq!(
12322 &buffer_changes.lock()[1..],
12323 &[
12324 (
12325 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12326 "".into()
12327 ),
12328 (
12329 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12330 "".into()
12331 ),
12332 (
12333 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12334 "\n".into()
12335 ),
12336 ]
12337 );
12338
12339 // Insert blank lines between each line of the buffer.
12340 async move {
12341 Ok(Some(vec![
12342 lsp::TextEdit {
12343 range: lsp::Range::new(
12344 lsp::Position::new(1, 0),
12345 lsp::Position::new(1, 0),
12346 ),
12347 new_text: "\n".into(),
12348 },
12349 lsp::TextEdit {
12350 range: lsp::Range::new(
12351 lsp::Position::new(2, 0),
12352 lsp::Position::new(2, 0),
12353 ),
12354 new_text: "\n".into(),
12355 },
12356 ]))
12357 }
12358 }
12359 });
12360
12361 // After formatting the buffer, the trailing whitespace is stripped,
12362 // a newline is appended, and the edits provided by the language server
12363 // have been applied.
12364 format.await.unwrap();
12365 cx.assert_editor_state(
12366 &[
12367 "one", //
12368 "", //
12369 "twoˇ", //
12370 "", //
12371 "three", //
12372 "four", //
12373 "", //
12374 ]
12375 .join("\n"),
12376 );
12377
12378 // Undoing the formatting undoes the trailing whitespace removal, the
12379 // trailing newline, and the LSP edits.
12380 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12381 cx.assert_editor_state(
12382 &[
12383 "one ", //
12384 "twoˇ", //
12385 "three ", //
12386 "four", //
12387 ]
12388 .join("\n"),
12389 );
12390}
12391
12392#[gpui::test]
12393async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12394 cx: &mut TestAppContext,
12395) {
12396 init_test(cx, |_| {});
12397
12398 cx.update(|cx| {
12399 cx.update_global::<SettingsStore, _>(|settings, cx| {
12400 settings.update_user_settings(cx, |settings| {
12401 settings.editor.auto_signature_help = Some(true);
12402 });
12403 });
12404 });
12405
12406 let mut cx = EditorLspTestContext::new_rust(
12407 lsp::ServerCapabilities {
12408 signature_help_provider: Some(lsp::SignatureHelpOptions {
12409 ..Default::default()
12410 }),
12411 ..Default::default()
12412 },
12413 cx,
12414 )
12415 .await;
12416
12417 let language = Language::new(
12418 LanguageConfig {
12419 name: "Rust".into(),
12420 brackets: BracketPairConfig {
12421 pairs: vec![
12422 BracketPair {
12423 start: "{".to_string(),
12424 end: "}".to_string(),
12425 close: true,
12426 surround: true,
12427 newline: true,
12428 },
12429 BracketPair {
12430 start: "(".to_string(),
12431 end: ")".to_string(),
12432 close: true,
12433 surround: true,
12434 newline: true,
12435 },
12436 BracketPair {
12437 start: "/*".to_string(),
12438 end: " */".to_string(),
12439 close: true,
12440 surround: true,
12441 newline: true,
12442 },
12443 BracketPair {
12444 start: "[".to_string(),
12445 end: "]".to_string(),
12446 close: false,
12447 surround: false,
12448 newline: true,
12449 },
12450 BracketPair {
12451 start: "\"".to_string(),
12452 end: "\"".to_string(),
12453 close: true,
12454 surround: true,
12455 newline: false,
12456 },
12457 BracketPair {
12458 start: "<".to_string(),
12459 end: ">".to_string(),
12460 close: false,
12461 surround: true,
12462 newline: true,
12463 },
12464 ],
12465 ..Default::default()
12466 },
12467 autoclose_before: "})]".to_string(),
12468 ..Default::default()
12469 },
12470 Some(tree_sitter_rust::LANGUAGE.into()),
12471 );
12472 let language = Arc::new(language);
12473
12474 cx.language_registry().add(language.clone());
12475 cx.update_buffer(|buffer, cx| {
12476 buffer.set_language(Some(language), cx);
12477 });
12478
12479 cx.set_state(
12480 &r#"
12481 fn main() {
12482 sampleˇ
12483 }
12484 "#
12485 .unindent(),
12486 );
12487
12488 cx.update_editor(|editor, window, cx| {
12489 editor.handle_input("(", window, cx);
12490 });
12491 cx.assert_editor_state(
12492 &"
12493 fn main() {
12494 sample(ˇ)
12495 }
12496 "
12497 .unindent(),
12498 );
12499
12500 let mocked_response = lsp::SignatureHelp {
12501 signatures: vec![lsp::SignatureInformation {
12502 label: "fn sample(param1: u8, param2: u8)".to_string(),
12503 documentation: None,
12504 parameters: Some(vec![
12505 lsp::ParameterInformation {
12506 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12507 documentation: None,
12508 },
12509 lsp::ParameterInformation {
12510 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12511 documentation: None,
12512 },
12513 ]),
12514 active_parameter: None,
12515 }],
12516 active_signature: Some(0),
12517 active_parameter: Some(0),
12518 };
12519 handle_signature_help_request(&mut cx, mocked_response).await;
12520
12521 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12522 .await;
12523
12524 cx.editor(|editor, _, _| {
12525 let signature_help_state = editor.signature_help_state.popover().cloned();
12526 let signature = signature_help_state.unwrap();
12527 assert_eq!(
12528 signature.signatures[signature.current_signature].label,
12529 "fn sample(param1: u8, param2: u8)"
12530 );
12531 });
12532}
12533
12534#[gpui::test]
12535async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12536 init_test(cx, |_| {});
12537
12538 cx.update(|cx| {
12539 cx.update_global::<SettingsStore, _>(|settings, cx| {
12540 settings.update_user_settings(cx, |settings| {
12541 settings.editor.auto_signature_help = Some(false);
12542 settings.editor.show_signature_help_after_edits = Some(false);
12543 });
12544 });
12545 });
12546
12547 let mut cx = EditorLspTestContext::new_rust(
12548 lsp::ServerCapabilities {
12549 signature_help_provider: Some(lsp::SignatureHelpOptions {
12550 ..Default::default()
12551 }),
12552 ..Default::default()
12553 },
12554 cx,
12555 )
12556 .await;
12557
12558 let language = Language::new(
12559 LanguageConfig {
12560 name: "Rust".into(),
12561 brackets: BracketPairConfig {
12562 pairs: vec![
12563 BracketPair {
12564 start: "{".to_string(),
12565 end: "}".to_string(),
12566 close: true,
12567 surround: true,
12568 newline: true,
12569 },
12570 BracketPair {
12571 start: "(".to_string(),
12572 end: ")".to_string(),
12573 close: true,
12574 surround: true,
12575 newline: true,
12576 },
12577 BracketPair {
12578 start: "/*".to_string(),
12579 end: " */".to_string(),
12580 close: true,
12581 surround: true,
12582 newline: true,
12583 },
12584 BracketPair {
12585 start: "[".to_string(),
12586 end: "]".to_string(),
12587 close: false,
12588 surround: false,
12589 newline: true,
12590 },
12591 BracketPair {
12592 start: "\"".to_string(),
12593 end: "\"".to_string(),
12594 close: true,
12595 surround: true,
12596 newline: false,
12597 },
12598 BracketPair {
12599 start: "<".to_string(),
12600 end: ">".to_string(),
12601 close: false,
12602 surround: true,
12603 newline: true,
12604 },
12605 ],
12606 ..Default::default()
12607 },
12608 autoclose_before: "})]".to_string(),
12609 ..Default::default()
12610 },
12611 Some(tree_sitter_rust::LANGUAGE.into()),
12612 );
12613 let language = Arc::new(language);
12614
12615 cx.language_registry().add(language.clone());
12616 cx.update_buffer(|buffer, cx| {
12617 buffer.set_language(Some(language), cx);
12618 });
12619
12620 // Ensure that signature_help is not called when no signature help is enabled.
12621 cx.set_state(
12622 &r#"
12623 fn main() {
12624 sampleˇ
12625 }
12626 "#
12627 .unindent(),
12628 );
12629 cx.update_editor(|editor, window, cx| {
12630 editor.handle_input("(", window, cx);
12631 });
12632 cx.assert_editor_state(
12633 &"
12634 fn main() {
12635 sample(ˇ)
12636 }
12637 "
12638 .unindent(),
12639 );
12640 cx.editor(|editor, _, _| {
12641 assert!(editor.signature_help_state.task().is_none());
12642 });
12643
12644 let mocked_response = lsp::SignatureHelp {
12645 signatures: vec![lsp::SignatureInformation {
12646 label: "fn sample(param1: u8, param2: u8)".to_string(),
12647 documentation: None,
12648 parameters: Some(vec![
12649 lsp::ParameterInformation {
12650 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12651 documentation: None,
12652 },
12653 lsp::ParameterInformation {
12654 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12655 documentation: None,
12656 },
12657 ]),
12658 active_parameter: None,
12659 }],
12660 active_signature: Some(0),
12661 active_parameter: Some(0),
12662 };
12663
12664 // Ensure that signature_help is called when enabled afte edits
12665 cx.update(|_, cx| {
12666 cx.update_global::<SettingsStore, _>(|settings, cx| {
12667 settings.update_user_settings(cx, |settings| {
12668 settings.editor.auto_signature_help = Some(false);
12669 settings.editor.show_signature_help_after_edits = Some(true);
12670 });
12671 });
12672 });
12673 cx.set_state(
12674 &r#"
12675 fn main() {
12676 sampleˇ
12677 }
12678 "#
12679 .unindent(),
12680 );
12681 cx.update_editor(|editor, window, cx| {
12682 editor.handle_input("(", window, cx);
12683 });
12684 cx.assert_editor_state(
12685 &"
12686 fn main() {
12687 sample(ˇ)
12688 }
12689 "
12690 .unindent(),
12691 );
12692 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12693 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12694 .await;
12695 cx.update_editor(|editor, _, _| {
12696 let signature_help_state = editor.signature_help_state.popover().cloned();
12697 assert!(signature_help_state.is_some());
12698 let signature = signature_help_state.unwrap();
12699 assert_eq!(
12700 signature.signatures[signature.current_signature].label,
12701 "fn sample(param1: u8, param2: u8)"
12702 );
12703 editor.signature_help_state = SignatureHelpState::default();
12704 });
12705
12706 // Ensure that signature_help is called when auto signature help override is enabled
12707 cx.update(|_, cx| {
12708 cx.update_global::<SettingsStore, _>(|settings, cx| {
12709 settings.update_user_settings(cx, |settings| {
12710 settings.editor.auto_signature_help = Some(true);
12711 settings.editor.show_signature_help_after_edits = Some(false);
12712 });
12713 });
12714 });
12715 cx.set_state(
12716 &r#"
12717 fn main() {
12718 sampleˇ
12719 }
12720 "#
12721 .unindent(),
12722 );
12723 cx.update_editor(|editor, window, cx| {
12724 editor.handle_input("(", window, cx);
12725 });
12726 cx.assert_editor_state(
12727 &"
12728 fn main() {
12729 sample(ˇ)
12730 }
12731 "
12732 .unindent(),
12733 );
12734 handle_signature_help_request(&mut cx, mocked_response).await;
12735 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12736 .await;
12737 cx.editor(|editor, _, _| {
12738 let signature_help_state = editor.signature_help_state.popover().cloned();
12739 assert!(signature_help_state.is_some());
12740 let signature = signature_help_state.unwrap();
12741 assert_eq!(
12742 signature.signatures[signature.current_signature].label,
12743 "fn sample(param1: u8, param2: u8)"
12744 );
12745 });
12746}
12747
12748#[gpui::test]
12749async fn test_signature_help(cx: &mut TestAppContext) {
12750 init_test(cx, |_| {});
12751 cx.update(|cx| {
12752 cx.update_global::<SettingsStore, _>(|settings, cx| {
12753 settings.update_user_settings(cx, |settings| {
12754 settings.editor.auto_signature_help = Some(true);
12755 });
12756 });
12757 });
12758
12759 let mut cx = EditorLspTestContext::new_rust(
12760 lsp::ServerCapabilities {
12761 signature_help_provider: Some(lsp::SignatureHelpOptions {
12762 ..Default::default()
12763 }),
12764 ..Default::default()
12765 },
12766 cx,
12767 )
12768 .await;
12769
12770 // A test that directly calls `show_signature_help`
12771 cx.update_editor(|editor, window, cx| {
12772 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12773 });
12774
12775 let mocked_response = lsp::SignatureHelp {
12776 signatures: vec![lsp::SignatureInformation {
12777 label: "fn sample(param1: u8, param2: u8)".to_string(),
12778 documentation: None,
12779 parameters: Some(vec![
12780 lsp::ParameterInformation {
12781 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12782 documentation: None,
12783 },
12784 lsp::ParameterInformation {
12785 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12786 documentation: None,
12787 },
12788 ]),
12789 active_parameter: None,
12790 }],
12791 active_signature: Some(0),
12792 active_parameter: Some(0),
12793 };
12794 handle_signature_help_request(&mut cx, mocked_response).await;
12795
12796 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12797 .await;
12798
12799 cx.editor(|editor, _, _| {
12800 let signature_help_state = editor.signature_help_state.popover().cloned();
12801 assert!(signature_help_state.is_some());
12802 let signature = signature_help_state.unwrap();
12803 assert_eq!(
12804 signature.signatures[signature.current_signature].label,
12805 "fn sample(param1: u8, param2: u8)"
12806 );
12807 });
12808
12809 // When exiting outside from inside the brackets, `signature_help` is closed.
12810 cx.set_state(indoc! {"
12811 fn main() {
12812 sample(ˇ);
12813 }
12814
12815 fn sample(param1: u8, param2: u8) {}
12816 "});
12817
12818 cx.update_editor(|editor, window, cx| {
12819 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12820 s.select_ranges([0..0])
12821 });
12822 });
12823
12824 let mocked_response = lsp::SignatureHelp {
12825 signatures: Vec::new(),
12826 active_signature: None,
12827 active_parameter: None,
12828 };
12829 handle_signature_help_request(&mut cx, mocked_response).await;
12830
12831 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12832 .await;
12833
12834 cx.editor(|editor, _, _| {
12835 assert!(!editor.signature_help_state.is_shown());
12836 });
12837
12838 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12839 cx.set_state(indoc! {"
12840 fn main() {
12841 sample(ˇ);
12842 }
12843
12844 fn sample(param1: u8, param2: u8) {}
12845 "});
12846
12847 let mocked_response = lsp::SignatureHelp {
12848 signatures: vec![lsp::SignatureInformation {
12849 label: "fn sample(param1: u8, param2: u8)".to_string(),
12850 documentation: None,
12851 parameters: Some(vec![
12852 lsp::ParameterInformation {
12853 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12854 documentation: None,
12855 },
12856 lsp::ParameterInformation {
12857 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12858 documentation: None,
12859 },
12860 ]),
12861 active_parameter: None,
12862 }],
12863 active_signature: Some(0),
12864 active_parameter: Some(0),
12865 };
12866 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12867 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12868 .await;
12869 cx.editor(|editor, _, _| {
12870 assert!(editor.signature_help_state.is_shown());
12871 });
12872
12873 // Restore the popover with more parameter input
12874 cx.set_state(indoc! {"
12875 fn main() {
12876 sample(param1, param2ˇ);
12877 }
12878
12879 fn sample(param1: u8, param2: u8) {}
12880 "});
12881
12882 let mocked_response = lsp::SignatureHelp {
12883 signatures: vec![lsp::SignatureInformation {
12884 label: "fn sample(param1: u8, param2: u8)".to_string(),
12885 documentation: None,
12886 parameters: Some(vec![
12887 lsp::ParameterInformation {
12888 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12889 documentation: None,
12890 },
12891 lsp::ParameterInformation {
12892 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12893 documentation: None,
12894 },
12895 ]),
12896 active_parameter: None,
12897 }],
12898 active_signature: Some(0),
12899 active_parameter: Some(1),
12900 };
12901 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12902 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12903 .await;
12904
12905 // When selecting a range, the popover is gone.
12906 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12907 cx.update_editor(|editor, window, cx| {
12908 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12909 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12910 })
12911 });
12912 cx.assert_editor_state(indoc! {"
12913 fn main() {
12914 sample(param1, «ˇparam2»);
12915 }
12916
12917 fn sample(param1: u8, param2: u8) {}
12918 "});
12919 cx.editor(|editor, _, _| {
12920 assert!(!editor.signature_help_state.is_shown());
12921 });
12922
12923 // When unselecting again, the popover is back if within the brackets.
12924 cx.update_editor(|editor, window, cx| {
12925 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12926 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12927 })
12928 });
12929 cx.assert_editor_state(indoc! {"
12930 fn main() {
12931 sample(param1, ˇparam2);
12932 }
12933
12934 fn sample(param1: u8, param2: u8) {}
12935 "});
12936 handle_signature_help_request(&mut cx, mocked_response).await;
12937 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12938 .await;
12939 cx.editor(|editor, _, _| {
12940 assert!(editor.signature_help_state.is_shown());
12941 });
12942
12943 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
12944 cx.update_editor(|editor, window, cx| {
12945 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12946 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
12947 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12948 })
12949 });
12950 cx.assert_editor_state(indoc! {"
12951 fn main() {
12952 sample(param1, ˇparam2);
12953 }
12954
12955 fn sample(param1: u8, param2: u8) {}
12956 "});
12957
12958 let mocked_response = lsp::SignatureHelp {
12959 signatures: vec![lsp::SignatureInformation {
12960 label: "fn sample(param1: u8, param2: u8)".to_string(),
12961 documentation: None,
12962 parameters: Some(vec![
12963 lsp::ParameterInformation {
12964 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12965 documentation: None,
12966 },
12967 lsp::ParameterInformation {
12968 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12969 documentation: None,
12970 },
12971 ]),
12972 active_parameter: None,
12973 }],
12974 active_signature: Some(0),
12975 active_parameter: Some(1),
12976 };
12977 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12978 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12979 .await;
12980 cx.update_editor(|editor, _, cx| {
12981 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
12982 });
12983 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12984 .await;
12985 cx.update_editor(|editor, window, cx| {
12986 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12987 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12988 })
12989 });
12990 cx.assert_editor_state(indoc! {"
12991 fn main() {
12992 sample(param1, «ˇparam2»);
12993 }
12994
12995 fn sample(param1: u8, param2: u8) {}
12996 "});
12997 cx.update_editor(|editor, window, cx| {
12998 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12999 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13000 })
13001 });
13002 cx.assert_editor_state(indoc! {"
13003 fn main() {
13004 sample(param1, ˇparam2);
13005 }
13006
13007 fn sample(param1: u8, param2: u8) {}
13008 "});
13009 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13010 .await;
13011}
13012
13013#[gpui::test]
13014async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13015 init_test(cx, |_| {});
13016
13017 let mut cx = EditorLspTestContext::new_rust(
13018 lsp::ServerCapabilities {
13019 signature_help_provider: Some(lsp::SignatureHelpOptions {
13020 ..Default::default()
13021 }),
13022 ..Default::default()
13023 },
13024 cx,
13025 )
13026 .await;
13027
13028 cx.set_state(indoc! {"
13029 fn main() {
13030 overloadedˇ
13031 }
13032 "});
13033
13034 cx.update_editor(|editor, window, cx| {
13035 editor.handle_input("(", window, cx);
13036 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13037 });
13038
13039 // Mock response with 3 signatures
13040 let mocked_response = lsp::SignatureHelp {
13041 signatures: vec![
13042 lsp::SignatureInformation {
13043 label: "fn overloaded(x: i32)".to_string(),
13044 documentation: None,
13045 parameters: Some(vec![lsp::ParameterInformation {
13046 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13047 documentation: None,
13048 }]),
13049 active_parameter: None,
13050 },
13051 lsp::SignatureInformation {
13052 label: "fn overloaded(x: i32, y: i32)".to_string(),
13053 documentation: None,
13054 parameters: Some(vec![
13055 lsp::ParameterInformation {
13056 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13057 documentation: None,
13058 },
13059 lsp::ParameterInformation {
13060 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13061 documentation: None,
13062 },
13063 ]),
13064 active_parameter: None,
13065 },
13066 lsp::SignatureInformation {
13067 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13068 documentation: None,
13069 parameters: Some(vec![
13070 lsp::ParameterInformation {
13071 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13072 documentation: None,
13073 },
13074 lsp::ParameterInformation {
13075 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13076 documentation: None,
13077 },
13078 lsp::ParameterInformation {
13079 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13080 documentation: None,
13081 },
13082 ]),
13083 active_parameter: None,
13084 },
13085 ],
13086 active_signature: Some(1),
13087 active_parameter: Some(0),
13088 };
13089 handle_signature_help_request(&mut cx, mocked_response).await;
13090
13091 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13092 .await;
13093
13094 // Verify we have multiple signatures and the right one is selected
13095 cx.editor(|editor, _, _| {
13096 let popover = editor.signature_help_state.popover().cloned().unwrap();
13097 assert_eq!(popover.signatures.len(), 3);
13098 // active_signature was 1, so that should be the current
13099 assert_eq!(popover.current_signature, 1);
13100 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13101 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13102 assert_eq!(
13103 popover.signatures[2].label,
13104 "fn overloaded(x: i32, y: i32, z: i32)"
13105 );
13106 });
13107
13108 // Test navigation functionality
13109 cx.update_editor(|editor, window, cx| {
13110 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13111 });
13112
13113 cx.editor(|editor, _, _| {
13114 let popover = editor.signature_help_state.popover().cloned().unwrap();
13115 assert_eq!(popover.current_signature, 2);
13116 });
13117
13118 // Test wrap around
13119 cx.update_editor(|editor, window, cx| {
13120 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13121 });
13122
13123 cx.editor(|editor, _, _| {
13124 let popover = editor.signature_help_state.popover().cloned().unwrap();
13125 assert_eq!(popover.current_signature, 0);
13126 });
13127
13128 // Test previous navigation
13129 cx.update_editor(|editor, window, cx| {
13130 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13131 });
13132
13133 cx.editor(|editor, _, _| {
13134 let popover = editor.signature_help_state.popover().cloned().unwrap();
13135 assert_eq!(popover.current_signature, 2);
13136 });
13137}
13138
13139#[gpui::test]
13140async fn test_completion_mode(cx: &mut TestAppContext) {
13141 init_test(cx, |_| {});
13142 let mut cx = EditorLspTestContext::new_rust(
13143 lsp::ServerCapabilities {
13144 completion_provider: Some(lsp::CompletionOptions {
13145 resolve_provider: Some(true),
13146 ..Default::default()
13147 }),
13148 ..Default::default()
13149 },
13150 cx,
13151 )
13152 .await;
13153
13154 struct Run {
13155 run_description: &'static str,
13156 initial_state: String,
13157 buffer_marked_text: String,
13158 completion_label: &'static str,
13159 completion_text: &'static str,
13160 expected_with_insert_mode: String,
13161 expected_with_replace_mode: String,
13162 expected_with_replace_subsequence_mode: String,
13163 expected_with_replace_suffix_mode: String,
13164 }
13165
13166 let runs = [
13167 Run {
13168 run_description: "Start of word matches completion text",
13169 initial_state: "before ediˇ after".into(),
13170 buffer_marked_text: "before <edi|> after".into(),
13171 completion_label: "editor",
13172 completion_text: "editor",
13173 expected_with_insert_mode: "before editorˇ after".into(),
13174 expected_with_replace_mode: "before editorˇ after".into(),
13175 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13176 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13177 },
13178 Run {
13179 run_description: "Accept same text at the middle of the word",
13180 initial_state: "before ediˇtor after".into(),
13181 buffer_marked_text: "before <edi|tor> after".into(),
13182 completion_label: "editor",
13183 completion_text: "editor",
13184 expected_with_insert_mode: "before editorˇtor after".into(),
13185 expected_with_replace_mode: "before editorˇ after".into(),
13186 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13187 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13188 },
13189 Run {
13190 run_description: "End of word matches completion text -- cursor at end",
13191 initial_state: "before torˇ after".into(),
13192 buffer_marked_text: "before <tor|> after".into(),
13193 completion_label: "editor",
13194 completion_text: "editor",
13195 expected_with_insert_mode: "before editorˇ after".into(),
13196 expected_with_replace_mode: "before editorˇ after".into(),
13197 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13198 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13199 },
13200 Run {
13201 run_description: "End of word matches completion text -- cursor at start",
13202 initial_state: "before ˇtor after".into(),
13203 buffer_marked_text: "before <|tor> after".into(),
13204 completion_label: "editor",
13205 completion_text: "editor",
13206 expected_with_insert_mode: "before editorˇtor after".into(),
13207 expected_with_replace_mode: "before editorˇ after".into(),
13208 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13209 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13210 },
13211 Run {
13212 run_description: "Prepend text containing whitespace",
13213 initial_state: "pˇfield: bool".into(),
13214 buffer_marked_text: "<p|field>: bool".into(),
13215 completion_label: "pub ",
13216 completion_text: "pub ",
13217 expected_with_insert_mode: "pub ˇfield: bool".into(),
13218 expected_with_replace_mode: "pub ˇ: bool".into(),
13219 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13220 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13221 },
13222 Run {
13223 run_description: "Add element to start of list",
13224 initial_state: "[element_ˇelement_2]".into(),
13225 buffer_marked_text: "[<element_|element_2>]".into(),
13226 completion_label: "element_1",
13227 completion_text: "element_1",
13228 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13229 expected_with_replace_mode: "[element_1ˇ]".into(),
13230 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13231 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13232 },
13233 Run {
13234 run_description: "Add element to start of list -- first and second elements are equal",
13235 initial_state: "[elˇelement]".into(),
13236 buffer_marked_text: "[<el|element>]".into(),
13237 completion_label: "element",
13238 completion_text: "element",
13239 expected_with_insert_mode: "[elementˇelement]".into(),
13240 expected_with_replace_mode: "[elementˇ]".into(),
13241 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13242 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13243 },
13244 Run {
13245 run_description: "Ends with matching suffix",
13246 initial_state: "SubˇError".into(),
13247 buffer_marked_text: "<Sub|Error>".into(),
13248 completion_label: "SubscriptionError",
13249 completion_text: "SubscriptionError",
13250 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13251 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13252 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13253 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13254 },
13255 Run {
13256 run_description: "Suffix is a subsequence -- contiguous",
13257 initial_state: "SubˇErr".into(),
13258 buffer_marked_text: "<Sub|Err>".into(),
13259 completion_label: "SubscriptionError",
13260 completion_text: "SubscriptionError",
13261 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13262 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13263 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13264 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13265 },
13266 Run {
13267 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13268 initial_state: "Suˇscrirr".into(),
13269 buffer_marked_text: "<Su|scrirr>".into(),
13270 completion_label: "SubscriptionError",
13271 completion_text: "SubscriptionError",
13272 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13273 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13274 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13275 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13276 },
13277 Run {
13278 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13279 initial_state: "foo(indˇix)".into(),
13280 buffer_marked_text: "foo(<ind|ix>)".into(),
13281 completion_label: "node_index",
13282 completion_text: "node_index",
13283 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13284 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13285 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13286 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13287 },
13288 Run {
13289 run_description: "Replace range ends before cursor - should extend to cursor",
13290 initial_state: "before editˇo after".into(),
13291 buffer_marked_text: "before <{ed}>it|o after".into(),
13292 completion_label: "editor",
13293 completion_text: "editor",
13294 expected_with_insert_mode: "before editorˇo after".into(),
13295 expected_with_replace_mode: "before editorˇo after".into(),
13296 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13297 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13298 },
13299 Run {
13300 run_description: "Uses label for suffix matching",
13301 initial_state: "before ediˇtor after".into(),
13302 buffer_marked_text: "before <edi|tor> after".into(),
13303 completion_label: "editor",
13304 completion_text: "editor()",
13305 expected_with_insert_mode: "before editor()ˇtor after".into(),
13306 expected_with_replace_mode: "before editor()ˇ after".into(),
13307 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13308 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13309 },
13310 Run {
13311 run_description: "Case insensitive subsequence and suffix matching",
13312 initial_state: "before EDiˇtoR after".into(),
13313 buffer_marked_text: "before <EDi|toR> after".into(),
13314 completion_label: "editor",
13315 completion_text: "editor",
13316 expected_with_insert_mode: "before editorˇtoR after".into(),
13317 expected_with_replace_mode: "before editorˇ after".into(),
13318 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13319 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13320 },
13321 ];
13322
13323 for run in runs {
13324 let run_variations = [
13325 (LspInsertMode::Insert, run.expected_with_insert_mode),
13326 (LspInsertMode::Replace, run.expected_with_replace_mode),
13327 (
13328 LspInsertMode::ReplaceSubsequence,
13329 run.expected_with_replace_subsequence_mode,
13330 ),
13331 (
13332 LspInsertMode::ReplaceSuffix,
13333 run.expected_with_replace_suffix_mode,
13334 ),
13335 ];
13336
13337 for (lsp_insert_mode, expected_text) in run_variations {
13338 eprintln!(
13339 "run = {:?}, mode = {lsp_insert_mode:.?}",
13340 run.run_description,
13341 );
13342
13343 update_test_language_settings(&mut cx, |settings| {
13344 settings.defaults.completions = Some(CompletionSettingsContent {
13345 lsp_insert_mode: Some(lsp_insert_mode),
13346 words: Some(WordsCompletionMode::Disabled),
13347 words_min_length: Some(0),
13348 ..Default::default()
13349 });
13350 });
13351
13352 cx.set_state(&run.initial_state);
13353 cx.update_editor(|editor, window, cx| {
13354 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13355 });
13356
13357 let counter = Arc::new(AtomicUsize::new(0));
13358 handle_completion_request_with_insert_and_replace(
13359 &mut cx,
13360 &run.buffer_marked_text,
13361 vec![(run.completion_label, run.completion_text)],
13362 counter.clone(),
13363 )
13364 .await;
13365 cx.condition(|editor, _| editor.context_menu_visible())
13366 .await;
13367 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13368
13369 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13370 editor
13371 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13372 .unwrap()
13373 });
13374 cx.assert_editor_state(&expected_text);
13375 handle_resolve_completion_request(&mut cx, None).await;
13376 apply_additional_edits.await.unwrap();
13377 }
13378 }
13379}
13380
13381#[gpui::test]
13382async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13383 init_test(cx, |_| {});
13384 let mut cx = EditorLspTestContext::new_rust(
13385 lsp::ServerCapabilities {
13386 completion_provider: Some(lsp::CompletionOptions {
13387 resolve_provider: Some(true),
13388 ..Default::default()
13389 }),
13390 ..Default::default()
13391 },
13392 cx,
13393 )
13394 .await;
13395
13396 let initial_state = "SubˇError";
13397 let buffer_marked_text = "<Sub|Error>";
13398 let completion_text = "SubscriptionError";
13399 let expected_with_insert_mode = "SubscriptionErrorˇError";
13400 let expected_with_replace_mode = "SubscriptionErrorˇ";
13401
13402 update_test_language_settings(&mut cx, |settings| {
13403 settings.defaults.completions = Some(CompletionSettingsContent {
13404 words: Some(WordsCompletionMode::Disabled),
13405 words_min_length: Some(0),
13406 // set the opposite here to ensure that the action is overriding the default behavior
13407 lsp_insert_mode: Some(LspInsertMode::Insert),
13408 ..Default::default()
13409 });
13410 });
13411
13412 cx.set_state(initial_state);
13413 cx.update_editor(|editor, window, cx| {
13414 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13415 });
13416
13417 let counter = Arc::new(AtomicUsize::new(0));
13418 handle_completion_request_with_insert_and_replace(
13419 &mut cx,
13420 buffer_marked_text,
13421 vec![(completion_text, completion_text)],
13422 counter.clone(),
13423 )
13424 .await;
13425 cx.condition(|editor, _| editor.context_menu_visible())
13426 .await;
13427 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13428
13429 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13430 editor
13431 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13432 .unwrap()
13433 });
13434 cx.assert_editor_state(expected_with_replace_mode);
13435 handle_resolve_completion_request(&mut cx, None).await;
13436 apply_additional_edits.await.unwrap();
13437
13438 update_test_language_settings(&mut cx, |settings| {
13439 settings.defaults.completions = Some(CompletionSettingsContent {
13440 words: Some(WordsCompletionMode::Disabled),
13441 words_min_length: Some(0),
13442 // set the opposite here to ensure that the action is overriding the default behavior
13443 lsp_insert_mode: Some(LspInsertMode::Replace),
13444 ..Default::default()
13445 });
13446 });
13447
13448 cx.set_state(initial_state);
13449 cx.update_editor(|editor, window, cx| {
13450 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13451 });
13452 handle_completion_request_with_insert_and_replace(
13453 &mut cx,
13454 buffer_marked_text,
13455 vec![(completion_text, completion_text)],
13456 counter.clone(),
13457 )
13458 .await;
13459 cx.condition(|editor, _| editor.context_menu_visible())
13460 .await;
13461 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13462
13463 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13464 editor
13465 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13466 .unwrap()
13467 });
13468 cx.assert_editor_state(expected_with_insert_mode);
13469 handle_resolve_completion_request(&mut cx, None).await;
13470 apply_additional_edits.await.unwrap();
13471}
13472
13473#[gpui::test]
13474async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13475 init_test(cx, |_| {});
13476 let mut cx = EditorLspTestContext::new_rust(
13477 lsp::ServerCapabilities {
13478 completion_provider: Some(lsp::CompletionOptions {
13479 resolve_provider: Some(true),
13480 ..Default::default()
13481 }),
13482 ..Default::default()
13483 },
13484 cx,
13485 )
13486 .await;
13487
13488 // scenario: surrounding text matches completion text
13489 let completion_text = "to_offset";
13490 let initial_state = indoc! {"
13491 1. buf.to_offˇsuffix
13492 2. buf.to_offˇsuf
13493 3. buf.to_offˇfix
13494 4. buf.to_offˇ
13495 5. into_offˇensive
13496 6. ˇsuffix
13497 7. let ˇ //
13498 8. aaˇzz
13499 9. buf.to_off«zzzzzˇ»suffix
13500 10. buf.«ˇzzzzz»suffix
13501 11. to_off«ˇzzzzz»
13502
13503 buf.to_offˇsuffix // newest cursor
13504 "};
13505 let completion_marked_buffer = indoc! {"
13506 1. buf.to_offsuffix
13507 2. buf.to_offsuf
13508 3. buf.to_offfix
13509 4. buf.to_off
13510 5. into_offensive
13511 6. suffix
13512 7. let //
13513 8. aazz
13514 9. buf.to_offzzzzzsuffix
13515 10. buf.zzzzzsuffix
13516 11. to_offzzzzz
13517
13518 buf.<to_off|suffix> // newest cursor
13519 "};
13520 let expected = indoc! {"
13521 1. buf.to_offsetˇ
13522 2. buf.to_offsetˇsuf
13523 3. buf.to_offsetˇfix
13524 4. buf.to_offsetˇ
13525 5. into_offsetˇensive
13526 6. to_offsetˇsuffix
13527 7. let to_offsetˇ //
13528 8. aato_offsetˇzz
13529 9. buf.to_offsetˇ
13530 10. buf.to_offsetˇsuffix
13531 11. to_offsetˇ
13532
13533 buf.to_offsetˇ // newest cursor
13534 "};
13535 cx.set_state(initial_state);
13536 cx.update_editor(|editor, window, cx| {
13537 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13538 });
13539 handle_completion_request_with_insert_and_replace(
13540 &mut cx,
13541 completion_marked_buffer,
13542 vec![(completion_text, completion_text)],
13543 Arc::new(AtomicUsize::new(0)),
13544 )
13545 .await;
13546 cx.condition(|editor, _| editor.context_menu_visible())
13547 .await;
13548 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13549 editor
13550 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13551 .unwrap()
13552 });
13553 cx.assert_editor_state(expected);
13554 handle_resolve_completion_request(&mut cx, None).await;
13555 apply_additional_edits.await.unwrap();
13556
13557 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13558 let completion_text = "foo_and_bar";
13559 let initial_state = indoc! {"
13560 1. ooanbˇ
13561 2. zooanbˇ
13562 3. ooanbˇz
13563 4. zooanbˇz
13564 5. ooanˇ
13565 6. oanbˇ
13566
13567 ooanbˇ
13568 "};
13569 let completion_marked_buffer = indoc! {"
13570 1. ooanb
13571 2. zooanb
13572 3. ooanbz
13573 4. zooanbz
13574 5. ooan
13575 6. oanb
13576
13577 <ooanb|>
13578 "};
13579 let expected = indoc! {"
13580 1. foo_and_barˇ
13581 2. zfoo_and_barˇ
13582 3. foo_and_barˇz
13583 4. zfoo_and_barˇz
13584 5. ooanfoo_and_barˇ
13585 6. oanbfoo_and_barˇ
13586
13587 foo_and_barˇ
13588 "};
13589 cx.set_state(initial_state);
13590 cx.update_editor(|editor, window, cx| {
13591 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13592 });
13593 handle_completion_request_with_insert_and_replace(
13594 &mut cx,
13595 completion_marked_buffer,
13596 vec![(completion_text, completion_text)],
13597 Arc::new(AtomicUsize::new(0)),
13598 )
13599 .await;
13600 cx.condition(|editor, _| editor.context_menu_visible())
13601 .await;
13602 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13603 editor
13604 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13605 .unwrap()
13606 });
13607 cx.assert_editor_state(expected);
13608 handle_resolve_completion_request(&mut cx, None).await;
13609 apply_additional_edits.await.unwrap();
13610
13611 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13612 // (expects the same as if it was inserted at the end)
13613 let completion_text = "foo_and_bar";
13614 let initial_state = indoc! {"
13615 1. ooˇanb
13616 2. zooˇanb
13617 3. ooˇanbz
13618 4. zooˇanbz
13619
13620 ooˇanb
13621 "};
13622 let completion_marked_buffer = indoc! {"
13623 1. ooanb
13624 2. zooanb
13625 3. ooanbz
13626 4. zooanbz
13627
13628 <oo|anb>
13629 "};
13630 let expected = indoc! {"
13631 1. foo_and_barˇ
13632 2. zfoo_and_barˇ
13633 3. foo_and_barˇz
13634 4. zfoo_and_barˇz
13635
13636 foo_and_barˇ
13637 "};
13638 cx.set_state(initial_state);
13639 cx.update_editor(|editor, window, cx| {
13640 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13641 });
13642 handle_completion_request_with_insert_and_replace(
13643 &mut cx,
13644 completion_marked_buffer,
13645 vec![(completion_text, completion_text)],
13646 Arc::new(AtomicUsize::new(0)),
13647 )
13648 .await;
13649 cx.condition(|editor, _| editor.context_menu_visible())
13650 .await;
13651 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13652 editor
13653 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13654 .unwrap()
13655 });
13656 cx.assert_editor_state(expected);
13657 handle_resolve_completion_request(&mut cx, None).await;
13658 apply_additional_edits.await.unwrap();
13659}
13660
13661// This used to crash
13662#[gpui::test]
13663async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13664 init_test(cx, |_| {});
13665
13666 let buffer_text = indoc! {"
13667 fn main() {
13668 10.satu;
13669
13670 //
13671 // separate cursors so they open in different excerpts (manually reproducible)
13672 //
13673
13674 10.satu20;
13675 }
13676 "};
13677 let multibuffer_text_with_selections = indoc! {"
13678 fn main() {
13679 10.satuˇ;
13680
13681 //
13682
13683 //
13684
13685 10.satuˇ20;
13686 }
13687 "};
13688 let expected_multibuffer = indoc! {"
13689 fn main() {
13690 10.saturating_sub()ˇ;
13691
13692 //
13693
13694 //
13695
13696 10.saturating_sub()ˇ;
13697 }
13698 "};
13699
13700 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13701 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13702
13703 let fs = FakeFs::new(cx.executor());
13704 fs.insert_tree(
13705 path!("/a"),
13706 json!({
13707 "main.rs": buffer_text,
13708 }),
13709 )
13710 .await;
13711
13712 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13713 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13714 language_registry.add(rust_lang());
13715 let mut fake_servers = language_registry.register_fake_lsp(
13716 "Rust",
13717 FakeLspAdapter {
13718 capabilities: lsp::ServerCapabilities {
13719 completion_provider: Some(lsp::CompletionOptions {
13720 resolve_provider: None,
13721 ..lsp::CompletionOptions::default()
13722 }),
13723 ..lsp::ServerCapabilities::default()
13724 },
13725 ..FakeLspAdapter::default()
13726 },
13727 );
13728 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13729 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13730 let buffer = project
13731 .update(cx, |project, cx| {
13732 project.open_local_buffer(path!("/a/main.rs"), cx)
13733 })
13734 .await
13735 .unwrap();
13736
13737 let multi_buffer = cx.new(|cx| {
13738 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13739 multi_buffer.push_excerpts(
13740 buffer.clone(),
13741 [ExcerptRange::new(0..first_excerpt_end)],
13742 cx,
13743 );
13744 multi_buffer.push_excerpts(
13745 buffer.clone(),
13746 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13747 cx,
13748 );
13749 multi_buffer
13750 });
13751
13752 let editor = workspace
13753 .update(cx, |_, window, cx| {
13754 cx.new(|cx| {
13755 Editor::new(
13756 EditorMode::Full {
13757 scale_ui_elements_with_buffer_font_size: false,
13758 show_active_line_background: false,
13759 sized_by_content: false,
13760 },
13761 multi_buffer.clone(),
13762 Some(project.clone()),
13763 window,
13764 cx,
13765 )
13766 })
13767 })
13768 .unwrap();
13769
13770 let pane = workspace
13771 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13772 .unwrap();
13773 pane.update_in(cx, |pane, window, cx| {
13774 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13775 });
13776
13777 let fake_server = fake_servers.next().await.unwrap();
13778
13779 editor.update_in(cx, |editor, window, cx| {
13780 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13781 s.select_ranges([
13782 Point::new(1, 11)..Point::new(1, 11),
13783 Point::new(7, 11)..Point::new(7, 11),
13784 ])
13785 });
13786
13787 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13788 });
13789
13790 editor.update_in(cx, |editor, window, cx| {
13791 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13792 });
13793
13794 fake_server
13795 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13796 let completion_item = lsp::CompletionItem {
13797 label: "saturating_sub()".into(),
13798 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13799 lsp::InsertReplaceEdit {
13800 new_text: "saturating_sub()".to_owned(),
13801 insert: lsp::Range::new(
13802 lsp::Position::new(7, 7),
13803 lsp::Position::new(7, 11),
13804 ),
13805 replace: lsp::Range::new(
13806 lsp::Position::new(7, 7),
13807 lsp::Position::new(7, 13),
13808 ),
13809 },
13810 )),
13811 ..lsp::CompletionItem::default()
13812 };
13813
13814 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13815 })
13816 .next()
13817 .await
13818 .unwrap();
13819
13820 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13821 .await;
13822
13823 editor
13824 .update_in(cx, |editor, window, cx| {
13825 editor
13826 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13827 .unwrap()
13828 })
13829 .await
13830 .unwrap();
13831
13832 editor.update(cx, |editor, cx| {
13833 assert_text_with_selections(editor, expected_multibuffer, cx);
13834 })
13835}
13836
13837#[gpui::test]
13838async fn test_completion(cx: &mut TestAppContext) {
13839 init_test(cx, |_| {});
13840
13841 let mut cx = EditorLspTestContext::new_rust(
13842 lsp::ServerCapabilities {
13843 completion_provider: Some(lsp::CompletionOptions {
13844 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13845 resolve_provider: Some(true),
13846 ..Default::default()
13847 }),
13848 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13849 ..Default::default()
13850 },
13851 cx,
13852 )
13853 .await;
13854 let counter = Arc::new(AtomicUsize::new(0));
13855
13856 cx.set_state(indoc! {"
13857 oneˇ
13858 two
13859 three
13860 "});
13861 cx.simulate_keystroke(".");
13862 handle_completion_request(
13863 indoc! {"
13864 one.|<>
13865 two
13866 three
13867 "},
13868 vec!["first_completion", "second_completion"],
13869 true,
13870 counter.clone(),
13871 &mut cx,
13872 )
13873 .await;
13874 cx.condition(|editor, _| editor.context_menu_visible())
13875 .await;
13876 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13877
13878 let _handler = handle_signature_help_request(
13879 &mut cx,
13880 lsp::SignatureHelp {
13881 signatures: vec![lsp::SignatureInformation {
13882 label: "test signature".to_string(),
13883 documentation: None,
13884 parameters: Some(vec![lsp::ParameterInformation {
13885 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13886 documentation: None,
13887 }]),
13888 active_parameter: None,
13889 }],
13890 active_signature: None,
13891 active_parameter: None,
13892 },
13893 );
13894 cx.update_editor(|editor, window, cx| {
13895 assert!(
13896 !editor.signature_help_state.is_shown(),
13897 "No signature help was called for"
13898 );
13899 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13900 });
13901 cx.run_until_parked();
13902 cx.update_editor(|editor, _, _| {
13903 assert!(
13904 !editor.signature_help_state.is_shown(),
13905 "No signature help should be shown when completions menu is open"
13906 );
13907 });
13908
13909 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13910 editor.context_menu_next(&Default::default(), window, cx);
13911 editor
13912 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13913 .unwrap()
13914 });
13915 cx.assert_editor_state(indoc! {"
13916 one.second_completionˇ
13917 two
13918 three
13919 "});
13920
13921 handle_resolve_completion_request(
13922 &mut cx,
13923 Some(vec![
13924 (
13925 //This overlaps with the primary completion edit which is
13926 //misbehavior from the LSP spec, test that we filter it out
13927 indoc! {"
13928 one.second_ˇcompletion
13929 two
13930 threeˇ
13931 "},
13932 "overlapping additional edit",
13933 ),
13934 (
13935 indoc! {"
13936 one.second_completion
13937 two
13938 threeˇ
13939 "},
13940 "\nadditional edit",
13941 ),
13942 ]),
13943 )
13944 .await;
13945 apply_additional_edits.await.unwrap();
13946 cx.assert_editor_state(indoc! {"
13947 one.second_completionˇ
13948 two
13949 three
13950 additional edit
13951 "});
13952
13953 cx.set_state(indoc! {"
13954 one.second_completion
13955 twoˇ
13956 threeˇ
13957 additional edit
13958 "});
13959 cx.simulate_keystroke(" ");
13960 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13961 cx.simulate_keystroke("s");
13962 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13963
13964 cx.assert_editor_state(indoc! {"
13965 one.second_completion
13966 two sˇ
13967 three sˇ
13968 additional edit
13969 "});
13970 handle_completion_request(
13971 indoc! {"
13972 one.second_completion
13973 two s
13974 three <s|>
13975 additional edit
13976 "},
13977 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13978 true,
13979 counter.clone(),
13980 &mut cx,
13981 )
13982 .await;
13983 cx.condition(|editor, _| editor.context_menu_visible())
13984 .await;
13985 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13986
13987 cx.simulate_keystroke("i");
13988
13989 handle_completion_request(
13990 indoc! {"
13991 one.second_completion
13992 two si
13993 three <si|>
13994 additional edit
13995 "},
13996 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13997 true,
13998 counter.clone(),
13999 &mut cx,
14000 )
14001 .await;
14002 cx.condition(|editor, _| editor.context_menu_visible())
14003 .await;
14004 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14005
14006 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14007 editor
14008 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14009 .unwrap()
14010 });
14011 cx.assert_editor_state(indoc! {"
14012 one.second_completion
14013 two sixth_completionˇ
14014 three sixth_completionˇ
14015 additional edit
14016 "});
14017
14018 apply_additional_edits.await.unwrap();
14019
14020 update_test_language_settings(&mut cx, |settings| {
14021 settings.defaults.show_completions_on_input = Some(false);
14022 });
14023 cx.set_state("editorˇ");
14024 cx.simulate_keystroke(".");
14025 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14026 cx.simulate_keystrokes("c l o");
14027 cx.assert_editor_state("editor.cloˇ");
14028 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14029 cx.update_editor(|editor, window, cx| {
14030 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14031 });
14032 handle_completion_request(
14033 "editor.<clo|>",
14034 vec!["close", "clobber"],
14035 true,
14036 counter.clone(),
14037 &mut cx,
14038 )
14039 .await;
14040 cx.condition(|editor, _| editor.context_menu_visible())
14041 .await;
14042 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14043
14044 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14045 editor
14046 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14047 .unwrap()
14048 });
14049 cx.assert_editor_state("editor.clobberˇ");
14050 handle_resolve_completion_request(&mut cx, None).await;
14051 apply_additional_edits.await.unwrap();
14052}
14053
14054#[gpui::test]
14055async fn test_completion_reuse(cx: &mut TestAppContext) {
14056 init_test(cx, |_| {});
14057
14058 let mut cx = EditorLspTestContext::new_rust(
14059 lsp::ServerCapabilities {
14060 completion_provider: Some(lsp::CompletionOptions {
14061 trigger_characters: Some(vec![".".to_string()]),
14062 ..Default::default()
14063 }),
14064 ..Default::default()
14065 },
14066 cx,
14067 )
14068 .await;
14069
14070 let counter = Arc::new(AtomicUsize::new(0));
14071 cx.set_state("objˇ");
14072 cx.simulate_keystroke(".");
14073
14074 // Initial completion request returns complete results
14075 let is_incomplete = false;
14076 handle_completion_request(
14077 "obj.|<>",
14078 vec!["a", "ab", "abc"],
14079 is_incomplete,
14080 counter.clone(),
14081 &mut cx,
14082 )
14083 .await;
14084 cx.run_until_parked();
14085 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14086 cx.assert_editor_state("obj.ˇ");
14087 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14088
14089 // Type "a" - filters existing completions
14090 cx.simulate_keystroke("a");
14091 cx.run_until_parked();
14092 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14093 cx.assert_editor_state("obj.aˇ");
14094 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14095
14096 // Type "b" - filters existing completions
14097 cx.simulate_keystroke("b");
14098 cx.run_until_parked();
14099 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14100 cx.assert_editor_state("obj.abˇ");
14101 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14102
14103 // Type "c" - filters existing completions
14104 cx.simulate_keystroke("c");
14105 cx.run_until_parked();
14106 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14107 cx.assert_editor_state("obj.abcˇ");
14108 check_displayed_completions(vec!["abc"], &mut cx);
14109
14110 // Backspace to delete "c" - filters existing completions
14111 cx.update_editor(|editor, window, cx| {
14112 editor.backspace(&Backspace, window, cx);
14113 });
14114 cx.run_until_parked();
14115 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14116 cx.assert_editor_state("obj.abˇ");
14117 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14118
14119 // Moving cursor to the left dismisses menu.
14120 cx.update_editor(|editor, window, cx| {
14121 editor.move_left(&MoveLeft, window, cx);
14122 });
14123 cx.run_until_parked();
14124 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14125 cx.assert_editor_state("obj.aˇb");
14126 cx.update_editor(|editor, _, _| {
14127 assert_eq!(editor.context_menu_visible(), false);
14128 });
14129
14130 // Type "b" - new request
14131 cx.simulate_keystroke("b");
14132 let is_incomplete = false;
14133 handle_completion_request(
14134 "obj.<ab|>a",
14135 vec!["ab", "abc"],
14136 is_incomplete,
14137 counter.clone(),
14138 &mut cx,
14139 )
14140 .await;
14141 cx.run_until_parked();
14142 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14143 cx.assert_editor_state("obj.abˇb");
14144 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14145
14146 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14147 cx.update_editor(|editor, window, cx| {
14148 editor.backspace(&Backspace, window, cx);
14149 });
14150 let is_incomplete = false;
14151 handle_completion_request(
14152 "obj.<a|>b",
14153 vec!["a", "ab", "abc"],
14154 is_incomplete,
14155 counter.clone(),
14156 &mut cx,
14157 )
14158 .await;
14159 cx.run_until_parked();
14160 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14161 cx.assert_editor_state("obj.aˇb");
14162 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14163
14164 // Backspace to delete "a" - dismisses menu.
14165 cx.update_editor(|editor, window, cx| {
14166 editor.backspace(&Backspace, window, cx);
14167 });
14168 cx.run_until_parked();
14169 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14170 cx.assert_editor_state("obj.ˇb");
14171 cx.update_editor(|editor, _, _| {
14172 assert_eq!(editor.context_menu_visible(), false);
14173 });
14174}
14175
14176#[gpui::test]
14177async fn test_word_completion(cx: &mut TestAppContext) {
14178 let lsp_fetch_timeout_ms = 10;
14179 init_test(cx, |language_settings| {
14180 language_settings.defaults.completions = Some(CompletionSettingsContent {
14181 words_min_length: Some(0),
14182 lsp_fetch_timeout_ms: Some(10),
14183 lsp_insert_mode: Some(LspInsertMode::Insert),
14184 ..Default::default()
14185 });
14186 });
14187
14188 let mut cx = EditorLspTestContext::new_rust(
14189 lsp::ServerCapabilities {
14190 completion_provider: Some(lsp::CompletionOptions {
14191 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14192 ..lsp::CompletionOptions::default()
14193 }),
14194 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14195 ..lsp::ServerCapabilities::default()
14196 },
14197 cx,
14198 )
14199 .await;
14200
14201 let throttle_completions = Arc::new(AtomicBool::new(false));
14202
14203 let lsp_throttle_completions = throttle_completions.clone();
14204 let _completion_requests_handler =
14205 cx.lsp
14206 .server
14207 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14208 let lsp_throttle_completions = lsp_throttle_completions.clone();
14209 let cx = cx.clone();
14210 async move {
14211 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14212 cx.background_executor()
14213 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14214 .await;
14215 }
14216 Ok(Some(lsp::CompletionResponse::Array(vec![
14217 lsp::CompletionItem {
14218 label: "first".into(),
14219 ..lsp::CompletionItem::default()
14220 },
14221 lsp::CompletionItem {
14222 label: "last".into(),
14223 ..lsp::CompletionItem::default()
14224 },
14225 ])))
14226 }
14227 });
14228
14229 cx.set_state(indoc! {"
14230 oneˇ
14231 two
14232 three
14233 "});
14234 cx.simulate_keystroke(".");
14235 cx.executor().run_until_parked();
14236 cx.condition(|editor, _| editor.context_menu_visible())
14237 .await;
14238 cx.update_editor(|editor, window, cx| {
14239 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14240 {
14241 assert_eq!(
14242 completion_menu_entries(menu),
14243 &["first", "last"],
14244 "When LSP server is fast to reply, no fallback word completions are used"
14245 );
14246 } else {
14247 panic!("expected completion menu to be open");
14248 }
14249 editor.cancel(&Cancel, window, cx);
14250 });
14251 cx.executor().run_until_parked();
14252 cx.condition(|editor, _| !editor.context_menu_visible())
14253 .await;
14254
14255 throttle_completions.store(true, atomic::Ordering::Release);
14256 cx.simulate_keystroke(".");
14257 cx.executor()
14258 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14259 cx.executor().run_until_parked();
14260 cx.condition(|editor, _| editor.context_menu_visible())
14261 .await;
14262 cx.update_editor(|editor, _, _| {
14263 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14264 {
14265 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14266 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14267 } else {
14268 panic!("expected completion menu to be open");
14269 }
14270 });
14271}
14272
14273#[gpui::test]
14274async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14275 init_test(cx, |language_settings| {
14276 language_settings.defaults.completions = Some(CompletionSettingsContent {
14277 words: Some(WordsCompletionMode::Enabled),
14278 words_min_length: Some(0),
14279 lsp_insert_mode: Some(LspInsertMode::Insert),
14280 ..Default::default()
14281 });
14282 });
14283
14284 let mut cx = EditorLspTestContext::new_rust(
14285 lsp::ServerCapabilities {
14286 completion_provider: Some(lsp::CompletionOptions {
14287 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14288 ..lsp::CompletionOptions::default()
14289 }),
14290 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14291 ..lsp::ServerCapabilities::default()
14292 },
14293 cx,
14294 )
14295 .await;
14296
14297 let _completion_requests_handler =
14298 cx.lsp
14299 .server
14300 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14301 Ok(Some(lsp::CompletionResponse::Array(vec![
14302 lsp::CompletionItem {
14303 label: "first".into(),
14304 ..lsp::CompletionItem::default()
14305 },
14306 lsp::CompletionItem {
14307 label: "last".into(),
14308 ..lsp::CompletionItem::default()
14309 },
14310 ])))
14311 });
14312
14313 cx.set_state(indoc! {"ˇ
14314 first
14315 last
14316 second
14317 "});
14318 cx.simulate_keystroke(".");
14319 cx.executor().run_until_parked();
14320 cx.condition(|editor, _| editor.context_menu_visible())
14321 .await;
14322 cx.update_editor(|editor, _, _| {
14323 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14324 {
14325 assert_eq!(
14326 completion_menu_entries(menu),
14327 &["first", "last", "second"],
14328 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14329 );
14330 } else {
14331 panic!("expected completion menu to be open");
14332 }
14333 });
14334}
14335
14336#[gpui::test]
14337async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14338 init_test(cx, |language_settings| {
14339 language_settings.defaults.completions = Some(CompletionSettingsContent {
14340 words: Some(WordsCompletionMode::Disabled),
14341 words_min_length: Some(0),
14342 lsp_insert_mode: Some(LspInsertMode::Insert),
14343 ..Default::default()
14344 });
14345 });
14346
14347 let mut cx = EditorLspTestContext::new_rust(
14348 lsp::ServerCapabilities {
14349 completion_provider: Some(lsp::CompletionOptions {
14350 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14351 ..lsp::CompletionOptions::default()
14352 }),
14353 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14354 ..lsp::ServerCapabilities::default()
14355 },
14356 cx,
14357 )
14358 .await;
14359
14360 let _completion_requests_handler =
14361 cx.lsp
14362 .server
14363 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14364 panic!("LSP completions should not be queried when dealing with word completions")
14365 });
14366
14367 cx.set_state(indoc! {"ˇ
14368 first
14369 last
14370 second
14371 "});
14372 cx.update_editor(|editor, window, cx| {
14373 editor.show_word_completions(&ShowWordCompletions, window, cx);
14374 });
14375 cx.executor().run_until_parked();
14376 cx.condition(|editor, _| editor.context_menu_visible())
14377 .await;
14378 cx.update_editor(|editor, _, _| {
14379 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14380 {
14381 assert_eq!(
14382 completion_menu_entries(menu),
14383 &["first", "last", "second"],
14384 "`ShowWordCompletions` action should show word completions"
14385 );
14386 } else {
14387 panic!("expected completion menu to be open");
14388 }
14389 });
14390
14391 cx.simulate_keystroke("l");
14392 cx.executor().run_until_parked();
14393 cx.condition(|editor, _| editor.context_menu_visible())
14394 .await;
14395 cx.update_editor(|editor, _, _| {
14396 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14397 {
14398 assert_eq!(
14399 completion_menu_entries(menu),
14400 &["last"],
14401 "After showing word completions, further editing should filter them and not query the LSP"
14402 );
14403 } else {
14404 panic!("expected completion menu to be open");
14405 }
14406 });
14407}
14408
14409#[gpui::test]
14410async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14411 init_test(cx, |language_settings| {
14412 language_settings.defaults.completions = Some(CompletionSettingsContent {
14413 words_min_length: Some(0),
14414 lsp: Some(false),
14415 lsp_insert_mode: Some(LspInsertMode::Insert),
14416 ..Default::default()
14417 });
14418 });
14419
14420 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14421
14422 cx.set_state(indoc! {"ˇ
14423 0_usize
14424 let
14425 33
14426 4.5f32
14427 "});
14428 cx.update_editor(|editor, window, cx| {
14429 editor.show_completions(&ShowCompletions::default(), window, cx);
14430 });
14431 cx.executor().run_until_parked();
14432 cx.condition(|editor, _| editor.context_menu_visible())
14433 .await;
14434 cx.update_editor(|editor, window, cx| {
14435 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14436 {
14437 assert_eq!(
14438 completion_menu_entries(menu),
14439 &["let"],
14440 "With no digits in the completion query, no digits should be in the word completions"
14441 );
14442 } else {
14443 panic!("expected completion menu to be open");
14444 }
14445 editor.cancel(&Cancel, window, cx);
14446 });
14447
14448 cx.set_state(indoc! {"3ˇ
14449 0_usize
14450 let
14451 3
14452 33.35f32
14453 "});
14454 cx.update_editor(|editor, window, cx| {
14455 editor.show_completions(&ShowCompletions::default(), window, cx);
14456 });
14457 cx.executor().run_until_parked();
14458 cx.condition(|editor, _| editor.context_menu_visible())
14459 .await;
14460 cx.update_editor(|editor, _, _| {
14461 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14462 {
14463 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14464 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14465 } else {
14466 panic!("expected completion menu to be open");
14467 }
14468 });
14469}
14470
14471#[gpui::test]
14472async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14473 init_test(cx, |language_settings| {
14474 language_settings.defaults.completions = Some(CompletionSettingsContent {
14475 words: Some(WordsCompletionMode::Enabled),
14476 words_min_length: Some(3),
14477 lsp_insert_mode: Some(LspInsertMode::Insert),
14478 ..Default::default()
14479 });
14480 });
14481
14482 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14483 cx.set_state(indoc! {"ˇ
14484 wow
14485 wowen
14486 wowser
14487 "});
14488 cx.simulate_keystroke("w");
14489 cx.executor().run_until_parked();
14490 cx.update_editor(|editor, _, _| {
14491 if editor.context_menu.borrow_mut().is_some() {
14492 panic!(
14493 "expected completion menu to be hidden, as words completion threshold is not met"
14494 );
14495 }
14496 });
14497
14498 cx.update_editor(|editor, window, cx| {
14499 editor.show_word_completions(&ShowWordCompletions, window, cx);
14500 });
14501 cx.executor().run_until_parked();
14502 cx.update_editor(|editor, window, cx| {
14503 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14504 {
14505 assert_eq!(completion_menu_entries(menu), &["wowser", "wowen", "wow"], "Even though the threshold is not met, invoking word completions with an action should provide the completions");
14506 } else {
14507 panic!("expected completion menu to be open after the word completions are called with an action");
14508 }
14509
14510 editor.cancel(&Cancel, window, cx);
14511 });
14512 cx.update_editor(|editor, _, _| {
14513 if editor.context_menu.borrow_mut().is_some() {
14514 panic!("expected completion menu to be hidden after canceling");
14515 }
14516 });
14517
14518 cx.simulate_keystroke("o");
14519 cx.executor().run_until_parked();
14520 cx.update_editor(|editor, _, _| {
14521 if editor.context_menu.borrow_mut().is_some() {
14522 panic!(
14523 "expected completion menu to be hidden, as words completion threshold is not met still"
14524 );
14525 }
14526 });
14527
14528 cx.simulate_keystroke("w");
14529 cx.executor().run_until_parked();
14530 cx.update_editor(|editor, _, _| {
14531 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14532 {
14533 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14534 } else {
14535 panic!("expected completion menu to be open after the word completions threshold is met");
14536 }
14537 });
14538}
14539
14540#[gpui::test]
14541async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14542 init_test(cx, |language_settings| {
14543 language_settings.defaults.completions = Some(CompletionSettingsContent {
14544 words: Some(WordsCompletionMode::Enabled),
14545 words_min_length: Some(0),
14546 lsp_insert_mode: Some(LspInsertMode::Insert),
14547 ..Default::default()
14548 });
14549 });
14550
14551 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14552 cx.update_editor(|editor, _, _| {
14553 editor.disable_word_completions();
14554 });
14555 cx.set_state(indoc! {"ˇ
14556 wow
14557 wowen
14558 wowser
14559 "});
14560 cx.simulate_keystroke("w");
14561 cx.executor().run_until_parked();
14562 cx.update_editor(|editor, _, _| {
14563 if editor.context_menu.borrow_mut().is_some() {
14564 panic!(
14565 "expected completion menu to be hidden, as words completion are disabled for this editor"
14566 );
14567 }
14568 });
14569
14570 cx.update_editor(|editor, window, cx| {
14571 editor.show_word_completions(&ShowWordCompletions, window, cx);
14572 });
14573 cx.executor().run_until_parked();
14574 cx.update_editor(|editor, _, _| {
14575 if editor.context_menu.borrow_mut().is_some() {
14576 panic!(
14577 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14578 );
14579 }
14580 });
14581}
14582
14583fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14584 let position = || lsp::Position {
14585 line: params.text_document_position.position.line,
14586 character: params.text_document_position.position.character,
14587 };
14588 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14589 range: lsp::Range {
14590 start: position(),
14591 end: position(),
14592 },
14593 new_text: text.to_string(),
14594 }))
14595}
14596
14597#[gpui::test]
14598async fn test_multiline_completion(cx: &mut TestAppContext) {
14599 init_test(cx, |_| {});
14600
14601 let fs = FakeFs::new(cx.executor());
14602 fs.insert_tree(
14603 path!("/a"),
14604 json!({
14605 "main.ts": "a",
14606 }),
14607 )
14608 .await;
14609
14610 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14611 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14612 let typescript_language = Arc::new(Language::new(
14613 LanguageConfig {
14614 name: "TypeScript".into(),
14615 matcher: LanguageMatcher {
14616 path_suffixes: vec!["ts".to_string()],
14617 ..LanguageMatcher::default()
14618 },
14619 line_comments: vec!["// ".into()],
14620 ..LanguageConfig::default()
14621 },
14622 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14623 ));
14624 language_registry.add(typescript_language.clone());
14625 let mut fake_servers = language_registry.register_fake_lsp(
14626 "TypeScript",
14627 FakeLspAdapter {
14628 capabilities: lsp::ServerCapabilities {
14629 completion_provider: Some(lsp::CompletionOptions {
14630 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14631 ..lsp::CompletionOptions::default()
14632 }),
14633 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14634 ..lsp::ServerCapabilities::default()
14635 },
14636 // Emulate vtsls label generation
14637 label_for_completion: Some(Box::new(|item, _| {
14638 let text = if let Some(description) = item
14639 .label_details
14640 .as_ref()
14641 .and_then(|label_details| label_details.description.as_ref())
14642 {
14643 format!("{} {}", item.label, description)
14644 } else if let Some(detail) = &item.detail {
14645 format!("{} {}", item.label, detail)
14646 } else {
14647 item.label.clone()
14648 };
14649 let len = text.len();
14650 Some(language::CodeLabel {
14651 text,
14652 runs: Vec::new(),
14653 filter_range: 0..len,
14654 })
14655 })),
14656 ..FakeLspAdapter::default()
14657 },
14658 );
14659 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14660 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14661 let worktree_id = workspace
14662 .update(cx, |workspace, _window, cx| {
14663 workspace.project().update(cx, |project, cx| {
14664 project.worktrees(cx).next().unwrap().read(cx).id()
14665 })
14666 })
14667 .unwrap();
14668 let _buffer = project
14669 .update(cx, |project, cx| {
14670 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14671 })
14672 .await
14673 .unwrap();
14674 let editor = workspace
14675 .update(cx, |workspace, window, cx| {
14676 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14677 })
14678 .unwrap()
14679 .await
14680 .unwrap()
14681 .downcast::<Editor>()
14682 .unwrap();
14683 let fake_server = fake_servers.next().await.unwrap();
14684
14685 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14686 let multiline_label_2 = "a\nb\nc\n";
14687 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14688 let multiline_description = "d\ne\nf\n";
14689 let multiline_detail_2 = "g\nh\ni\n";
14690
14691 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14692 move |params, _| async move {
14693 Ok(Some(lsp::CompletionResponse::Array(vec![
14694 lsp::CompletionItem {
14695 label: multiline_label.to_string(),
14696 text_edit: gen_text_edit(¶ms, "new_text_1"),
14697 ..lsp::CompletionItem::default()
14698 },
14699 lsp::CompletionItem {
14700 label: "single line label 1".to_string(),
14701 detail: Some(multiline_detail.to_string()),
14702 text_edit: gen_text_edit(¶ms, "new_text_2"),
14703 ..lsp::CompletionItem::default()
14704 },
14705 lsp::CompletionItem {
14706 label: "single line label 2".to_string(),
14707 label_details: Some(lsp::CompletionItemLabelDetails {
14708 description: Some(multiline_description.to_string()),
14709 detail: None,
14710 }),
14711 text_edit: gen_text_edit(¶ms, "new_text_2"),
14712 ..lsp::CompletionItem::default()
14713 },
14714 lsp::CompletionItem {
14715 label: multiline_label_2.to_string(),
14716 detail: Some(multiline_detail_2.to_string()),
14717 text_edit: gen_text_edit(¶ms, "new_text_3"),
14718 ..lsp::CompletionItem::default()
14719 },
14720 lsp::CompletionItem {
14721 label: "Label with many spaces and \t but without newlines".to_string(),
14722 detail: Some(
14723 "Details with many spaces and \t but without newlines".to_string(),
14724 ),
14725 text_edit: gen_text_edit(¶ms, "new_text_4"),
14726 ..lsp::CompletionItem::default()
14727 },
14728 ])))
14729 },
14730 );
14731
14732 editor.update_in(cx, |editor, window, cx| {
14733 cx.focus_self(window);
14734 editor.move_to_end(&MoveToEnd, window, cx);
14735 editor.handle_input(".", window, cx);
14736 });
14737 cx.run_until_parked();
14738 completion_handle.next().await.unwrap();
14739
14740 editor.update(cx, |editor, _| {
14741 assert!(editor.context_menu_visible());
14742 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14743 {
14744 let completion_labels = menu
14745 .completions
14746 .borrow()
14747 .iter()
14748 .map(|c| c.label.text.clone())
14749 .collect::<Vec<_>>();
14750 assert_eq!(
14751 completion_labels,
14752 &[
14753 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14754 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14755 "single line label 2 d e f ",
14756 "a b c g h i ",
14757 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14758 ],
14759 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14760 );
14761
14762 for completion in menu
14763 .completions
14764 .borrow()
14765 .iter() {
14766 assert_eq!(
14767 completion.label.filter_range,
14768 0..completion.label.text.len(),
14769 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14770 );
14771 }
14772 } else {
14773 panic!("expected completion menu to be open");
14774 }
14775 });
14776}
14777
14778#[gpui::test]
14779async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14780 init_test(cx, |_| {});
14781 let mut cx = EditorLspTestContext::new_rust(
14782 lsp::ServerCapabilities {
14783 completion_provider: Some(lsp::CompletionOptions {
14784 trigger_characters: Some(vec![".".to_string()]),
14785 ..Default::default()
14786 }),
14787 ..Default::default()
14788 },
14789 cx,
14790 )
14791 .await;
14792 cx.lsp
14793 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14794 Ok(Some(lsp::CompletionResponse::Array(vec![
14795 lsp::CompletionItem {
14796 label: "first".into(),
14797 ..Default::default()
14798 },
14799 lsp::CompletionItem {
14800 label: "last".into(),
14801 ..Default::default()
14802 },
14803 ])))
14804 });
14805 cx.set_state("variableˇ");
14806 cx.simulate_keystroke(".");
14807 cx.executor().run_until_parked();
14808
14809 cx.update_editor(|editor, _, _| {
14810 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14811 {
14812 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14813 } else {
14814 panic!("expected completion menu to be open");
14815 }
14816 });
14817
14818 cx.update_editor(|editor, window, cx| {
14819 editor.move_page_down(&MovePageDown::default(), window, cx);
14820 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14821 {
14822 assert!(
14823 menu.selected_item == 1,
14824 "expected PageDown to select the last item from the context menu"
14825 );
14826 } else {
14827 panic!("expected completion menu to stay open after PageDown");
14828 }
14829 });
14830
14831 cx.update_editor(|editor, window, cx| {
14832 editor.move_page_up(&MovePageUp::default(), window, cx);
14833 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14834 {
14835 assert!(
14836 menu.selected_item == 0,
14837 "expected PageUp to select the first item from the context menu"
14838 );
14839 } else {
14840 panic!("expected completion menu to stay open after PageUp");
14841 }
14842 });
14843}
14844
14845#[gpui::test]
14846async fn test_as_is_completions(cx: &mut TestAppContext) {
14847 init_test(cx, |_| {});
14848 let mut cx = EditorLspTestContext::new_rust(
14849 lsp::ServerCapabilities {
14850 completion_provider: Some(lsp::CompletionOptions {
14851 ..Default::default()
14852 }),
14853 ..Default::default()
14854 },
14855 cx,
14856 )
14857 .await;
14858 cx.lsp
14859 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14860 Ok(Some(lsp::CompletionResponse::Array(vec![
14861 lsp::CompletionItem {
14862 label: "unsafe".into(),
14863 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14864 range: lsp::Range {
14865 start: lsp::Position {
14866 line: 1,
14867 character: 2,
14868 },
14869 end: lsp::Position {
14870 line: 1,
14871 character: 3,
14872 },
14873 },
14874 new_text: "unsafe".to_string(),
14875 })),
14876 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14877 ..Default::default()
14878 },
14879 ])))
14880 });
14881 cx.set_state("fn a() {}\n nˇ");
14882 cx.executor().run_until_parked();
14883 cx.update_editor(|editor, window, cx| {
14884 editor.show_completions(
14885 &ShowCompletions {
14886 trigger: Some("\n".into()),
14887 },
14888 window,
14889 cx,
14890 );
14891 });
14892 cx.executor().run_until_parked();
14893
14894 cx.update_editor(|editor, window, cx| {
14895 editor.confirm_completion(&Default::default(), window, cx)
14896 });
14897 cx.executor().run_until_parked();
14898 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14899}
14900
14901#[gpui::test]
14902async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14903 init_test(cx, |_| {});
14904 let language =
14905 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14906 let mut cx = EditorLspTestContext::new(
14907 language,
14908 lsp::ServerCapabilities {
14909 completion_provider: Some(lsp::CompletionOptions {
14910 ..lsp::CompletionOptions::default()
14911 }),
14912 ..lsp::ServerCapabilities::default()
14913 },
14914 cx,
14915 )
14916 .await;
14917
14918 cx.set_state(
14919 "#ifndef BAR_H
14920#define BAR_H
14921
14922#include <stdbool.h>
14923
14924int fn_branch(bool do_branch1, bool do_branch2);
14925
14926#endif // BAR_H
14927ˇ",
14928 );
14929 cx.executor().run_until_parked();
14930 cx.update_editor(|editor, window, cx| {
14931 editor.handle_input("#", window, cx);
14932 });
14933 cx.executor().run_until_parked();
14934 cx.update_editor(|editor, window, cx| {
14935 editor.handle_input("i", window, cx);
14936 });
14937 cx.executor().run_until_parked();
14938 cx.update_editor(|editor, window, cx| {
14939 editor.handle_input("n", window, cx);
14940 });
14941 cx.executor().run_until_parked();
14942 cx.assert_editor_state(
14943 "#ifndef BAR_H
14944#define BAR_H
14945
14946#include <stdbool.h>
14947
14948int fn_branch(bool do_branch1, bool do_branch2);
14949
14950#endif // BAR_H
14951#inˇ",
14952 );
14953
14954 cx.lsp
14955 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14956 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14957 is_incomplete: false,
14958 item_defaults: None,
14959 items: vec![lsp::CompletionItem {
14960 kind: Some(lsp::CompletionItemKind::SNIPPET),
14961 label_details: Some(lsp::CompletionItemLabelDetails {
14962 detail: Some("header".to_string()),
14963 description: None,
14964 }),
14965 label: " include".to_string(),
14966 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14967 range: lsp::Range {
14968 start: lsp::Position {
14969 line: 8,
14970 character: 1,
14971 },
14972 end: lsp::Position {
14973 line: 8,
14974 character: 1,
14975 },
14976 },
14977 new_text: "include \"$0\"".to_string(),
14978 })),
14979 sort_text: Some("40b67681include".to_string()),
14980 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14981 filter_text: Some("include".to_string()),
14982 insert_text: Some("include \"$0\"".to_string()),
14983 ..lsp::CompletionItem::default()
14984 }],
14985 })))
14986 });
14987 cx.update_editor(|editor, window, cx| {
14988 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14989 });
14990 cx.executor().run_until_parked();
14991 cx.update_editor(|editor, window, cx| {
14992 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
14993 });
14994 cx.executor().run_until_parked();
14995 cx.assert_editor_state(
14996 "#ifndef BAR_H
14997#define BAR_H
14998
14999#include <stdbool.h>
15000
15001int fn_branch(bool do_branch1, bool do_branch2);
15002
15003#endif // BAR_H
15004#include \"ˇ\"",
15005 );
15006
15007 cx.lsp
15008 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15009 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15010 is_incomplete: true,
15011 item_defaults: None,
15012 items: vec![lsp::CompletionItem {
15013 kind: Some(lsp::CompletionItemKind::FILE),
15014 label: "AGL/".to_string(),
15015 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15016 range: lsp::Range {
15017 start: lsp::Position {
15018 line: 8,
15019 character: 10,
15020 },
15021 end: lsp::Position {
15022 line: 8,
15023 character: 11,
15024 },
15025 },
15026 new_text: "AGL/".to_string(),
15027 })),
15028 sort_text: Some("40b67681AGL/".to_string()),
15029 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15030 filter_text: Some("AGL/".to_string()),
15031 insert_text: Some("AGL/".to_string()),
15032 ..lsp::CompletionItem::default()
15033 }],
15034 })))
15035 });
15036 cx.update_editor(|editor, window, cx| {
15037 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15038 });
15039 cx.executor().run_until_parked();
15040 cx.update_editor(|editor, window, cx| {
15041 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15042 });
15043 cx.executor().run_until_parked();
15044 cx.assert_editor_state(
15045 r##"#ifndef BAR_H
15046#define BAR_H
15047
15048#include <stdbool.h>
15049
15050int fn_branch(bool do_branch1, bool do_branch2);
15051
15052#endif // BAR_H
15053#include "AGL/ˇ"##,
15054 );
15055
15056 cx.update_editor(|editor, window, cx| {
15057 editor.handle_input("\"", window, cx);
15058 });
15059 cx.executor().run_until_parked();
15060 cx.assert_editor_state(
15061 r##"#ifndef BAR_H
15062#define BAR_H
15063
15064#include <stdbool.h>
15065
15066int fn_branch(bool do_branch1, bool do_branch2);
15067
15068#endif // BAR_H
15069#include "AGL/"ˇ"##,
15070 );
15071}
15072
15073#[gpui::test]
15074async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15075 init_test(cx, |_| {});
15076
15077 let mut cx = EditorLspTestContext::new_rust(
15078 lsp::ServerCapabilities {
15079 completion_provider: Some(lsp::CompletionOptions {
15080 trigger_characters: Some(vec![".".to_string()]),
15081 resolve_provider: Some(true),
15082 ..Default::default()
15083 }),
15084 ..Default::default()
15085 },
15086 cx,
15087 )
15088 .await;
15089
15090 cx.set_state("fn main() { let a = 2ˇ; }");
15091 cx.simulate_keystroke(".");
15092 let completion_item = lsp::CompletionItem {
15093 label: "Some".into(),
15094 kind: Some(lsp::CompletionItemKind::SNIPPET),
15095 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15096 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15097 kind: lsp::MarkupKind::Markdown,
15098 value: "```rust\nSome(2)\n```".to_string(),
15099 })),
15100 deprecated: Some(false),
15101 sort_text: Some("Some".to_string()),
15102 filter_text: Some("Some".to_string()),
15103 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15104 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15105 range: lsp::Range {
15106 start: lsp::Position {
15107 line: 0,
15108 character: 22,
15109 },
15110 end: lsp::Position {
15111 line: 0,
15112 character: 22,
15113 },
15114 },
15115 new_text: "Some(2)".to_string(),
15116 })),
15117 additional_text_edits: Some(vec![lsp::TextEdit {
15118 range: lsp::Range {
15119 start: lsp::Position {
15120 line: 0,
15121 character: 20,
15122 },
15123 end: lsp::Position {
15124 line: 0,
15125 character: 22,
15126 },
15127 },
15128 new_text: "".to_string(),
15129 }]),
15130 ..Default::default()
15131 };
15132
15133 let closure_completion_item = completion_item.clone();
15134 let counter = Arc::new(AtomicUsize::new(0));
15135 let counter_clone = counter.clone();
15136 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15137 let task_completion_item = closure_completion_item.clone();
15138 counter_clone.fetch_add(1, atomic::Ordering::Release);
15139 async move {
15140 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15141 is_incomplete: true,
15142 item_defaults: None,
15143 items: vec![task_completion_item],
15144 })))
15145 }
15146 });
15147
15148 cx.condition(|editor, _| editor.context_menu_visible())
15149 .await;
15150 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15151 assert!(request.next().await.is_some());
15152 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15153
15154 cx.simulate_keystrokes("S o m");
15155 cx.condition(|editor, _| editor.context_menu_visible())
15156 .await;
15157 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15158 assert!(request.next().await.is_some());
15159 assert!(request.next().await.is_some());
15160 assert!(request.next().await.is_some());
15161 request.close();
15162 assert!(request.next().await.is_none());
15163 assert_eq!(
15164 counter.load(atomic::Ordering::Acquire),
15165 4,
15166 "With the completions menu open, only one LSP request should happen per input"
15167 );
15168}
15169
15170#[gpui::test]
15171async fn test_toggle_comment(cx: &mut TestAppContext) {
15172 init_test(cx, |_| {});
15173 let mut cx = EditorTestContext::new(cx).await;
15174 let language = Arc::new(Language::new(
15175 LanguageConfig {
15176 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15177 ..Default::default()
15178 },
15179 Some(tree_sitter_rust::LANGUAGE.into()),
15180 ));
15181 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15182
15183 // If multiple selections intersect a line, the line is only toggled once.
15184 cx.set_state(indoc! {"
15185 fn a() {
15186 «//b();
15187 ˇ»// «c();
15188 //ˇ» d();
15189 }
15190 "});
15191
15192 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15193
15194 cx.assert_editor_state(indoc! {"
15195 fn a() {
15196 «b();
15197 c();
15198 ˇ» d();
15199 }
15200 "});
15201
15202 // The comment prefix is inserted at the same column for every line in a
15203 // selection.
15204 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15205
15206 cx.assert_editor_state(indoc! {"
15207 fn a() {
15208 // «b();
15209 // c();
15210 ˇ»// d();
15211 }
15212 "});
15213
15214 // If a selection ends at the beginning of a line, that line is not toggled.
15215 cx.set_selections_state(indoc! {"
15216 fn a() {
15217 // b();
15218 «// c();
15219 ˇ» // d();
15220 }
15221 "});
15222
15223 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15224
15225 cx.assert_editor_state(indoc! {"
15226 fn a() {
15227 // b();
15228 «c();
15229 ˇ» // d();
15230 }
15231 "});
15232
15233 // If a selection span a single line and is empty, the line is toggled.
15234 cx.set_state(indoc! {"
15235 fn a() {
15236 a();
15237 b();
15238 ˇ
15239 }
15240 "});
15241
15242 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15243
15244 cx.assert_editor_state(indoc! {"
15245 fn a() {
15246 a();
15247 b();
15248 //•ˇ
15249 }
15250 "});
15251
15252 // If a selection span multiple lines, empty lines are not toggled.
15253 cx.set_state(indoc! {"
15254 fn a() {
15255 «a();
15256
15257 c();ˇ»
15258 }
15259 "});
15260
15261 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15262
15263 cx.assert_editor_state(indoc! {"
15264 fn a() {
15265 // «a();
15266
15267 // c();ˇ»
15268 }
15269 "});
15270
15271 // If a selection includes multiple comment prefixes, all lines are uncommented.
15272 cx.set_state(indoc! {"
15273 fn a() {
15274 «// a();
15275 /// b();
15276 //! c();ˇ»
15277 }
15278 "});
15279
15280 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15281
15282 cx.assert_editor_state(indoc! {"
15283 fn a() {
15284 «a();
15285 b();
15286 c();ˇ»
15287 }
15288 "});
15289}
15290
15291#[gpui::test]
15292async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15293 init_test(cx, |_| {});
15294 let mut cx = EditorTestContext::new(cx).await;
15295 let language = Arc::new(Language::new(
15296 LanguageConfig {
15297 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15298 ..Default::default()
15299 },
15300 Some(tree_sitter_rust::LANGUAGE.into()),
15301 ));
15302 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15303
15304 let toggle_comments = &ToggleComments {
15305 advance_downwards: false,
15306 ignore_indent: true,
15307 };
15308
15309 // If multiple selections intersect a line, the line is only toggled once.
15310 cx.set_state(indoc! {"
15311 fn a() {
15312 // «b();
15313 // c();
15314 // ˇ» d();
15315 }
15316 "});
15317
15318 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15319
15320 cx.assert_editor_state(indoc! {"
15321 fn a() {
15322 «b();
15323 c();
15324 ˇ» d();
15325 }
15326 "});
15327
15328 // The comment prefix is inserted at the beginning of each line
15329 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15330
15331 cx.assert_editor_state(indoc! {"
15332 fn a() {
15333 // «b();
15334 // c();
15335 // ˇ» d();
15336 }
15337 "});
15338
15339 // If a selection ends at the beginning of a line, that line is not toggled.
15340 cx.set_selections_state(indoc! {"
15341 fn a() {
15342 // b();
15343 // «c();
15344 ˇ»// d();
15345 }
15346 "});
15347
15348 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15349
15350 cx.assert_editor_state(indoc! {"
15351 fn a() {
15352 // b();
15353 «c();
15354 ˇ»// d();
15355 }
15356 "});
15357
15358 // If a selection span a single line and is empty, the line is toggled.
15359 cx.set_state(indoc! {"
15360 fn a() {
15361 a();
15362 b();
15363 ˇ
15364 }
15365 "});
15366
15367 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15368
15369 cx.assert_editor_state(indoc! {"
15370 fn a() {
15371 a();
15372 b();
15373 //ˇ
15374 }
15375 "});
15376
15377 // If a selection span multiple lines, empty lines are not toggled.
15378 cx.set_state(indoc! {"
15379 fn a() {
15380 «a();
15381
15382 c();ˇ»
15383 }
15384 "});
15385
15386 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15387
15388 cx.assert_editor_state(indoc! {"
15389 fn a() {
15390 // «a();
15391
15392 // c();ˇ»
15393 }
15394 "});
15395
15396 // If a selection includes multiple comment prefixes, all lines are uncommented.
15397 cx.set_state(indoc! {"
15398 fn a() {
15399 // «a();
15400 /// b();
15401 //! c();ˇ»
15402 }
15403 "});
15404
15405 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15406
15407 cx.assert_editor_state(indoc! {"
15408 fn a() {
15409 «a();
15410 b();
15411 c();ˇ»
15412 }
15413 "});
15414}
15415
15416#[gpui::test]
15417async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15418 init_test(cx, |_| {});
15419
15420 let language = Arc::new(Language::new(
15421 LanguageConfig {
15422 line_comments: vec!["// ".into()],
15423 ..Default::default()
15424 },
15425 Some(tree_sitter_rust::LANGUAGE.into()),
15426 ));
15427
15428 let mut cx = EditorTestContext::new(cx).await;
15429
15430 cx.language_registry().add(language.clone());
15431 cx.update_buffer(|buffer, cx| {
15432 buffer.set_language(Some(language), cx);
15433 });
15434
15435 let toggle_comments = &ToggleComments {
15436 advance_downwards: true,
15437 ignore_indent: false,
15438 };
15439
15440 // Single cursor on one line -> advance
15441 // Cursor moves horizontally 3 characters as well on non-blank line
15442 cx.set_state(indoc!(
15443 "fn a() {
15444 ˇdog();
15445 cat();
15446 }"
15447 ));
15448 cx.update_editor(|editor, window, cx| {
15449 editor.toggle_comments(toggle_comments, window, cx);
15450 });
15451 cx.assert_editor_state(indoc!(
15452 "fn a() {
15453 // dog();
15454 catˇ();
15455 }"
15456 ));
15457
15458 // Single selection on one line -> don't advance
15459 cx.set_state(indoc!(
15460 "fn a() {
15461 «dog()ˇ»;
15462 cat();
15463 }"
15464 ));
15465 cx.update_editor(|editor, window, cx| {
15466 editor.toggle_comments(toggle_comments, window, cx);
15467 });
15468 cx.assert_editor_state(indoc!(
15469 "fn a() {
15470 // «dog()ˇ»;
15471 cat();
15472 }"
15473 ));
15474
15475 // Multiple cursors on one line -> advance
15476 cx.set_state(indoc!(
15477 "fn a() {
15478 ˇdˇog();
15479 cat();
15480 }"
15481 ));
15482 cx.update_editor(|editor, window, cx| {
15483 editor.toggle_comments(toggle_comments, window, cx);
15484 });
15485 cx.assert_editor_state(indoc!(
15486 "fn a() {
15487 // dog();
15488 catˇ(ˇ);
15489 }"
15490 ));
15491
15492 // Multiple cursors on one line, with selection -> don't advance
15493 cx.set_state(indoc!(
15494 "fn a() {
15495 ˇdˇog«()ˇ»;
15496 cat();
15497 }"
15498 ));
15499 cx.update_editor(|editor, window, cx| {
15500 editor.toggle_comments(toggle_comments, window, cx);
15501 });
15502 cx.assert_editor_state(indoc!(
15503 "fn a() {
15504 // ˇdˇog«()ˇ»;
15505 cat();
15506 }"
15507 ));
15508
15509 // Single cursor on one line -> advance
15510 // Cursor moves to column 0 on blank line
15511 cx.set_state(indoc!(
15512 "fn a() {
15513 ˇdog();
15514
15515 cat();
15516 }"
15517 ));
15518 cx.update_editor(|editor, window, cx| {
15519 editor.toggle_comments(toggle_comments, window, cx);
15520 });
15521 cx.assert_editor_state(indoc!(
15522 "fn a() {
15523 // dog();
15524 ˇ
15525 cat();
15526 }"
15527 ));
15528
15529 // Single cursor on one line -> advance
15530 // Cursor starts and ends at column 0
15531 cx.set_state(indoc!(
15532 "fn a() {
15533 ˇ dog();
15534 cat();
15535 }"
15536 ));
15537 cx.update_editor(|editor, window, cx| {
15538 editor.toggle_comments(toggle_comments, window, cx);
15539 });
15540 cx.assert_editor_state(indoc!(
15541 "fn a() {
15542 // dog();
15543 ˇ cat();
15544 }"
15545 ));
15546}
15547
15548#[gpui::test]
15549async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15550 init_test(cx, |_| {});
15551
15552 let mut cx = EditorTestContext::new(cx).await;
15553
15554 let html_language = Arc::new(
15555 Language::new(
15556 LanguageConfig {
15557 name: "HTML".into(),
15558 block_comment: Some(BlockCommentConfig {
15559 start: "<!-- ".into(),
15560 prefix: "".into(),
15561 end: " -->".into(),
15562 tab_size: 0,
15563 }),
15564 ..Default::default()
15565 },
15566 Some(tree_sitter_html::LANGUAGE.into()),
15567 )
15568 .with_injection_query(
15569 r#"
15570 (script_element
15571 (raw_text) @injection.content
15572 (#set! injection.language "javascript"))
15573 "#,
15574 )
15575 .unwrap(),
15576 );
15577
15578 let javascript_language = Arc::new(Language::new(
15579 LanguageConfig {
15580 name: "JavaScript".into(),
15581 line_comments: vec!["// ".into()],
15582 ..Default::default()
15583 },
15584 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15585 ));
15586
15587 cx.language_registry().add(html_language.clone());
15588 cx.language_registry().add(javascript_language);
15589 cx.update_buffer(|buffer, cx| {
15590 buffer.set_language(Some(html_language), cx);
15591 });
15592
15593 // Toggle comments for empty selections
15594 cx.set_state(
15595 &r#"
15596 <p>A</p>ˇ
15597 <p>B</p>ˇ
15598 <p>C</p>ˇ
15599 "#
15600 .unindent(),
15601 );
15602 cx.update_editor(|editor, window, cx| {
15603 editor.toggle_comments(&ToggleComments::default(), window, cx)
15604 });
15605 cx.assert_editor_state(
15606 &r#"
15607 <!-- <p>A</p>ˇ -->
15608 <!-- <p>B</p>ˇ -->
15609 <!-- <p>C</p>ˇ -->
15610 "#
15611 .unindent(),
15612 );
15613 cx.update_editor(|editor, window, cx| {
15614 editor.toggle_comments(&ToggleComments::default(), window, cx)
15615 });
15616 cx.assert_editor_state(
15617 &r#"
15618 <p>A</p>ˇ
15619 <p>B</p>ˇ
15620 <p>C</p>ˇ
15621 "#
15622 .unindent(),
15623 );
15624
15625 // Toggle comments for mixture of empty and non-empty selections, where
15626 // multiple selections occupy a given line.
15627 cx.set_state(
15628 &r#"
15629 <p>A«</p>
15630 <p>ˇ»B</p>ˇ
15631 <p>C«</p>
15632 <p>ˇ»D</p>ˇ
15633 "#
15634 .unindent(),
15635 );
15636
15637 cx.update_editor(|editor, window, cx| {
15638 editor.toggle_comments(&ToggleComments::default(), window, cx)
15639 });
15640 cx.assert_editor_state(
15641 &r#"
15642 <!-- <p>A«</p>
15643 <p>ˇ»B</p>ˇ -->
15644 <!-- <p>C«</p>
15645 <p>ˇ»D</p>ˇ -->
15646 "#
15647 .unindent(),
15648 );
15649 cx.update_editor(|editor, window, cx| {
15650 editor.toggle_comments(&ToggleComments::default(), window, cx)
15651 });
15652 cx.assert_editor_state(
15653 &r#"
15654 <p>A«</p>
15655 <p>ˇ»B</p>ˇ
15656 <p>C«</p>
15657 <p>ˇ»D</p>ˇ
15658 "#
15659 .unindent(),
15660 );
15661
15662 // Toggle comments when different languages are active for different
15663 // selections.
15664 cx.set_state(
15665 &r#"
15666 ˇ<script>
15667 ˇvar x = new Y();
15668 ˇ</script>
15669 "#
15670 .unindent(),
15671 );
15672 cx.executor().run_until_parked();
15673 cx.update_editor(|editor, window, cx| {
15674 editor.toggle_comments(&ToggleComments::default(), window, cx)
15675 });
15676 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15677 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15678 cx.assert_editor_state(
15679 &r#"
15680 <!-- ˇ<script> -->
15681 // ˇvar x = new Y();
15682 <!-- ˇ</script> -->
15683 "#
15684 .unindent(),
15685 );
15686}
15687
15688#[gpui::test]
15689fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15690 init_test(cx, |_| {});
15691
15692 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15693 let multibuffer = cx.new(|cx| {
15694 let mut multibuffer = MultiBuffer::new(ReadWrite);
15695 multibuffer.push_excerpts(
15696 buffer.clone(),
15697 [
15698 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15699 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15700 ],
15701 cx,
15702 );
15703 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15704 multibuffer
15705 });
15706
15707 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15708 editor.update_in(cx, |editor, window, cx| {
15709 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15710 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15711 s.select_ranges([
15712 Point::new(0, 0)..Point::new(0, 0),
15713 Point::new(1, 0)..Point::new(1, 0),
15714 ])
15715 });
15716
15717 editor.handle_input("X", window, cx);
15718 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15719 assert_eq!(
15720 editor.selections.ranges(cx),
15721 [
15722 Point::new(0, 1)..Point::new(0, 1),
15723 Point::new(1, 1)..Point::new(1, 1),
15724 ]
15725 );
15726
15727 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15729 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15730 });
15731 editor.backspace(&Default::default(), window, cx);
15732 assert_eq!(editor.text(cx), "Xa\nbbb");
15733 assert_eq!(
15734 editor.selections.ranges(cx),
15735 [Point::new(1, 0)..Point::new(1, 0)]
15736 );
15737
15738 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15739 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15740 });
15741 editor.backspace(&Default::default(), window, cx);
15742 assert_eq!(editor.text(cx), "X\nbb");
15743 assert_eq!(
15744 editor.selections.ranges(cx),
15745 [Point::new(0, 1)..Point::new(0, 1)]
15746 );
15747 });
15748}
15749
15750#[gpui::test]
15751fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15752 init_test(cx, |_| {});
15753
15754 let markers = vec![('[', ']').into(), ('(', ')').into()];
15755 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15756 indoc! {"
15757 [aaaa
15758 (bbbb]
15759 cccc)",
15760 },
15761 markers.clone(),
15762 );
15763 let excerpt_ranges = markers.into_iter().map(|marker| {
15764 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15765 ExcerptRange::new(context)
15766 });
15767 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15768 let multibuffer = cx.new(|cx| {
15769 let mut multibuffer = MultiBuffer::new(ReadWrite);
15770 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15771 multibuffer
15772 });
15773
15774 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15775 editor.update_in(cx, |editor, window, cx| {
15776 let (expected_text, selection_ranges) = marked_text_ranges(
15777 indoc! {"
15778 aaaa
15779 bˇbbb
15780 bˇbbˇb
15781 cccc"
15782 },
15783 true,
15784 );
15785 assert_eq!(editor.text(cx), expected_text);
15786 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15787 s.select_ranges(selection_ranges)
15788 });
15789
15790 editor.handle_input("X", window, cx);
15791
15792 let (expected_text, expected_selections) = marked_text_ranges(
15793 indoc! {"
15794 aaaa
15795 bXˇbbXb
15796 bXˇbbXˇb
15797 cccc"
15798 },
15799 false,
15800 );
15801 assert_eq!(editor.text(cx), expected_text);
15802 assert_eq!(editor.selections.ranges(cx), expected_selections);
15803
15804 editor.newline(&Newline, window, cx);
15805 let (expected_text, expected_selections) = marked_text_ranges(
15806 indoc! {"
15807 aaaa
15808 bX
15809 ˇbbX
15810 b
15811 bX
15812 ˇbbX
15813 ˇb
15814 cccc"
15815 },
15816 false,
15817 );
15818 assert_eq!(editor.text(cx), expected_text);
15819 assert_eq!(editor.selections.ranges(cx), expected_selections);
15820 });
15821}
15822
15823#[gpui::test]
15824fn test_refresh_selections(cx: &mut TestAppContext) {
15825 init_test(cx, |_| {});
15826
15827 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15828 let mut excerpt1_id = None;
15829 let multibuffer = cx.new(|cx| {
15830 let mut multibuffer = MultiBuffer::new(ReadWrite);
15831 excerpt1_id = multibuffer
15832 .push_excerpts(
15833 buffer.clone(),
15834 [
15835 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15836 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15837 ],
15838 cx,
15839 )
15840 .into_iter()
15841 .next();
15842 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15843 multibuffer
15844 });
15845
15846 let editor = cx.add_window(|window, cx| {
15847 let mut editor = build_editor(multibuffer.clone(), window, cx);
15848 let snapshot = editor.snapshot(window, cx);
15849 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15850 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15851 });
15852 editor.begin_selection(
15853 Point::new(2, 1).to_display_point(&snapshot),
15854 true,
15855 1,
15856 window,
15857 cx,
15858 );
15859 assert_eq!(
15860 editor.selections.ranges(cx),
15861 [
15862 Point::new(1, 3)..Point::new(1, 3),
15863 Point::new(2, 1)..Point::new(2, 1),
15864 ]
15865 );
15866 editor
15867 });
15868
15869 // Refreshing selections is a no-op when excerpts haven't changed.
15870 _ = editor.update(cx, |editor, window, cx| {
15871 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15872 assert_eq!(
15873 editor.selections.ranges(cx),
15874 [
15875 Point::new(1, 3)..Point::new(1, 3),
15876 Point::new(2, 1)..Point::new(2, 1),
15877 ]
15878 );
15879 });
15880
15881 multibuffer.update(cx, |multibuffer, cx| {
15882 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15883 });
15884 _ = editor.update(cx, |editor, window, cx| {
15885 // Removing an excerpt causes the first selection to become degenerate.
15886 assert_eq!(
15887 editor.selections.ranges(cx),
15888 [
15889 Point::new(0, 0)..Point::new(0, 0),
15890 Point::new(0, 1)..Point::new(0, 1)
15891 ]
15892 );
15893
15894 // Refreshing selections will relocate the first selection to the original buffer
15895 // location.
15896 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15897 assert_eq!(
15898 editor.selections.ranges(cx),
15899 [
15900 Point::new(0, 1)..Point::new(0, 1),
15901 Point::new(0, 3)..Point::new(0, 3)
15902 ]
15903 );
15904 assert!(editor.selections.pending_anchor().is_some());
15905 });
15906}
15907
15908#[gpui::test]
15909fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15910 init_test(cx, |_| {});
15911
15912 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15913 let mut excerpt1_id = None;
15914 let multibuffer = cx.new(|cx| {
15915 let mut multibuffer = MultiBuffer::new(ReadWrite);
15916 excerpt1_id = multibuffer
15917 .push_excerpts(
15918 buffer.clone(),
15919 [
15920 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15921 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15922 ],
15923 cx,
15924 )
15925 .into_iter()
15926 .next();
15927 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15928 multibuffer
15929 });
15930
15931 let editor = cx.add_window(|window, cx| {
15932 let mut editor = build_editor(multibuffer.clone(), window, cx);
15933 let snapshot = editor.snapshot(window, cx);
15934 editor.begin_selection(
15935 Point::new(1, 3).to_display_point(&snapshot),
15936 false,
15937 1,
15938 window,
15939 cx,
15940 );
15941 assert_eq!(
15942 editor.selections.ranges(cx),
15943 [Point::new(1, 3)..Point::new(1, 3)]
15944 );
15945 editor
15946 });
15947
15948 multibuffer.update(cx, |multibuffer, cx| {
15949 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15950 });
15951 _ = editor.update(cx, |editor, window, cx| {
15952 assert_eq!(
15953 editor.selections.ranges(cx),
15954 [Point::new(0, 0)..Point::new(0, 0)]
15955 );
15956
15957 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
15958 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15959 assert_eq!(
15960 editor.selections.ranges(cx),
15961 [Point::new(0, 3)..Point::new(0, 3)]
15962 );
15963 assert!(editor.selections.pending_anchor().is_some());
15964 });
15965}
15966
15967#[gpui::test]
15968async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
15969 init_test(cx, |_| {});
15970
15971 let language = Arc::new(
15972 Language::new(
15973 LanguageConfig {
15974 brackets: BracketPairConfig {
15975 pairs: vec![
15976 BracketPair {
15977 start: "{".to_string(),
15978 end: "}".to_string(),
15979 close: true,
15980 surround: true,
15981 newline: true,
15982 },
15983 BracketPair {
15984 start: "/* ".to_string(),
15985 end: " */".to_string(),
15986 close: true,
15987 surround: true,
15988 newline: true,
15989 },
15990 ],
15991 ..Default::default()
15992 },
15993 ..Default::default()
15994 },
15995 Some(tree_sitter_rust::LANGUAGE.into()),
15996 )
15997 .with_indents_query("")
15998 .unwrap(),
15999 );
16000
16001 let text = concat!(
16002 "{ }\n", //
16003 " x\n", //
16004 " /* */\n", //
16005 "x\n", //
16006 "{{} }\n", //
16007 );
16008
16009 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16010 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16011 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16012 editor
16013 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16014 .await;
16015
16016 editor.update_in(cx, |editor, window, cx| {
16017 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16018 s.select_display_ranges([
16019 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16020 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16021 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16022 ])
16023 });
16024 editor.newline(&Newline, window, cx);
16025
16026 assert_eq!(
16027 editor.buffer().read(cx).read(cx).text(),
16028 concat!(
16029 "{ \n", // Suppress rustfmt
16030 "\n", //
16031 "}\n", //
16032 " x\n", //
16033 " /* \n", //
16034 " \n", //
16035 " */\n", //
16036 "x\n", //
16037 "{{} \n", //
16038 "}\n", //
16039 )
16040 );
16041 });
16042}
16043
16044#[gpui::test]
16045fn test_highlighted_ranges(cx: &mut TestAppContext) {
16046 init_test(cx, |_| {});
16047
16048 let editor = cx.add_window(|window, cx| {
16049 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16050 build_editor(buffer, window, cx)
16051 });
16052
16053 _ = editor.update(cx, |editor, window, cx| {
16054 struct Type1;
16055 struct Type2;
16056
16057 let buffer = editor.buffer.read(cx).snapshot(cx);
16058
16059 let anchor_range =
16060 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16061
16062 editor.highlight_background::<Type1>(
16063 &[
16064 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16065 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16066 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16067 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16068 ],
16069 |_| Hsla::red(),
16070 cx,
16071 );
16072 editor.highlight_background::<Type2>(
16073 &[
16074 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16075 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16076 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16077 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16078 ],
16079 |_| Hsla::green(),
16080 cx,
16081 );
16082
16083 let snapshot = editor.snapshot(window, cx);
16084 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16085 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16086 &snapshot,
16087 cx.theme(),
16088 );
16089 assert_eq!(
16090 highlighted_ranges,
16091 &[
16092 (
16093 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16094 Hsla::green(),
16095 ),
16096 (
16097 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16098 Hsla::red(),
16099 ),
16100 (
16101 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16102 Hsla::green(),
16103 ),
16104 (
16105 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16106 Hsla::red(),
16107 ),
16108 ]
16109 );
16110 assert_eq!(
16111 editor.sorted_background_highlights_in_range(
16112 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16113 &snapshot,
16114 cx.theme(),
16115 ),
16116 &[(
16117 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16118 Hsla::red(),
16119 )]
16120 );
16121 });
16122}
16123
16124#[gpui::test]
16125async fn test_following(cx: &mut TestAppContext) {
16126 init_test(cx, |_| {});
16127
16128 let fs = FakeFs::new(cx.executor());
16129 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16130
16131 let buffer = project.update(cx, |project, cx| {
16132 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16133 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16134 });
16135 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16136 let follower = cx.update(|cx| {
16137 cx.open_window(
16138 WindowOptions {
16139 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16140 gpui::Point::new(px(0.), px(0.)),
16141 gpui::Point::new(px(10.), px(80.)),
16142 ))),
16143 ..Default::default()
16144 },
16145 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16146 )
16147 .unwrap()
16148 });
16149
16150 let is_still_following = Rc::new(RefCell::new(true));
16151 let follower_edit_event_count = Rc::new(RefCell::new(0));
16152 let pending_update = Rc::new(RefCell::new(None));
16153 let leader_entity = leader.root(cx).unwrap();
16154 let follower_entity = follower.root(cx).unwrap();
16155 _ = follower.update(cx, {
16156 let update = pending_update.clone();
16157 let is_still_following = is_still_following.clone();
16158 let follower_edit_event_count = follower_edit_event_count.clone();
16159 |_, window, cx| {
16160 cx.subscribe_in(
16161 &leader_entity,
16162 window,
16163 move |_, leader, event, window, cx| {
16164 leader.read(cx).add_event_to_update_proto(
16165 event,
16166 &mut update.borrow_mut(),
16167 window,
16168 cx,
16169 );
16170 },
16171 )
16172 .detach();
16173
16174 cx.subscribe_in(
16175 &follower_entity,
16176 window,
16177 move |_, _, event: &EditorEvent, _window, _cx| {
16178 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16179 *is_still_following.borrow_mut() = false;
16180 }
16181
16182 if let EditorEvent::BufferEdited = event {
16183 *follower_edit_event_count.borrow_mut() += 1;
16184 }
16185 },
16186 )
16187 .detach();
16188 }
16189 });
16190
16191 // Update the selections only
16192 _ = leader.update(cx, |leader, window, cx| {
16193 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16194 s.select_ranges([1..1])
16195 });
16196 });
16197 follower
16198 .update(cx, |follower, window, cx| {
16199 follower.apply_update_proto(
16200 &project,
16201 pending_update.borrow_mut().take().unwrap(),
16202 window,
16203 cx,
16204 )
16205 })
16206 .unwrap()
16207 .await
16208 .unwrap();
16209 _ = follower.update(cx, |follower, _, cx| {
16210 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16211 });
16212 assert!(*is_still_following.borrow());
16213 assert_eq!(*follower_edit_event_count.borrow(), 0);
16214
16215 // Update the scroll position only
16216 _ = leader.update(cx, |leader, window, cx| {
16217 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16218 });
16219 follower
16220 .update(cx, |follower, window, cx| {
16221 follower.apply_update_proto(
16222 &project,
16223 pending_update.borrow_mut().take().unwrap(),
16224 window,
16225 cx,
16226 )
16227 })
16228 .unwrap()
16229 .await
16230 .unwrap();
16231 assert_eq!(
16232 follower
16233 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16234 .unwrap(),
16235 gpui::Point::new(1.5, 3.5)
16236 );
16237 assert!(*is_still_following.borrow());
16238 assert_eq!(*follower_edit_event_count.borrow(), 0);
16239
16240 // Update the selections and scroll position. The follower's scroll position is updated
16241 // via autoscroll, not via the leader's exact scroll position.
16242 _ = leader.update(cx, |leader, window, cx| {
16243 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16244 s.select_ranges([0..0])
16245 });
16246 leader.request_autoscroll(Autoscroll::newest(), cx);
16247 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16248 });
16249 follower
16250 .update(cx, |follower, window, cx| {
16251 follower.apply_update_proto(
16252 &project,
16253 pending_update.borrow_mut().take().unwrap(),
16254 window,
16255 cx,
16256 )
16257 })
16258 .unwrap()
16259 .await
16260 .unwrap();
16261 _ = follower.update(cx, |follower, _, cx| {
16262 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16263 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16264 });
16265 assert!(*is_still_following.borrow());
16266
16267 // Creating a pending selection that precedes another selection
16268 _ = leader.update(cx, |leader, window, cx| {
16269 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16270 s.select_ranges([1..1])
16271 });
16272 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16273 });
16274 follower
16275 .update(cx, |follower, window, cx| {
16276 follower.apply_update_proto(
16277 &project,
16278 pending_update.borrow_mut().take().unwrap(),
16279 window,
16280 cx,
16281 )
16282 })
16283 .unwrap()
16284 .await
16285 .unwrap();
16286 _ = follower.update(cx, |follower, _, cx| {
16287 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16288 });
16289 assert!(*is_still_following.borrow());
16290
16291 // Extend the pending selection so that it surrounds another selection
16292 _ = leader.update(cx, |leader, window, cx| {
16293 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16294 });
16295 follower
16296 .update(cx, |follower, window, cx| {
16297 follower.apply_update_proto(
16298 &project,
16299 pending_update.borrow_mut().take().unwrap(),
16300 window,
16301 cx,
16302 )
16303 })
16304 .unwrap()
16305 .await
16306 .unwrap();
16307 _ = follower.update(cx, |follower, _, cx| {
16308 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16309 });
16310
16311 // Scrolling locally breaks the follow
16312 _ = follower.update(cx, |follower, window, cx| {
16313 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16314 follower.set_scroll_anchor(
16315 ScrollAnchor {
16316 anchor: top_anchor,
16317 offset: gpui::Point::new(0.0, 0.5),
16318 },
16319 window,
16320 cx,
16321 );
16322 });
16323 assert!(!(*is_still_following.borrow()));
16324}
16325
16326#[gpui::test]
16327async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16328 init_test(cx, |_| {});
16329
16330 let fs = FakeFs::new(cx.executor());
16331 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16332 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16333 let pane = workspace
16334 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16335 .unwrap();
16336
16337 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16338
16339 let leader = pane.update_in(cx, |_, window, cx| {
16340 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16341 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16342 });
16343
16344 // Start following the editor when it has no excerpts.
16345 let mut state_message =
16346 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16347 let workspace_entity = workspace.root(cx).unwrap();
16348 let follower_1 = cx
16349 .update_window(*workspace.deref(), |_, window, cx| {
16350 Editor::from_state_proto(
16351 workspace_entity,
16352 ViewId {
16353 creator: CollaboratorId::PeerId(PeerId::default()),
16354 id: 0,
16355 },
16356 &mut state_message,
16357 window,
16358 cx,
16359 )
16360 })
16361 .unwrap()
16362 .unwrap()
16363 .await
16364 .unwrap();
16365
16366 let update_message = Rc::new(RefCell::new(None));
16367 follower_1.update_in(cx, {
16368 let update = update_message.clone();
16369 |_, window, cx| {
16370 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16371 leader.read(cx).add_event_to_update_proto(
16372 event,
16373 &mut update.borrow_mut(),
16374 window,
16375 cx,
16376 );
16377 })
16378 .detach();
16379 }
16380 });
16381
16382 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16383 (
16384 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16385 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16386 )
16387 });
16388
16389 // Insert some excerpts.
16390 leader.update(cx, |leader, cx| {
16391 leader.buffer.update(cx, |multibuffer, cx| {
16392 multibuffer.set_excerpts_for_path(
16393 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
16394 buffer_1.clone(),
16395 vec![
16396 Point::row_range(0..3),
16397 Point::row_range(1..6),
16398 Point::row_range(12..15),
16399 ],
16400 0,
16401 cx,
16402 );
16403 multibuffer.set_excerpts_for_path(
16404 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
16405 buffer_2.clone(),
16406 vec![Point::row_range(0..6), Point::row_range(8..12)],
16407 0,
16408 cx,
16409 );
16410 });
16411 });
16412
16413 // Apply the update of adding the excerpts.
16414 follower_1
16415 .update_in(cx, |follower, window, cx| {
16416 follower.apply_update_proto(
16417 &project,
16418 update_message.borrow().clone().unwrap(),
16419 window,
16420 cx,
16421 )
16422 })
16423 .await
16424 .unwrap();
16425 assert_eq!(
16426 follower_1.update(cx, |editor, cx| editor.text(cx)),
16427 leader.update(cx, |editor, cx| editor.text(cx))
16428 );
16429 update_message.borrow_mut().take();
16430
16431 // Start following separately after it already has excerpts.
16432 let mut state_message =
16433 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16434 let workspace_entity = workspace.root(cx).unwrap();
16435 let follower_2 = cx
16436 .update_window(*workspace.deref(), |_, window, cx| {
16437 Editor::from_state_proto(
16438 workspace_entity,
16439 ViewId {
16440 creator: CollaboratorId::PeerId(PeerId::default()),
16441 id: 0,
16442 },
16443 &mut state_message,
16444 window,
16445 cx,
16446 )
16447 })
16448 .unwrap()
16449 .unwrap()
16450 .await
16451 .unwrap();
16452 assert_eq!(
16453 follower_2.update(cx, |editor, cx| editor.text(cx)),
16454 leader.update(cx, |editor, cx| editor.text(cx))
16455 );
16456
16457 // Remove some excerpts.
16458 leader.update(cx, |leader, cx| {
16459 leader.buffer.update(cx, |multibuffer, cx| {
16460 let excerpt_ids = multibuffer.excerpt_ids();
16461 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16462 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16463 });
16464 });
16465
16466 // Apply the update of removing the excerpts.
16467 follower_1
16468 .update_in(cx, |follower, window, cx| {
16469 follower.apply_update_proto(
16470 &project,
16471 update_message.borrow().clone().unwrap(),
16472 window,
16473 cx,
16474 )
16475 })
16476 .await
16477 .unwrap();
16478 follower_2
16479 .update_in(cx, |follower, window, cx| {
16480 follower.apply_update_proto(
16481 &project,
16482 update_message.borrow().clone().unwrap(),
16483 window,
16484 cx,
16485 )
16486 })
16487 .await
16488 .unwrap();
16489 update_message.borrow_mut().take();
16490 assert_eq!(
16491 follower_1.update(cx, |editor, cx| editor.text(cx)),
16492 leader.update(cx, |editor, cx| editor.text(cx))
16493 );
16494}
16495
16496#[gpui::test]
16497async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16498 init_test(cx, |_| {});
16499
16500 let mut cx = EditorTestContext::new(cx).await;
16501 let lsp_store =
16502 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16503
16504 cx.set_state(indoc! {"
16505 ˇfn func(abc def: i32) -> u32 {
16506 }
16507 "});
16508
16509 cx.update(|_, cx| {
16510 lsp_store.update(cx, |lsp_store, cx| {
16511 lsp_store
16512 .update_diagnostics(
16513 LanguageServerId(0),
16514 lsp::PublishDiagnosticsParams {
16515 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16516 version: None,
16517 diagnostics: vec![
16518 lsp::Diagnostic {
16519 range: lsp::Range::new(
16520 lsp::Position::new(0, 11),
16521 lsp::Position::new(0, 12),
16522 ),
16523 severity: Some(lsp::DiagnosticSeverity::ERROR),
16524 ..Default::default()
16525 },
16526 lsp::Diagnostic {
16527 range: lsp::Range::new(
16528 lsp::Position::new(0, 12),
16529 lsp::Position::new(0, 15),
16530 ),
16531 severity: Some(lsp::DiagnosticSeverity::ERROR),
16532 ..Default::default()
16533 },
16534 lsp::Diagnostic {
16535 range: lsp::Range::new(
16536 lsp::Position::new(0, 25),
16537 lsp::Position::new(0, 28),
16538 ),
16539 severity: Some(lsp::DiagnosticSeverity::ERROR),
16540 ..Default::default()
16541 },
16542 ],
16543 },
16544 None,
16545 DiagnosticSourceKind::Pushed,
16546 &[],
16547 cx,
16548 )
16549 .unwrap()
16550 });
16551 });
16552
16553 executor.run_until_parked();
16554
16555 cx.update_editor(|editor, window, cx| {
16556 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16557 });
16558
16559 cx.assert_editor_state(indoc! {"
16560 fn func(abc def: i32) -> ˇu32 {
16561 }
16562 "});
16563
16564 cx.update_editor(|editor, window, cx| {
16565 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16566 });
16567
16568 cx.assert_editor_state(indoc! {"
16569 fn func(abc ˇdef: i32) -> u32 {
16570 }
16571 "});
16572
16573 cx.update_editor(|editor, window, cx| {
16574 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16575 });
16576
16577 cx.assert_editor_state(indoc! {"
16578 fn func(abcˇ def: i32) -> u32 {
16579 }
16580 "});
16581
16582 cx.update_editor(|editor, window, cx| {
16583 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16584 });
16585
16586 cx.assert_editor_state(indoc! {"
16587 fn func(abc def: i32) -> ˇu32 {
16588 }
16589 "});
16590}
16591
16592#[gpui::test]
16593async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16594 init_test(cx, |_| {});
16595
16596 let mut cx = EditorTestContext::new(cx).await;
16597
16598 let diff_base = r#"
16599 use some::mod;
16600
16601 const A: u32 = 42;
16602
16603 fn main() {
16604 println!("hello");
16605
16606 println!("world");
16607 }
16608 "#
16609 .unindent();
16610
16611 // Edits are modified, removed, modified, added
16612 cx.set_state(
16613 &r#"
16614 use some::modified;
16615
16616 ˇ
16617 fn main() {
16618 println!("hello there");
16619
16620 println!("around the");
16621 println!("world");
16622 }
16623 "#
16624 .unindent(),
16625 );
16626
16627 cx.set_head_text(&diff_base);
16628 executor.run_until_parked();
16629
16630 cx.update_editor(|editor, window, cx| {
16631 //Wrap around the bottom of the buffer
16632 for _ in 0..3 {
16633 editor.go_to_next_hunk(&GoToHunk, window, cx);
16634 }
16635 });
16636
16637 cx.assert_editor_state(
16638 &r#"
16639 ˇuse some::modified;
16640
16641
16642 fn main() {
16643 println!("hello there");
16644
16645 println!("around the");
16646 println!("world");
16647 }
16648 "#
16649 .unindent(),
16650 );
16651
16652 cx.update_editor(|editor, window, cx| {
16653 //Wrap around the top of the buffer
16654 for _ in 0..2 {
16655 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16656 }
16657 });
16658
16659 cx.assert_editor_state(
16660 &r#"
16661 use some::modified;
16662
16663
16664 fn main() {
16665 ˇ println!("hello there");
16666
16667 println!("around the");
16668 println!("world");
16669 }
16670 "#
16671 .unindent(),
16672 );
16673
16674 cx.update_editor(|editor, window, cx| {
16675 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16676 });
16677
16678 cx.assert_editor_state(
16679 &r#"
16680 use some::modified;
16681
16682 ˇ
16683 fn main() {
16684 println!("hello there");
16685
16686 println!("around the");
16687 println!("world");
16688 }
16689 "#
16690 .unindent(),
16691 );
16692
16693 cx.update_editor(|editor, window, cx| {
16694 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16695 });
16696
16697 cx.assert_editor_state(
16698 &r#"
16699 ˇuse some::modified;
16700
16701
16702 fn main() {
16703 println!("hello there");
16704
16705 println!("around the");
16706 println!("world");
16707 }
16708 "#
16709 .unindent(),
16710 );
16711
16712 cx.update_editor(|editor, window, cx| {
16713 for _ in 0..2 {
16714 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16715 }
16716 });
16717
16718 cx.assert_editor_state(
16719 &r#"
16720 use some::modified;
16721
16722
16723 fn main() {
16724 ˇ println!("hello there");
16725
16726 println!("around the");
16727 println!("world");
16728 }
16729 "#
16730 .unindent(),
16731 );
16732
16733 cx.update_editor(|editor, window, cx| {
16734 editor.fold(&Fold, window, cx);
16735 });
16736
16737 cx.update_editor(|editor, window, cx| {
16738 editor.go_to_next_hunk(&GoToHunk, window, cx);
16739 });
16740
16741 cx.assert_editor_state(
16742 &r#"
16743 ˇuse some::modified;
16744
16745
16746 fn main() {
16747 println!("hello there");
16748
16749 println!("around the");
16750 println!("world");
16751 }
16752 "#
16753 .unindent(),
16754 );
16755}
16756
16757#[test]
16758fn test_split_words() {
16759 fn split(text: &str) -> Vec<&str> {
16760 split_words(text).collect()
16761 }
16762
16763 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16764 assert_eq!(split("hello_world"), &["hello_", "world"]);
16765 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16766 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16767 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16768 assert_eq!(split("helloworld"), &["helloworld"]);
16769
16770 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16771}
16772
16773#[gpui::test]
16774async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16775 init_test(cx, |_| {});
16776
16777 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16778 let mut assert = |before, after| {
16779 let _state_context = cx.set_state(before);
16780 cx.run_until_parked();
16781 cx.update_editor(|editor, window, cx| {
16782 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16783 });
16784 cx.run_until_parked();
16785 cx.assert_editor_state(after);
16786 };
16787
16788 // Outside bracket jumps to outside of matching bracket
16789 assert("console.logˇ(var);", "console.log(var)ˇ;");
16790 assert("console.log(var)ˇ;", "console.logˇ(var);");
16791
16792 // Inside bracket jumps to inside of matching bracket
16793 assert("console.log(ˇvar);", "console.log(varˇ);");
16794 assert("console.log(varˇ);", "console.log(ˇvar);");
16795
16796 // When outside a bracket and inside, favor jumping to the inside bracket
16797 assert(
16798 "console.log('foo', [1, 2, 3]ˇ);",
16799 "console.log(ˇ'foo', [1, 2, 3]);",
16800 );
16801 assert(
16802 "console.log(ˇ'foo', [1, 2, 3]);",
16803 "console.log('foo', [1, 2, 3]ˇ);",
16804 );
16805
16806 // Bias forward if two options are equally likely
16807 assert(
16808 "let result = curried_fun()ˇ();",
16809 "let result = curried_fun()()ˇ;",
16810 );
16811
16812 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16813 assert(
16814 indoc! {"
16815 function test() {
16816 console.log('test')ˇ
16817 }"},
16818 indoc! {"
16819 function test() {
16820 console.logˇ('test')
16821 }"},
16822 );
16823}
16824
16825#[gpui::test]
16826async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16827 init_test(cx, |_| {});
16828
16829 let fs = FakeFs::new(cx.executor());
16830 fs.insert_tree(
16831 path!("/a"),
16832 json!({
16833 "main.rs": "fn main() { let a = 5; }",
16834 "other.rs": "// Test file",
16835 }),
16836 )
16837 .await;
16838 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16839
16840 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16841 language_registry.add(Arc::new(Language::new(
16842 LanguageConfig {
16843 name: "Rust".into(),
16844 matcher: LanguageMatcher {
16845 path_suffixes: vec!["rs".to_string()],
16846 ..Default::default()
16847 },
16848 brackets: BracketPairConfig {
16849 pairs: vec![BracketPair {
16850 start: "{".to_string(),
16851 end: "}".to_string(),
16852 close: true,
16853 surround: true,
16854 newline: true,
16855 }],
16856 disabled_scopes_by_bracket_ix: Vec::new(),
16857 },
16858 ..Default::default()
16859 },
16860 Some(tree_sitter_rust::LANGUAGE.into()),
16861 )));
16862 let mut fake_servers = language_registry.register_fake_lsp(
16863 "Rust",
16864 FakeLspAdapter {
16865 capabilities: lsp::ServerCapabilities {
16866 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16867 first_trigger_character: "{".to_string(),
16868 more_trigger_character: None,
16869 }),
16870 ..Default::default()
16871 },
16872 ..Default::default()
16873 },
16874 );
16875
16876 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16877
16878 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16879
16880 let worktree_id = workspace
16881 .update(cx, |workspace, _, cx| {
16882 workspace.project().update(cx, |project, cx| {
16883 project.worktrees(cx).next().unwrap().read(cx).id()
16884 })
16885 })
16886 .unwrap();
16887
16888 let buffer = project
16889 .update(cx, |project, cx| {
16890 project.open_local_buffer(path!("/a/main.rs"), cx)
16891 })
16892 .await
16893 .unwrap();
16894 let editor_handle = workspace
16895 .update(cx, |workspace, window, cx| {
16896 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16897 })
16898 .unwrap()
16899 .await
16900 .unwrap()
16901 .downcast::<Editor>()
16902 .unwrap();
16903
16904 cx.executor().start_waiting();
16905 let fake_server = fake_servers.next().await.unwrap();
16906
16907 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16908 |params, _| async move {
16909 assert_eq!(
16910 params.text_document_position.text_document.uri,
16911 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16912 );
16913 assert_eq!(
16914 params.text_document_position.position,
16915 lsp::Position::new(0, 21),
16916 );
16917
16918 Ok(Some(vec![lsp::TextEdit {
16919 new_text: "]".to_string(),
16920 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16921 }]))
16922 },
16923 );
16924
16925 editor_handle.update_in(cx, |editor, window, cx| {
16926 window.focus(&editor.focus_handle(cx));
16927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16928 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16929 });
16930 editor.handle_input("{", window, cx);
16931 });
16932
16933 cx.executor().run_until_parked();
16934
16935 buffer.update(cx, |buffer, _| {
16936 assert_eq!(
16937 buffer.text(),
16938 "fn main() { let a = {5}; }",
16939 "No extra braces from on type formatting should appear in the buffer"
16940 )
16941 });
16942}
16943
16944#[gpui::test(iterations = 20, seeds(31))]
16945async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
16946 init_test(cx, |_| {});
16947
16948 let mut cx = EditorLspTestContext::new_rust(
16949 lsp::ServerCapabilities {
16950 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16951 first_trigger_character: ".".to_string(),
16952 more_trigger_character: None,
16953 }),
16954 ..Default::default()
16955 },
16956 cx,
16957 )
16958 .await;
16959
16960 cx.update_buffer(|buffer, _| {
16961 // This causes autoindent to be async.
16962 buffer.set_sync_parse_timeout(Duration::ZERO)
16963 });
16964
16965 cx.set_state("fn c() {\n d()ˇ\n}\n");
16966 cx.simulate_keystroke("\n");
16967 cx.run_until_parked();
16968
16969 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
16970 let mut request =
16971 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
16972 let buffer_cloned = buffer_cloned.clone();
16973 async move {
16974 buffer_cloned.update(&mut cx, |buffer, _| {
16975 assert_eq!(
16976 buffer.text(),
16977 "fn c() {\n d()\n .\n}\n",
16978 "OnTypeFormatting should triggered after autoindent applied"
16979 )
16980 })?;
16981
16982 Ok(Some(vec![]))
16983 }
16984 });
16985
16986 cx.simulate_keystroke(".");
16987 cx.run_until_parked();
16988
16989 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
16990 assert!(request.next().await.is_some());
16991 request.close();
16992 assert!(request.next().await.is_none());
16993}
16994
16995#[gpui::test]
16996async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
16997 init_test(cx, |_| {});
16998
16999 let fs = FakeFs::new(cx.executor());
17000 fs.insert_tree(
17001 path!("/a"),
17002 json!({
17003 "main.rs": "fn main() { let a = 5; }",
17004 "other.rs": "// Test file",
17005 }),
17006 )
17007 .await;
17008
17009 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17010
17011 let server_restarts = Arc::new(AtomicUsize::new(0));
17012 let closure_restarts = Arc::clone(&server_restarts);
17013 let language_server_name = "test language server";
17014 let language_name: LanguageName = "Rust".into();
17015
17016 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17017 language_registry.add(Arc::new(Language::new(
17018 LanguageConfig {
17019 name: language_name.clone(),
17020 matcher: LanguageMatcher {
17021 path_suffixes: vec!["rs".to_string()],
17022 ..Default::default()
17023 },
17024 ..Default::default()
17025 },
17026 Some(tree_sitter_rust::LANGUAGE.into()),
17027 )));
17028 let mut fake_servers = language_registry.register_fake_lsp(
17029 "Rust",
17030 FakeLspAdapter {
17031 name: language_server_name,
17032 initialization_options: Some(json!({
17033 "testOptionValue": true
17034 })),
17035 initializer: Some(Box::new(move |fake_server| {
17036 let task_restarts = Arc::clone(&closure_restarts);
17037 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17038 task_restarts.fetch_add(1, atomic::Ordering::Release);
17039 futures::future::ready(Ok(()))
17040 });
17041 })),
17042 ..Default::default()
17043 },
17044 );
17045
17046 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17047 let _buffer = project
17048 .update(cx, |project, cx| {
17049 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17050 })
17051 .await
17052 .unwrap();
17053 let _fake_server = fake_servers.next().await.unwrap();
17054 update_test_language_settings(cx, |language_settings| {
17055 language_settings.languages.0.insert(
17056 language_name.clone().0,
17057 LanguageSettingsContent {
17058 tab_size: NonZeroU32::new(8),
17059 ..Default::default()
17060 },
17061 );
17062 });
17063 cx.executor().run_until_parked();
17064 assert_eq!(
17065 server_restarts.load(atomic::Ordering::Acquire),
17066 0,
17067 "Should not restart LSP server on an unrelated change"
17068 );
17069
17070 update_test_project_settings(cx, |project_settings| {
17071 project_settings.lsp.insert(
17072 "Some other server name".into(),
17073 LspSettings {
17074 binary: None,
17075 settings: None,
17076 initialization_options: Some(json!({
17077 "some other init value": false
17078 })),
17079 enable_lsp_tasks: false,
17080 fetch: None,
17081 },
17082 );
17083 });
17084 cx.executor().run_until_parked();
17085 assert_eq!(
17086 server_restarts.load(atomic::Ordering::Acquire),
17087 0,
17088 "Should not restart LSP server on an unrelated LSP settings change"
17089 );
17090
17091 update_test_project_settings(cx, |project_settings| {
17092 project_settings.lsp.insert(
17093 language_server_name.into(),
17094 LspSettings {
17095 binary: None,
17096 settings: None,
17097 initialization_options: Some(json!({
17098 "anotherInitValue": false
17099 })),
17100 enable_lsp_tasks: false,
17101 fetch: None,
17102 },
17103 );
17104 });
17105 cx.executor().run_until_parked();
17106 assert_eq!(
17107 server_restarts.load(atomic::Ordering::Acquire),
17108 1,
17109 "Should restart LSP server on a related LSP settings change"
17110 );
17111
17112 update_test_project_settings(cx, |project_settings| {
17113 project_settings.lsp.insert(
17114 language_server_name.into(),
17115 LspSettings {
17116 binary: None,
17117 settings: None,
17118 initialization_options: Some(json!({
17119 "anotherInitValue": false
17120 })),
17121 enable_lsp_tasks: false,
17122 fetch: None,
17123 },
17124 );
17125 });
17126 cx.executor().run_until_parked();
17127 assert_eq!(
17128 server_restarts.load(atomic::Ordering::Acquire),
17129 1,
17130 "Should not restart LSP server on a related LSP settings change that is the same"
17131 );
17132
17133 update_test_project_settings(cx, |project_settings| {
17134 project_settings.lsp.insert(
17135 language_server_name.into(),
17136 LspSettings {
17137 binary: None,
17138 settings: None,
17139 initialization_options: None,
17140 enable_lsp_tasks: false,
17141 fetch: None,
17142 },
17143 );
17144 });
17145 cx.executor().run_until_parked();
17146 assert_eq!(
17147 server_restarts.load(atomic::Ordering::Acquire),
17148 2,
17149 "Should restart LSP server on another related LSP settings change"
17150 );
17151}
17152
17153#[gpui::test]
17154async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17155 init_test(cx, |_| {});
17156
17157 let mut cx = EditorLspTestContext::new_rust(
17158 lsp::ServerCapabilities {
17159 completion_provider: Some(lsp::CompletionOptions {
17160 trigger_characters: Some(vec![".".to_string()]),
17161 resolve_provider: Some(true),
17162 ..Default::default()
17163 }),
17164 ..Default::default()
17165 },
17166 cx,
17167 )
17168 .await;
17169
17170 cx.set_state("fn main() { let a = 2ˇ; }");
17171 cx.simulate_keystroke(".");
17172 let completion_item = lsp::CompletionItem {
17173 label: "some".into(),
17174 kind: Some(lsp::CompletionItemKind::SNIPPET),
17175 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17176 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17177 kind: lsp::MarkupKind::Markdown,
17178 value: "```rust\nSome(2)\n```".to_string(),
17179 })),
17180 deprecated: Some(false),
17181 sort_text: Some("fffffff2".to_string()),
17182 filter_text: Some("some".to_string()),
17183 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17184 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17185 range: lsp::Range {
17186 start: lsp::Position {
17187 line: 0,
17188 character: 22,
17189 },
17190 end: lsp::Position {
17191 line: 0,
17192 character: 22,
17193 },
17194 },
17195 new_text: "Some(2)".to_string(),
17196 })),
17197 additional_text_edits: Some(vec![lsp::TextEdit {
17198 range: lsp::Range {
17199 start: lsp::Position {
17200 line: 0,
17201 character: 20,
17202 },
17203 end: lsp::Position {
17204 line: 0,
17205 character: 22,
17206 },
17207 },
17208 new_text: "".to_string(),
17209 }]),
17210 ..Default::default()
17211 };
17212
17213 let closure_completion_item = completion_item.clone();
17214 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17215 let task_completion_item = closure_completion_item.clone();
17216 async move {
17217 Ok(Some(lsp::CompletionResponse::Array(vec![
17218 task_completion_item,
17219 ])))
17220 }
17221 });
17222
17223 request.next().await;
17224
17225 cx.condition(|editor, _| editor.context_menu_visible())
17226 .await;
17227 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17228 editor
17229 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17230 .unwrap()
17231 });
17232 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17233
17234 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17235 let task_completion_item = completion_item.clone();
17236 async move { Ok(task_completion_item) }
17237 })
17238 .next()
17239 .await
17240 .unwrap();
17241 apply_additional_edits.await.unwrap();
17242 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17243}
17244
17245#[gpui::test]
17246async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17247 init_test(cx, |_| {});
17248
17249 let mut cx = EditorLspTestContext::new_rust(
17250 lsp::ServerCapabilities {
17251 completion_provider: Some(lsp::CompletionOptions {
17252 trigger_characters: Some(vec![".".to_string()]),
17253 resolve_provider: Some(true),
17254 ..Default::default()
17255 }),
17256 ..Default::default()
17257 },
17258 cx,
17259 )
17260 .await;
17261
17262 cx.set_state("fn main() { let a = 2ˇ; }");
17263 cx.simulate_keystroke(".");
17264
17265 let item1 = lsp::CompletionItem {
17266 label: "method id()".to_string(),
17267 filter_text: Some("id".to_string()),
17268 detail: None,
17269 documentation: None,
17270 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17271 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17272 new_text: ".id".to_string(),
17273 })),
17274 ..lsp::CompletionItem::default()
17275 };
17276
17277 let item2 = lsp::CompletionItem {
17278 label: "other".to_string(),
17279 filter_text: Some("other".to_string()),
17280 detail: None,
17281 documentation: None,
17282 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17283 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17284 new_text: ".other".to_string(),
17285 })),
17286 ..lsp::CompletionItem::default()
17287 };
17288
17289 let item1 = item1.clone();
17290 cx.set_request_handler::<lsp::request::Completion, _, _>({
17291 let item1 = item1.clone();
17292 move |_, _, _| {
17293 let item1 = item1.clone();
17294 let item2 = item2.clone();
17295 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17296 }
17297 })
17298 .next()
17299 .await;
17300
17301 cx.condition(|editor, _| editor.context_menu_visible())
17302 .await;
17303 cx.update_editor(|editor, _, _| {
17304 let context_menu = editor.context_menu.borrow_mut();
17305 let context_menu = context_menu
17306 .as_ref()
17307 .expect("Should have the context menu deployed");
17308 match context_menu {
17309 CodeContextMenu::Completions(completions_menu) => {
17310 let completions = completions_menu.completions.borrow_mut();
17311 assert_eq!(
17312 completions
17313 .iter()
17314 .map(|completion| &completion.label.text)
17315 .collect::<Vec<_>>(),
17316 vec!["method id()", "other"]
17317 )
17318 }
17319 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17320 }
17321 });
17322
17323 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17324 let item1 = item1.clone();
17325 move |_, item_to_resolve, _| {
17326 let item1 = item1.clone();
17327 async move {
17328 if item1 == item_to_resolve {
17329 Ok(lsp::CompletionItem {
17330 label: "method id()".to_string(),
17331 filter_text: Some("id".to_string()),
17332 detail: Some("Now resolved!".to_string()),
17333 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17334 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17335 range: lsp::Range::new(
17336 lsp::Position::new(0, 22),
17337 lsp::Position::new(0, 22),
17338 ),
17339 new_text: ".id".to_string(),
17340 })),
17341 ..lsp::CompletionItem::default()
17342 })
17343 } else {
17344 Ok(item_to_resolve)
17345 }
17346 }
17347 }
17348 })
17349 .next()
17350 .await
17351 .unwrap();
17352 cx.run_until_parked();
17353
17354 cx.update_editor(|editor, window, cx| {
17355 editor.context_menu_next(&Default::default(), window, cx);
17356 });
17357
17358 cx.update_editor(|editor, _, _| {
17359 let context_menu = editor.context_menu.borrow_mut();
17360 let context_menu = context_menu
17361 .as_ref()
17362 .expect("Should have the context menu deployed");
17363 match context_menu {
17364 CodeContextMenu::Completions(completions_menu) => {
17365 let completions = completions_menu.completions.borrow_mut();
17366 assert_eq!(
17367 completions
17368 .iter()
17369 .map(|completion| &completion.label.text)
17370 .collect::<Vec<_>>(),
17371 vec!["method id() Now resolved!", "other"],
17372 "Should update first completion label, but not second as the filter text did not match."
17373 );
17374 }
17375 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17376 }
17377 });
17378}
17379
17380#[gpui::test]
17381async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17382 init_test(cx, |_| {});
17383 let mut cx = EditorLspTestContext::new_rust(
17384 lsp::ServerCapabilities {
17385 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17386 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17387 completion_provider: Some(lsp::CompletionOptions {
17388 resolve_provider: Some(true),
17389 ..Default::default()
17390 }),
17391 ..Default::default()
17392 },
17393 cx,
17394 )
17395 .await;
17396 cx.set_state(indoc! {"
17397 struct TestStruct {
17398 field: i32
17399 }
17400
17401 fn mainˇ() {
17402 let unused_var = 42;
17403 let test_struct = TestStruct { field: 42 };
17404 }
17405 "});
17406 let symbol_range = cx.lsp_range(indoc! {"
17407 struct TestStruct {
17408 field: i32
17409 }
17410
17411 «fn main»() {
17412 let unused_var = 42;
17413 let test_struct = TestStruct { field: 42 };
17414 }
17415 "});
17416 let mut hover_requests =
17417 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17418 Ok(Some(lsp::Hover {
17419 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17420 kind: lsp::MarkupKind::Markdown,
17421 value: "Function documentation".to_string(),
17422 }),
17423 range: Some(symbol_range),
17424 }))
17425 });
17426
17427 // Case 1: Test that code action menu hide hover popover
17428 cx.dispatch_action(Hover);
17429 hover_requests.next().await;
17430 cx.condition(|editor, _| editor.hover_state.visible()).await;
17431 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17432 move |_, _, _| async move {
17433 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17434 lsp::CodeAction {
17435 title: "Remove unused variable".to_string(),
17436 kind: Some(CodeActionKind::QUICKFIX),
17437 edit: Some(lsp::WorkspaceEdit {
17438 changes: Some(
17439 [(
17440 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17441 vec![lsp::TextEdit {
17442 range: lsp::Range::new(
17443 lsp::Position::new(5, 4),
17444 lsp::Position::new(5, 27),
17445 ),
17446 new_text: "".to_string(),
17447 }],
17448 )]
17449 .into_iter()
17450 .collect(),
17451 ),
17452 ..Default::default()
17453 }),
17454 ..Default::default()
17455 },
17456 )]))
17457 },
17458 );
17459 cx.update_editor(|editor, window, cx| {
17460 editor.toggle_code_actions(
17461 &ToggleCodeActions {
17462 deployed_from: None,
17463 quick_launch: false,
17464 },
17465 window,
17466 cx,
17467 );
17468 });
17469 code_action_requests.next().await;
17470 cx.run_until_parked();
17471 cx.condition(|editor, _| editor.context_menu_visible())
17472 .await;
17473 cx.update_editor(|editor, _, _| {
17474 assert!(
17475 !editor.hover_state.visible(),
17476 "Hover popover should be hidden when code action menu is shown"
17477 );
17478 // Hide code actions
17479 editor.context_menu.take();
17480 });
17481
17482 // Case 2: Test that code completions hide hover popover
17483 cx.dispatch_action(Hover);
17484 hover_requests.next().await;
17485 cx.condition(|editor, _| editor.hover_state.visible()).await;
17486 let counter = Arc::new(AtomicUsize::new(0));
17487 let mut completion_requests =
17488 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17489 let counter = counter.clone();
17490 async move {
17491 counter.fetch_add(1, atomic::Ordering::Release);
17492 Ok(Some(lsp::CompletionResponse::Array(vec![
17493 lsp::CompletionItem {
17494 label: "main".into(),
17495 kind: Some(lsp::CompletionItemKind::FUNCTION),
17496 detail: Some("() -> ()".to_string()),
17497 ..Default::default()
17498 },
17499 lsp::CompletionItem {
17500 label: "TestStruct".into(),
17501 kind: Some(lsp::CompletionItemKind::STRUCT),
17502 detail: Some("struct TestStruct".to_string()),
17503 ..Default::default()
17504 },
17505 ])))
17506 }
17507 });
17508 cx.update_editor(|editor, window, cx| {
17509 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17510 });
17511 completion_requests.next().await;
17512 cx.condition(|editor, _| editor.context_menu_visible())
17513 .await;
17514 cx.update_editor(|editor, _, _| {
17515 assert!(
17516 !editor.hover_state.visible(),
17517 "Hover popover should be hidden when completion menu is shown"
17518 );
17519 });
17520}
17521
17522#[gpui::test]
17523async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17524 init_test(cx, |_| {});
17525
17526 let mut cx = EditorLspTestContext::new_rust(
17527 lsp::ServerCapabilities {
17528 completion_provider: Some(lsp::CompletionOptions {
17529 trigger_characters: Some(vec![".".to_string()]),
17530 resolve_provider: Some(true),
17531 ..Default::default()
17532 }),
17533 ..Default::default()
17534 },
17535 cx,
17536 )
17537 .await;
17538
17539 cx.set_state("fn main() { let a = 2ˇ; }");
17540 cx.simulate_keystroke(".");
17541
17542 let unresolved_item_1 = lsp::CompletionItem {
17543 label: "id".to_string(),
17544 filter_text: Some("id".to_string()),
17545 detail: None,
17546 documentation: None,
17547 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17548 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17549 new_text: ".id".to_string(),
17550 })),
17551 ..lsp::CompletionItem::default()
17552 };
17553 let resolved_item_1 = lsp::CompletionItem {
17554 additional_text_edits: Some(vec![lsp::TextEdit {
17555 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17556 new_text: "!!".to_string(),
17557 }]),
17558 ..unresolved_item_1.clone()
17559 };
17560 let unresolved_item_2 = lsp::CompletionItem {
17561 label: "other".to_string(),
17562 filter_text: Some("other".to_string()),
17563 detail: None,
17564 documentation: None,
17565 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17566 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17567 new_text: ".other".to_string(),
17568 })),
17569 ..lsp::CompletionItem::default()
17570 };
17571 let resolved_item_2 = lsp::CompletionItem {
17572 additional_text_edits: Some(vec![lsp::TextEdit {
17573 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17574 new_text: "??".to_string(),
17575 }]),
17576 ..unresolved_item_2.clone()
17577 };
17578
17579 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17580 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17581 cx.lsp
17582 .server
17583 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17584 let unresolved_item_1 = unresolved_item_1.clone();
17585 let resolved_item_1 = resolved_item_1.clone();
17586 let unresolved_item_2 = unresolved_item_2.clone();
17587 let resolved_item_2 = resolved_item_2.clone();
17588 let resolve_requests_1 = resolve_requests_1.clone();
17589 let resolve_requests_2 = resolve_requests_2.clone();
17590 move |unresolved_request, _| {
17591 let unresolved_item_1 = unresolved_item_1.clone();
17592 let resolved_item_1 = resolved_item_1.clone();
17593 let unresolved_item_2 = unresolved_item_2.clone();
17594 let resolved_item_2 = resolved_item_2.clone();
17595 let resolve_requests_1 = resolve_requests_1.clone();
17596 let resolve_requests_2 = resolve_requests_2.clone();
17597 async move {
17598 if unresolved_request == unresolved_item_1 {
17599 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17600 Ok(resolved_item_1.clone())
17601 } else if unresolved_request == unresolved_item_2 {
17602 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17603 Ok(resolved_item_2.clone())
17604 } else {
17605 panic!("Unexpected completion item {unresolved_request:?}")
17606 }
17607 }
17608 }
17609 })
17610 .detach();
17611
17612 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17613 let unresolved_item_1 = unresolved_item_1.clone();
17614 let unresolved_item_2 = unresolved_item_2.clone();
17615 async move {
17616 Ok(Some(lsp::CompletionResponse::Array(vec![
17617 unresolved_item_1,
17618 unresolved_item_2,
17619 ])))
17620 }
17621 })
17622 .next()
17623 .await;
17624
17625 cx.condition(|editor, _| editor.context_menu_visible())
17626 .await;
17627 cx.update_editor(|editor, _, _| {
17628 let context_menu = editor.context_menu.borrow_mut();
17629 let context_menu = context_menu
17630 .as_ref()
17631 .expect("Should have the context menu deployed");
17632 match context_menu {
17633 CodeContextMenu::Completions(completions_menu) => {
17634 let completions = completions_menu.completions.borrow_mut();
17635 assert_eq!(
17636 completions
17637 .iter()
17638 .map(|completion| &completion.label.text)
17639 .collect::<Vec<_>>(),
17640 vec!["id", "other"]
17641 )
17642 }
17643 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17644 }
17645 });
17646 cx.run_until_parked();
17647
17648 cx.update_editor(|editor, window, cx| {
17649 editor.context_menu_next(&ContextMenuNext, window, cx);
17650 });
17651 cx.run_until_parked();
17652 cx.update_editor(|editor, window, cx| {
17653 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17654 });
17655 cx.run_until_parked();
17656 cx.update_editor(|editor, window, cx| {
17657 editor.context_menu_next(&ContextMenuNext, window, cx);
17658 });
17659 cx.run_until_parked();
17660 cx.update_editor(|editor, window, cx| {
17661 editor
17662 .compose_completion(&ComposeCompletion::default(), window, cx)
17663 .expect("No task returned")
17664 })
17665 .await
17666 .expect("Completion failed");
17667 cx.run_until_parked();
17668
17669 cx.update_editor(|editor, _, cx| {
17670 assert_eq!(
17671 resolve_requests_1.load(atomic::Ordering::Acquire),
17672 1,
17673 "Should always resolve once despite multiple selections"
17674 );
17675 assert_eq!(
17676 resolve_requests_2.load(atomic::Ordering::Acquire),
17677 1,
17678 "Should always resolve once after multiple selections and applying the completion"
17679 );
17680 assert_eq!(
17681 editor.text(cx),
17682 "fn main() { let a = ??.other; }",
17683 "Should use resolved data when applying the completion"
17684 );
17685 });
17686}
17687
17688#[gpui::test]
17689async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17690 init_test(cx, |_| {});
17691
17692 let item_0 = lsp::CompletionItem {
17693 label: "abs".into(),
17694 insert_text: Some("abs".into()),
17695 data: Some(json!({ "very": "special"})),
17696 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17697 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17698 lsp::InsertReplaceEdit {
17699 new_text: "abs".to_string(),
17700 insert: lsp::Range::default(),
17701 replace: lsp::Range::default(),
17702 },
17703 )),
17704 ..lsp::CompletionItem::default()
17705 };
17706 let items = iter::once(item_0.clone())
17707 .chain((11..51).map(|i| lsp::CompletionItem {
17708 label: format!("item_{}", i),
17709 insert_text: Some(format!("item_{}", i)),
17710 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17711 ..lsp::CompletionItem::default()
17712 }))
17713 .collect::<Vec<_>>();
17714
17715 let default_commit_characters = vec!["?".to_string()];
17716 let default_data = json!({ "default": "data"});
17717 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17718 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17719 let default_edit_range = lsp::Range {
17720 start: lsp::Position {
17721 line: 0,
17722 character: 5,
17723 },
17724 end: lsp::Position {
17725 line: 0,
17726 character: 5,
17727 },
17728 };
17729
17730 let mut cx = EditorLspTestContext::new_rust(
17731 lsp::ServerCapabilities {
17732 completion_provider: Some(lsp::CompletionOptions {
17733 trigger_characters: Some(vec![".".to_string()]),
17734 resolve_provider: Some(true),
17735 ..Default::default()
17736 }),
17737 ..Default::default()
17738 },
17739 cx,
17740 )
17741 .await;
17742
17743 cx.set_state("fn main() { let a = 2ˇ; }");
17744 cx.simulate_keystroke(".");
17745
17746 let completion_data = default_data.clone();
17747 let completion_characters = default_commit_characters.clone();
17748 let completion_items = items.clone();
17749 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17750 let default_data = completion_data.clone();
17751 let default_commit_characters = completion_characters.clone();
17752 let items = completion_items.clone();
17753 async move {
17754 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17755 items,
17756 item_defaults: Some(lsp::CompletionListItemDefaults {
17757 data: Some(default_data.clone()),
17758 commit_characters: Some(default_commit_characters.clone()),
17759 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17760 default_edit_range,
17761 )),
17762 insert_text_format: Some(default_insert_text_format),
17763 insert_text_mode: Some(default_insert_text_mode),
17764 }),
17765 ..lsp::CompletionList::default()
17766 })))
17767 }
17768 })
17769 .next()
17770 .await;
17771
17772 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17773 cx.lsp
17774 .server
17775 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17776 let closure_resolved_items = resolved_items.clone();
17777 move |item_to_resolve, _| {
17778 let closure_resolved_items = closure_resolved_items.clone();
17779 async move {
17780 closure_resolved_items.lock().push(item_to_resolve.clone());
17781 Ok(item_to_resolve)
17782 }
17783 }
17784 })
17785 .detach();
17786
17787 cx.condition(|editor, _| editor.context_menu_visible())
17788 .await;
17789 cx.run_until_parked();
17790 cx.update_editor(|editor, _, _| {
17791 let menu = editor.context_menu.borrow_mut();
17792 match menu.as_ref().expect("should have the completions menu") {
17793 CodeContextMenu::Completions(completions_menu) => {
17794 assert_eq!(
17795 completions_menu
17796 .entries
17797 .borrow()
17798 .iter()
17799 .map(|mat| mat.string.clone())
17800 .collect::<Vec<String>>(),
17801 items
17802 .iter()
17803 .map(|completion| completion.label.clone())
17804 .collect::<Vec<String>>()
17805 );
17806 }
17807 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17808 }
17809 });
17810 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17811 // with 4 from the end.
17812 assert_eq!(
17813 *resolved_items.lock(),
17814 [&items[0..16], &items[items.len() - 4..items.len()]]
17815 .concat()
17816 .iter()
17817 .cloned()
17818 .map(|mut item| {
17819 if item.data.is_none() {
17820 item.data = Some(default_data.clone());
17821 }
17822 item
17823 })
17824 .collect::<Vec<lsp::CompletionItem>>(),
17825 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17826 );
17827 resolved_items.lock().clear();
17828
17829 cx.update_editor(|editor, window, cx| {
17830 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17831 });
17832 cx.run_until_parked();
17833 // Completions that have already been resolved are skipped.
17834 assert_eq!(
17835 *resolved_items.lock(),
17836 items[items.len() - 17..items.len() - 4]
17837 .iter()
17838 .cloned()
17839 .map(|mut item| {
17840 if item.data.is_none() {
17841 item.data = Some(default_data.clone());
17842 }
17843 item
17844 })
17845 .collect::<Vec<lsp::CompletionItem>>()
17846 );
17847 resolved_items.lock().clear();
17848}
17849
17850#[gpui::test]
17851async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17852 init_test(cx, |_| {});
17853
17854 let mut cx = EditorLspTestContext::new(
17855 Language::new(
17856 LanguageConfig {
17857 matcher: LanguageMatcher {
17858 path_suffixes: vec!["jsx".into()],
17859 ..Default::default()
17860 },
17861 overrides: [(
17862 "element".into(),
17863 LanguageConfigOverride {
17864 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17865 ..Default::default()
17866 },
17867 )]
17868 .into_iter()
17869 .collect(),
17870 ..Default::default()
17871 },
17872 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17873 )
17874 .with_override_query("(jsx_self_closing_element) @element")
17875 .unwrap(),
17876 lsp::ServerCapabilities {
17877 completion_provider: Some(lsp::CompletionOptions {
17878 trigger_characters: Some(vec![":".to_string()]),
17879 ..Default::default()
17880 }),
17881 ..Default::default()
17882 },
17883 cx,
17884 )
17885 .await;
17886
17887 cx.lsp
17888 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17889 Ok(Some(lsp::CompletionResponse::Array(vec![
17890 lsp::CompletionItem {
17891 label: "bg-blue".into(),
17892 ..Default::default()
17893 },
17894 lsp::CompletionItem {
17895 label: "bg-red".into(),
17896 ..Default::default()
17897 },
17898 lsp::CompletionItem {
17899 label: "bg-yellow".into(),
17900 ..Default::default()
17901 },
17902 ])))
17903 });
17904
17905 cx.set_state(r#"<p class="bgˇ" />"#);
17906
17907 // Trigger completion when typing a dash, because the dash is an extra
17908 // word character in the 'element' scope, which contains the cursor.
17909 cx.simulate_keystroke("-");
17910 cx.executor().run_until_parked();
17911 cx.update_editor(|editor, _, _| {
17912 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17913 {
17914 assert_eq!(
17915 completion_menu_entries(menu),
17916 &["bg-blue", "bg-red", "bg-yellow"]
17917 );
17918 } else {
17919 panic!("expected completion menu to be open");
17920 }
17921 });
17922
17923 cx.simulate_keystroke("l");
17924 cx.executor().run_until_parked();
17925 cx.update_editor(|editor, _, _| {
17926 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17927 {
17928 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17929 } else {
17930 panic!("expected completion menu to be open");
17931 }
17932 });
17933
17934 // When filtering completions, consider the character after the '-' to
17935 // be the start of a subword.
17936 cx.set_state(r#"<p class="yelˇ" />"#);
17937 cx.simulate_keystroke("l");
17938 cx.executor().run_until_parked();
17939 cx.update_editor(|editor, _, _| {
17940 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17941 {
17942 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
17943 } else {
17944 panic!("expected completion menu to be open");
17945 }
17946 });
17947}
17948
17949fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
17950 let entries = menu.entries.borrow();
17951 entries.iter().map(|mat| mat.string.clone()).collect()
17952}
17953
17954#[gpui::test]
17955async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
17956 init_test(cx, |settings| {
17957 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
17958 Formatter::Prettier,
17959 )))
17960 });
17961
17962 let fs = FakeFs::new(cx.executor());
17963 fs.insert_file(path!("/file.ts"), Default::default()).await;
17964
17965 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
17966 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17967
17968 language_registry.add(Arc::new(Language::new(
17969 LanguageConfig {
17970 name: "TypeScript".into(),
17971 matcher: LanguageMatcher {
17972 path_suffixes: vec!["ts".to_string()],
17973 ..Default::default()
17974 },
17975 ..Default::default()
17976 },
17977 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17978 )));
17979 update_test_language_settings(cx, |settings| {
17980 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
17981 });
17982
17983 let test_plugin = "test_plugin";
17984 let _ = language_registry.register_fake_lsp(
17985 "TypeScript",
17986 FakeLspAdapter {
17987 prettier_plugins: vec![test_plugin],
17988 ..Default::default()
17989 },
17990 );
17991
17992 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
17993 let buffer = project
17994 .update(cx, |project, cx| {
17995 project.open_local_buffer(path!("/file.ts"), cx)
17996 })
17997 .await
17998 .unwrap();
17999
18000 let buffer_text = "one\ntwo\nthree\n";
18001 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18002 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18003 editor.update_in(cx, |editor, window, cx| {
18004 editor.set_text(buffer_text, window, cx)
18005 });
18006
18007 editor
18008 .update_in(cx, |editor, window, cx| {
18009 editor.perform_format(
18010 project.clone(),
18011 FormatTrigger::Manual,
18012 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18013 window,
18014 cx,
18015 )
18016 })
18017 .unwrap()
18018 .await;
18019 assert_eq!(
18020 editor.update(cx, |editor, cx| editor.text(cx)),
18021 buffer_text.to_string() + prettier_format_suffix,
18022 "Test prettier formatting was not applied to the original buffer text",
18023 );
18024
18025 update_test_language_settings(cx, |settings| {
18026 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18027 });
18028 let format = editor.update_in(cx, |editor, window, cx| {
18029 editor.perform_format(
18030 project.clone(),
18031 FormatTrigger::Manual,
18032 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18033 window,
18034 cx,
18035 )
18036 });
18037 format.await.unwrap();
18038 assert_eq!(
18039 editor.update(cx, |editor, cx| editor.text(cx)),
18040 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18041 "Autoformatting (via test prettier) was not applied to the original buffer text",
18042 );
18043}
18044
18045#[gpui::test]
18046async fn test_addition_reverts(cx: &mut TestAppContext) {
18047 init_test(cx, |_| {});
18048 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18049 let base_text = indoc! {r#"
18050 struct Row;
18051 struct Row1;
18052 struct Row2;
18053
18054 struct Row4;
18055 struct Row5;
18056 struct Row6;
18057
18058 struct Row8;
18059 struct Row9;
18060 struct Row10;"#};
18061
18062 // When addition hunks are not adjacent to carets, no hunk revert is performed
18063 assert_hunk_revert(
18064 indoc! {r#"struct Row;
18065 struct Row1;
18066 struct Row1.1;
18067 struct Row1.2;
18068 struct Row2;ˇ
18069
18070 struct Row4;
18071 struct Row5;
18072 struct Row6;
18073
18074 struct Row8;
18075 ˇstruct Row9;
18076 struct Row9.1;
18077 struct Row9.2;
18078 struct Row9.3;
18079 struct Row10;"#},
18080 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18081 indoc! {r#"struct Row;
18082 struct Row1;
18083 struct Row1.1;
18084 struct Row1.2;
18085 struct Row2;ˇ
18086
18087 struct Row4;
18088 struct Row5;
18089 struct Row6;
18090
18091 struct Row8;
18092 ˇstruct Row9;
18093 struct Row9.1;
18094 struct Row9.2;
18095 struct Row9.3;
18096 struct Row10;"#},
18097 base_text,
18098 &mut cx,
18099 );
18100 // Same for selections
18101 assert_hunk_revert(
18102 indoc! {r#"struct Row;
18103 struct Row1;
18104 struct Row2;
18105 struct Row2.1;
18106 struct Row2.2;
18107 «ˇ
18108 struct Row4;
18109 struct» Row5;
18110 «struct Row6;
18111 ˇ»
18112 struct Row9.1;
18113 struct Row9.2;
18114 struct Row9.3;
18115 struct Row8;
18116 struct Row9;
18117 struct Row10;"#},
18118 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18119 indoc! {r#"struct Row;
18120 struct Row1;
18121 struct Row2;
18122 struct Row2.1;
18123 struct Row2.2;
18124 «ˇ
18125 struct Row4;
18126 struct» Row5;
18127 «struct Row6;
18128 ˇ»
18129 struct Row9.1;
18130 struct Row9.2;
18131 struct Row9.3;
18132 struct Row8;
18133 struct Row9;
18134 struct Row10;"#},
18135 base_text,
18136 &mut cx,
18137 );
18138
18139 // When carets and selections intersect the addition hunks, those are reverted.
18140 // Adjacent carets got merged.
18141 assert_hunk_revert(
18142 indoc! {r#"struct Row;
18143 ˇ// something on the top
18144 struct Row1;
18145 struct Row2;
18146 struct Roˇw3.1;
18147 struct Row2.2;
18148 struct Row2.3;ˇ
18149
18150 struct Row4;
18151 struct ˇRow5.1;
18152 struct Row5.2;
18153 struct «Rowˇ»5.3;
18154 struct Row5;
18155 struct Row6;
18156 ˇ
18157 struct Row9.1;
18158 struct «Rowˇ»9.2;
18159 struct «ˇRow»9.3;
18160 struct Row8;
18161 struct Row9;
18162 «ˇ// something on bottom»
18163 struct Row10;"#},
18164 vec![
18165 DiffHunkStatusKind::Added,
18166 DiffHunkStatusKind::Added,
18167 DiffHunkStatusKind::Added,
18168 DiffHunkStatusKind::Added,
18169 DiffHunkStatusKind::Added,
18170 ],
18171 indoc! {r#"struct Row;
18172 ˇstruct Row1;
18173 struct Row2;
18174 ˇ
18175 struct Row4;
18176 ˇstruct Row5;
18177 struct Row6;
18178 ˇ
18179 ˇstruct Row8;
18180 struct Row9;
18181 ˇstruct Row10;"#},
18182 base_text,
18183 &mut cx,
18184 );
18185}
18186
18187#[gpui::test]
18188async fn test_modification_reverts(cx: &mut TestAppContext) {
18189 init_test(cx, |_| {});
18190 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18191 let base_text = indoc! {r#"
18192 struct Row;
18193 struct Row1;
18194 struct Row2;
18195
18196 struct Row4;
18197 struct Row5;
18198 struct Row6;
18199
18200 struct Row8;
18201 struct Row9;
18202 struct Row10;"#};
18203
18204 // Modification hunks behave the same as the addition ones.
18205 assert_hunk_revert(
18206 indoc! {r#"struct Row;
18207 struct Row1;
18208 struct Row33;
18209 ˇ
18210 struct Row4;
18211 struct Row5;
18212 struct Row6;
18213 ˇ
18214 struct Row99;
18215 struct Row9;
18216 struct Row10;"#},
18217 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18218 indoc! {r#"struct Row;
18219 struct Row1;
18220 struct Row33;
18221 ˇ
18222 struct Row4;
18223 struct Row5;
18224 struct Row6;
18225 ˇ
18226 struct Row99;
18227 struct Row9;
18228 struct Row10;"#},
18229 base_text,
18230 &mut cx,
18231 );
18232 assert_hunk_revert(
18233 indoc! {r#"struct Row;
18234 struct Row1;
18235 struct Row33;
18236 «ˇ
18237 struct Row4;
18238 struct» Row5;
18239 «struct Row6;
18240 ˇ»
18241 struct Row99;
18242 struct Row9;
18243 struct Row10;"#},
18244 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18245 indoc! {r#"struct Row;
18246 struct Row1;
18247 struct Row33;
18248 «ˇ
18249 struct Row4;
18250 struct» Row5;
18251 «struct Row6;
18252 ˇ»
18253 struct Row99;
18254 struct Row9;
18255 struct Row10;"#},
18256 base_text,
18257 &mut cx,
18258 );
18259
18260 assert_hunk_revert(
18261 indoc! {r#"ˇstruct Row1.1;
18262 struct Row1;
18263 «ˇstr»uct Row22;
18264
18265 struct ˇRow44;
18266 struct Row5;
18267 struct «Rˇ»ow66;ˇ
18268
18269 «struˇ»ct Row88;
18270 struct Row9;
18271 struct Row1011;ˇ"#},
18272 vec![
18273 DiffHunkStatusKind::Modified,
18274 DiffHunkStatusKind::Modified,
18275 DiffHunkStatusKind::Modified,
18276 DiffHunkStatusKind::Modified,
18277 DiffHunkStatusKind::Modified,
18278 DiffHunkStatusKind::Modified,
18279 ],
18280 indoc! {r#"struct Row;
18281 ˇstruct Row1;
18282 struct Row2;
18283 ˇ
18284 struct Row4;
18285 ˇstruct Row5;
18286 struct Row6;
18287 ˇ
18288 struct Row8;
18289 ˇstruct Row9;
18290 struct Row10;ˇ"#},
18291 base_text,
18292 &mut cx,
18293 );
18294}
18295
18296#[gpui::test]
18297async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18298 init_test(cx, |_| {});
18299 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18300 let base_text = indoc! {r#"
18301 one
18302
18303 two
18304 three
18305 "#};
18306
18307 cx.set_head_text(base_text);
18308 cx.set_state("\nˇ\n");
18309 cx.executor().run_until_parked();
18310 cx.update_editor(|editor, _window, cx| {
18311 editor.expand_selected_diff_hunks(cx);
18312 });
18313 cx.executor().run_until_parked();
18314 cx.update_editor(|editor, window, cx| {
18315 editor.backspace(&Default::default(), window, cx);
18316 });
18317 cx.run_until_parked();
18318 cx.assert_state_with_diff(
18319 indoc! {r#"
18320
18321 - two
18322 - threeˇ
18323 +
18324 "#}
18325 .to_string(),
18326 );
18327}
18328
18329#[gpui::test]
18330async fn test_deletion_reverts(cx: &mut TestAppContext) {
18331 init_test(cx, |_| {});
18332 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18333 let base_text = indoc! {r#"struct Row;
18334struct Row1;
18335struct Row2;
18336
18337struct Row4;
18338struct Row5;
18339struct Row6;
18340
18341struct Row8;
18342struct Row9;
18343struct Row10;"#};
18344
18345 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18346 assert_hunk_revert(
18347 indoc! {r#"struct Row;
18348 struct Row2;
18349
18350 ˇstruct Row4;
18351 struct Row5;
18352 struct Row6;
18353 ˇ
18354 struct Row8;
18355 struct Row10;"#},
18356 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18357 indoc! {r#"struct Row;
18358 struct Row2;
18359
18360 ˇstruct Row4;
18361 struct Row5;
18362 struct Row6;
18363 ˇ
18364 struct Row8;
18365 struct Row10;"#},
18366 base_text,
18367 &mut cx,
18368 );
18369 assert_hunk_revert(
18370 indoc! {r#"struct Row;
18371 struct Row2;
18372
18373 «ˇstruct Row4;
18374 struct» Row5;
18375 «struct Row6;
18376 ˇ»
18377 struct Row8;
18378 struct Row10;"#},
18379 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18380 indoc! {r#"struct Row;
18381 struct Row2;
18382
18383 «ˇstruct Row4;
18384 struct» Row5;
18385 «struct Row6;
18386 ˇ»
18387 struct Row8;
18388 struct Row10;"#},
18389 base_text,
18390 &mut cx,
18391 );
18392
18393 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18394 assert_hunk_revert(
18395 indoc! {r#"struct Row;
18396 ˇstruct Row2;
18397
18398 struct Row4;
18399 struct Row5;
18400 struct Row6;
18401
18402 struct Row8;ˇ
18403 struct Row10;"#},
18404 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18405 indoc! {r#"struct Row;
18406 struct Row1;
18407 ˇstruct Row2;
18408
18409 struct Row4;
18410 struct Row5;
18411 struct Row6;
18412
18413 struct Row8;ˇ
18414 struct Row9;
18415 struct Row10;"#},
18416 base_text,
18417 &mut cx,
18418 );
18419 assert_hunk_revert(
18420 indoc! {r#"struct Row;
18421 struct Row2«ˇ;
18422 struct Row4;
18423 struct» Row5;
18424 «struct Row6;
18425
18426 struct Row8;ˇ»
18427 struct Row10;"#},
18428 vec![
18429 DiffHunkStatusKind::Deleted,
18430 DiffHunkStatusKind::Deleted,
18431 DiffHunkStatusKind::Deleted,
18432 ],
18433 indoc! {r#"struct Row;
18434 struct Row1;
18435 struct Row2«ˇ;
18436
18437 struct Row4;
18438 struct» Row5;
18439 «struct Row6;
18440
18441 struct Row8;ˇ»
18442 struct Row9;
18443 struct Row10;"#},
18444 base_text,
18445 &mut cx,
18446 );
18447}
18448
18449#[gpui::test]
18450async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18451 init_test(cx, |_| {});
18452
18453 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18454 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18455 let base_text_3 =
18456 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18457
18458 let text_1 = edit_first_char_of_every_line(base_text_1);
18459 let text_2 = edit_first_char_of_every_line(base_text_2);
18460 let text_3 = edit_first_char_of_every_line(base_text_3);
18461
18462 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18463 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18464 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18465
18466 let multibuffer = cx.new(|cx| {
18467 let mut multibuffer = MultiBuffer::new(ReadWrite);
18468 multibuffer.push_excerpts(
18469 buffer_1.clone(),
18470 [
18471 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18472 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18473 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18474 ],
18475 cx,
18476 );
18477 multibuffer.push_excerpts(
18478 buffer_2.clone(),
18479 [
18480 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18481 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18482 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18483 ],
18484 cx,
18485 );
18486 multibuffer.push_excerpts(
18487 buffer_3.clone(),
18488 [
18489 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18490 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18491 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18492 ],
18493 cx,
18494 );
18495 multibuffer
18496 });
18497
18498 let fs = FakeFs::new(cx.executor());
18499 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18500 let (editor, cx) = cx
18501 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18502 editor.update_in(cx, |editor, _window, cx| {
18503 for (buffer, diff_base) in [
18504 (buffer_1.clone(), base_text_1),
18505 (buffer_2.clone(), base_text_2),
18506 (buffer_3.clone(), base_text_3),
18507 ] {
18508 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18509 editor
18510 .buffer
18511 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18512 }
18513 });
18514 cx.executor().run_until_parked();
18515
18516 editor.update_in(cx, |editor, window, cx| {
18517 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}");
18518 editor.select_all(&SelectAll, window, cx);
18519 editor.git_restore(&Default::default(), window, cx);
18520 });
18521 cx.executor().run_until_parked();
18522
18523 // When all ranges are selected, all buffer hunks are reverted.
18524 editor.update(cx, |editor, cx| {
18525 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");
18526 });
18527 buffer_1.update(cx, |buffer, _| {
18528 assert_eq!(buffer.text(), base_text_1);
18529 });
18530 buffer_2.update(cx, |buffer, _| {
18531 assert_eq!(buffer.text(), base_text_2);
18532 });
18533 buffer_3.update(cx, |buffer, _| {
18534 assert_eq!(buffer.text(), base_text_3);
18535 });
18536
18537 editor.update_in(cx, |editor, window, cx| {
18538 editor.undo(&Default::default(), window, cx);
18539 });
18540
18541 editor.update_in(cx, |editor, window, cx| {
18542 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18543 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18544 });
18545 editor.git_restore(&Default::default(), window, cx);
18546 });
18547
18548 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18549 // but not affect buffer_2 and its related excerpts.
18550 editor.update(cx, |editor, cx| {
18551 assert_eq!(
18552 editor.text(cx),
18553 "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}"
18554 );
18555 });
18556 buffer_1.update(cx, |buffer, _| {
18557 assert_eq!(buffer.text(), base_text_1);
18558 });
18559 buffer_2.update(cx, |buffer, _| {
18560 assert_eq!(
18561 buffer.text(),
18562 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18563 );
18564 });
18565 buffer_3.update(cx, |buffer, _| {
18566 assert_eq!(
18567 buffer.text(),
18568 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18569 );
18570 });
18571
18572 fn edit_first_char_of_every_line(text: &str) -> String {
18573 text.split('\n')
18574 .map(|line| format!("X{}", &line[1..]))
18575 .collect::<Vec<_>>()
18576 .join("\n")
18577 }
18578}
18579
18580#[gpui::test]
18581async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18582 init_test(cx, |_| {});
18583
18584 let cols = 4;
18585 let rows = 10;
18586 let sample_text_1 = sample_text(rows, cols, 'a');
18587 assert_eq!(
18588 sample_text_1,
18589 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18590 );
18591 let sample_text_2 = sample_text(rows, cols, 'l');
18592 assert_eq!(
18593 sample_text_2,
18594 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18595 );
18596 let sample_text_3 = sample_text(rows, cols, 'v');
18597 assert_eq!(
18598 sample_text_3,
18599 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18600 );
18601
18602 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18603 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18604 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18605
18606 let multi_buffer = cx.new(|cx| {
18607 let mut multibuffer = MultiBuffer::new(ReadWrite);
18608 multibuffer.push_excerpts(
18609 buffer_1.clone(),
18610 [
18611 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18612 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18613 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18614 ],
18615 cx,
18616 );
18617 multibuffer.push_excerpts(
18618 buffer_2.clone(),
18619 [
18620 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18621 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18622 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18623 ],
18624 cx,
18625 );
18626 multibuffer.push_excerpts(
18627 buffer_3.clone(),
18628 [
18629 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18630 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18631 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18632 ],
18633 cx,
18634 );
18635 multibuffer
18636 });
18637
18638 let fs = FakeFs::new(cx.executor());
18639 fs.insert_tree(
18640 "/a",
18641 json!({
18642 "main.rs": sample_text_1,
18643 "other.rs": sample_text_2,
18644 "lib.rs": sample_text_3,
18645 }),
18646 )
18647 .await;
18648 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18649 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18650 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18651 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18652 Editor::new(
18653 EditorMode::full(),
18654 multi_buffer,
18655 Some(project.clone()),
18656 window,
18657 cx,
18658 )
18659 });
18660 let multibuffer_item_id = workspace
18661 .update(cx, |workspace, window, cx| {
18662 assert!(
18663 workspace.active_item(cx).is_none(),
18664 "active item should be None before the first item is added"
18665 );
18666 workspace.add_item_to_active_pane(
18667 Box::new(multi_buffer_editor.clone()),
18668 None,
18669 true,
18670 window,
18671 cx,
18672 );
18673 let active_item = workspace
18674 .active_item(cx)
18675 .expect("should have an active item after adding the multi buffer");
18676 assert!(
18677 !active_item.is_singleton(cx),
18678 "A multi buffer was expected to active after adding"
18679 );
18680 active_item.item_id()
18681 })
18682 .unwrap();
18683 cx.executor().run_until_parked();
18684
18685 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18686 editor.change_selections(
18687 SelectionEffects::scroll(Autoscroll::Next),
18688 window,
18689 cx,
18690 |s| s.select_ranges(Some(1..2)),
18691 );
18692 editor.open_excerpts(&OpenExcerpts, window, cx);
18693 });
18694 cx.executor().run_until_parked();
18695 let first_item_id = workspace
18696 .update(cx, |workspace, window, cx| {
18697 let active_item = workspace
18698 .active_item(cx)
18699 .expect("should have an active item after navigating into the 1st buffer");
18700 let first_item_id = active_item.item_id();
18701 assert_ne!(
18702 first_item_id, multibuffer_item_id,
18703 "Should navigate into the 1st buffer and activate it"
18704 );
18705 assert!(
18706 active_item.is_singleton(cx),
18707 "New active item should be a singleton buffer"
18708 );
18709 assert_eq!(
18710 active_item
18711 .act_as::<Editor>(cx)
18712 .expect("should have navigated into an editor for the 1st buffer")
18713 .read(cx)
18714 .text(cx),
18715 sample_text_1
18716 );
18717
18718 workspace
18719 .go_back(workspace.active_pane().downgrade(), window, cx)
18720 .detach_and_log_err(cx);
18721
18722 first_item_id
18723 })
18724 .unwrap();
18725 cx.executor().run_until_parked();
18726 workspace
18727 .update(cx, |workspace, _, cx| {
18728 let active_item = workspace
18729 .active_item(cx)
18730 .expect("should have an active item after navigating back");
18731 assert_eq!(
18732 active_item.item_id(),
18733 multibuffer_item_id,
18734 "Should navigate back to the multi buffer"
18735 );
18736 assert!(!active_item.is_singleton(cx));
18737 })
18738 .unwrap();
18739
18740 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18741 editor.change_selections(
18742 SelectionEffects::scroll(Autoscroll::Next),
18743 window,
18744 cx,
18745 |s| s.select_ranges(Some(39..40)),
18746 );
18747 editor.open_excerpts(&OpenExcerpts, window, cx);
18748 });
18749 cx.executor().run_until_parked();
18750 let second_item_id = workspace
18751 .update(cx, |workspace, window, cx| {
18752 let active_item = workspace
18753 .active_item(cx)
18754 .expect("should have an active item after navigating into the 2nd buffer");
18755 let second_item_id = active_item.item_id();
18756 assert_ne!(
18757 second_item_id, multibuffer_item_id,
18758 "Should navigate away from the multibuffer"
18759 );
18760 assert_ne!(
18761 second_item_id, first_item_id,
18762 "Should navigate into the 2nd buffer and activate it"
18763 );
18764 assert!(
18765 active_item.is_singleton(cx),
18766 "New active item should be a singleton buffer"
18767 );
18768 assert_eq!(
18769 active_item
18770 .act_as::<Editor>(cx)
18771 .expect("should have navigated into an editor")
18772 .read(cx)
18773 .text(cx),
18774 sample_text_2
18775 );
18776
18777 workspace
18778 .go_back(workspace.active_pane().downgrade(), window, cx)
18779 .detach_and_log_err(cx);
18780
18781 second_item_id
18782 })
18783 .unwrap();
18784 cx.executor().run_until_parked();
18785 workspace
18786 .update(cx, |workspace, _, cx| {
18787 let active_item = workspace
18788 .active_item(cx)
18789 .expect("should have an active item after navigating back from the 2nd buffer");
18790 assert_eq!(
18791 active_item.item_id(),
18792 multibuffer_item_id,
18793 "Should navigate back from the 2nd buffer to the multi buffer"
18794 );
18795 assert!(!active_item.is_singleton(cx));
18796 })
18797 .unwrap();
18798
18799 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18800 editor.change_selections(
18801 SelectionEffects::scroll(Autoscroll::Next),
18802 window,
18803 cx,
18804 |s| s.select_ranges(Some(70..70)),
18805 );
18806 editor.open_excerpts(&OpenExcerpts, window, cx);
18807 });
18808 cx.executor().run_until_parked();
18809 workspace
18810 .update(cx, |workspace, window, cx| {
18811 let active_item = workspace
18812 .active_item(cx)
18813 .expect("should have an active item after navigating into the 3rd buffer");
18814 let third_item_id = active_item.item_id();
18815 assert_ne!(
18816 third_item_id, multibuffer_item_id,
18817 "Should navigate into the 3rd buffer and activate it"
18818 );
18819 assert_ne!(third_item_id, first_item_id);
18820 assert_ne!(third_item_id, second_item_id);
18821 assert!(
18822 active_item.is_singleton(cx),
18823 "New active item should be a singleton buffer"
18824 );
18825 assert_eq!(
18826 active_item
18827 .act_as::<Editor>(cx)
18828 .expect("should have navigated into an editor")
18829 .read(cx)
18830 .text(cx),
18831 sample_text_3
18832 );
18833
18834 workspace
18835 .go_back(workspace.active_pane().downgrade(), window, cx)
18836 .detach_and_log_err(cx);
18837 })
18838 .unwrap();
18839 cx.executor().run_until_parked();
18840 workspace
18841 .update(cx, |workspace, _, cx| {
18842 let active_item = workspace
18843 .active_item(cx)
18844 .expect("should have an active item after navigating back from the 3rd buffer");
18845 assert_eq!(
18846 active_item.item_id(),
18847 multibuffer_item_id,
18848 "Should navigate back from the 3rd buffer to the multi buffer"
18849 );
18850 assert!(!active_item.is_singleton(cx));
18851 })
18852 .unwrap();
18853}
18854
18855#[gpui::test]
18856async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18857 init_test(cx, |_| {});
18858
18859 let mut cx = EditorTestContext::new(cx).await;
18860
18861 let diff_base = r#"
18862 use some::mod;
18863
18864 const A: u32 = 42;
18865
18866 fn main() {
18867 println!("hello");
18868
18869 println!("world");
18870 }
18871 "#
18872 .unindent();
18873
18874 cx.set_state(
18875 &r#"
18876 use some::modified;
18877
18878 ˇ
18879 fn main() {
18880 println!("hello there");
18881
18882 println!("around the");
18883 println!("world");
18884 }
18885 "#
18886 .unindent(),
18887 );
18888
18889 cx.set_head_text(&diff_base);
18890 executor.run_until_parked();
18891
18892 cx.update_editor(|editor, window, cx| {
18893 editor.go_to_next_hunk(&GoToHunk, window, cx);
18894 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18895 });
18896 executor.run_until_parked();
18897 cx.assert_state_with_diff(
18898 r#"
18899 use some::modified;
18900
18901
18902 fn main() {
18903 - println!("hello");
18904 + ˇ println!("hello there");
18905
18906 println!("around the");
18907 println!("world");
18908 }
18909 "#
18910 .unindent(),
18911 );
18912
18913 cx.update_editor(|editor, window, cx| {
18914 for _ in 0..2 {
18915 editor.go_to_next_hunk(&GoToHunk, window, cx);
18916 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18917 }
18918 });
18919 executor.run_until_parked();
18920 cx.assert_state_with_diff(
18921 r#"
18922 - use some::mod;
18923 + ˇuse some::modified;
18924
18925
18926 fn main() {
18927 - println!("hello");
18928 + println!("hello there");
18929
18930 + println!("around the");
18931 println!("world");
18932 }
18933 "#
18934 .unindent(),
18935 );
18936
18937 cx.update_editor(|editor, window, cx| {
18938 editor.go_to_next_hunk(&GoToHunk, window, cx);
18939 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18940 });
18941 executor.run_until_parked();
18942 cx.assert_state_with_diff(
18943 r#"
18944 - use some::mod;
18945 + use some::modified;
18946
18947 - const A: u32 = 42;
18948 ˇ
18949 fn main() {
18950 - println!("hello");
18951 + println!("hello there");
18952
18953 + println!("around the");
18954 println!("world");
18955 }
18956 "#
18957 .unindent(),
18958 );
18959
18960 cx.update_editor(|editor, window, cx| {
18961 editor.cancel(&Cancel, window, cx);
18962 });
18963
18964 cx.assert_state_with_diff(
18965 r#"
18966 use some::modified;
18967
18968 ˇ
18969 fn main() {
18970 println!("hello there");
18971
18972 println!("around the");
18973 println!("world");
18974 }
18975 "#
18976 .unindent(),
18977 );
18978}
18979
18980#[gpui::test]
18981async fn test_diff_base_change_with_expanded_diff_hunks(
18982 executor: BackgroundExecutor,
18983 cx: &mut TestAppContext,
18984) {
18985 init_test(cx, |_| {});
18986
18987 let mut cx = EditorTestContext::new(cx).await;
18988
18989 let diff_base = r#"
18990 use some::mod1;
18991 use some::mod2;
18992
18993 const A: u32 = 42;
18994 const B: u32 = 42;
18995 const C: u32 = 42;
18996
18997 fn main() {
18998 println!("hello");
18999
19000 println!("world");
19001 }
19002 "#
19003 .unindent();
19004
19005 cx.set_state(
19006 &r#"
19007 use some::mod2;
19008
19009 const A: u32 = 42;
19010 const C: u32 = 42;
19011
19012 fn main(ˇ) {
19013 //println!("hello");
19014
19015 println!("world");
19016 //
19017 //
19018 }
19019 "#
19020 .unindent(),
19021 );
19022
19023 cx.set_head_text(&diff_base);
19024 executor.run_until_parked();
19025
19026 cx.update_editor(|editor, window, cx| {
19027 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19028 });
19029 executor.run_until_parked();
19030 cx.assert_state_with_diff(
19031 r#"
19032 - use some::mod1;
19033 use some::mod2;
19034
19035 const A: u32 = 42;
19036 - const B: u32 = 42;
19037 const C: u32 = 42;
19038
19039 fn main(ˇ) {
19040 - println!("hello");
19041 + //println!("hello");
19042
19043 println!("world");
19044 + //
19045 + //
19046 }
19047 "#
19048 .unindent(),
19049 );
19050
19051 cx.set_head_text("new diff base!");
19052 executor.run_until_parked();
19053 cx.assert_state_with_diff(
19054 r#"
19055 - new diff base!
19056 + use some::mod2;
19057 +
19058 + const A: u32 = 42;
19059 + const C: u32 = 42;
19060 +
19061 + fn main(ˇ) {
19062 + //println!("hello");
19063 +
19064 + println!("world");
19065 + //
19066 + //
19067 + }
19068 "#
19069 .unindent(),
19070 );
19071}
19072
19073#[gpui::test]
19074async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19075 init_test(cx, |_| {});
19076
19077 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19078 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19079 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19080 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19081 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19082 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19083
19084 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19085 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19086 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19087
19088 let multi_buffer = cx.new(|cx| {
19089 let mut multibuffer = MultiBuffer::new(ReadWrite);
19090 multibuffer.push_excerpts(
19091 buffer_1.clone(),
19092 [
19093 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19094 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19095 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19096 ],
19097 cx,
19098 );
19099 multibuffer.push_excerpts(
19100 buffer_2.clone(),
19101 [
19102 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19103 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19104 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19105 ],
19106 cx,
19107 );
19108 multibuffer.push_excerpts(
19109 buffer_3.clone(),
19110 [
19111 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19112 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19113 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19114 ],
19115 cx,
19116 );
19117 multibuffer
19118 });
19119
19120 let editor =
19121 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19122 editor
19123 .update(cx, |editor, _window, cx| {
19124 for (buffer, diff_base) in [
19125 (buffer_1.clone(), file_1_old),
19126 (buffer_2.clone(), file_2_old),
19127 (buffer_3.clone(), file_3_old),
19128 ] {
19129 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19130 editor
19131 .buffer
19132 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19133 }
19134 })
19135 .unwrap();
19136
19137 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19138 cx.run_until_parked();
19139
19140 cx.assert_editor_state(
19141 &"
19142 ˇaaa
19143 ccc
19144 ddd
19145
19146 ggg
19147 hhh
19148
19149
19150 lll
19151 mmm
19152 NNN
19153
19154 qqq
19155 rrr
19156
19157 uuu
19158 111
19159 222
19160 333
19161
19162 666
19163 777
19164
19165 000
19166 !!!"
19167 .unindent(),
19168 );
19169
19170 cx.update_editor(|editor, window, cx| {
19171 editor.select_all(&SelectAll, window, cx);
19172 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19173 });
19174 cx.executor().run_until_parked();
19175
19176 cx.assert_state_with_diff(
19177 "
19178 «aaa
19179 - bbb
19180 ccc
19181 ddd
19182
19183 ggg
19184 hhh
19185
19186
19187 lll
19188 mmm
19189 - nnn
19190 + NNN
19191
19192 qqq
19193 rrr
19194
19195 uuu
19196 111
19197 222
19198 333
19199
19200 + 666
19201 777
19202
19203 000
19204 !!!ˇ»"
19205 .unindent(),
19206 );
19207}
19208
19209#[gpui::test]
19210async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19211 init_test(cx, |_| {});
19212
19213 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19214 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19215
19216 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19217 let multi_buffer = cx.new(|cx| {
19218 let mut multibuffer = MultiBuffer::new(ReadWrite);
19219 multibuffer.push_excerpts(
19220 buffer.clone(),
19221 [
19222 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19223 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19224 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19225 ],
19226 cx,
19227 );
19228 multibuffer
19229 });
19230
19231 let editor =
19232 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19233 editor
19234 .update(cx, |editor, _window, cx| {
19235 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19236 editor
19237 .buffer
19238 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19239 })
19240 .unwrap();
19241
19242 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19243 cx.run_until_parked();
19244
19245 cx.update_editor(|editor, window, cx| {
19246 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19247 });
19248 cx.executor().run_until_parked();
19249
19250 // When the start of a hunk coincides with the start of its excerpt,
19251 // the hunk is expanded. When the start of a hunk is earlier than
19252 // the start of its excerpt, the hunk is not expanded.
19253 cx.assert_state_with_diff(
19254 "
19255 ˇaaa
19256 - bbb
19257 + BBB
19258
19259 - ddd
19260 - eee
19261 + DDD
19262 + EEE
19263 fff
19264
19265 iii
19266 "
19267 .unindent(),
19268 );
19269}
19270
19271#[gpui::test]
19272async fn test_edits_around_expanded_insertion_hunks(
19273 executor: BackgroundExecutor,
19274 cx: &mut TestAppContext,
19275) {
19276 init_test(cx, |_| {});
19277
19278 let mut cx = EditorTestContext::new(cx).await;
19279
19280 let diff_base = r#"
19281 use some::mod1;
19282 use some::mod2;
19283
19284 const A: u32 = 42;
19285
19286 fn main() {
19287 println!("hello");
19288
19289 println!("world");
19290 }
19291 "#
19292 .unindent();
19293 executor.run_until_parked();
19294 cx.set_state(
19295 &r#"
19296 use some::mod1;
19297 use some::mod2;
19298
19299 const A: u32 = 42;
19300 const B: u32 = 42;
19301 const C: u32 = 42;
19302 ˇ
19303
19304 fn main() {
19305 println!("hello");
19306
19307 println!("world");
19308 }
19309 "#
19310 .unindent(),
19311 );
19312
19313 cx.set_head_text(&diff_base);
19314 executor.run_until_parked();
19315
19316 cx.update_editor(|editor, window, cx| {
19317 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19318 });
19319 executor.run_until_parked();
19320
19321 cx.assert_state_with_diff(
19322 r#"
19323 use some::mod1;
19324 use some::mod2;
19325
19326 const A: u32 = 42;
19327 + const B: u32 = 42;
19328 + const C: u32 = 42;
19329 + ˇ
19330
19331 fn main() {
19332 println!("hello");
19333
19334 println!("world");
19335 }
19336 "#
19337 .unindent(),
19338 );
19339
19340 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19341 executor.run_until_parked();
19342
19343 cx.assert_state_with_diff(
19344 r#"
19345 use some::mod1;
19346 use some::mod2;
19347
19348 const A: u32 = 42;
19349 + const B: u32 = 42;
19350 + const C: u32 = 42;
19351 + const D: u32 = 42;
19352 + ˇ
19353
19354 fn main() {
19355 println!("hello");
19356
19357 println!("world");
19358 }
19359 "#
19360 .unindent(),
19361 );
19362
19363 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19364 executor.run_until_parked();
19365
19366 cx.assert_state_with_diff(
19367 r#"
19368 use some::mod1;
19369 use some::mod2;
19370
19371 const A: u32 = 42;
19372 + const B: u32 = 42;
19373 + const C: u32 = 42;
19374 + const D: u32 = 42;
19375 + const E: u32 = 42;
19376 + ˇ
19377
19378 fn main() {
19379 println!("hello");
19380
19381 println!("world");
19382 }
19383 "#
19384 .unindent(),
19385 );
19386
19387 cx.update_editor(|editor, window, cx| {
19388 editor.delete_line(&DeleteLine, window, cx);
19389 });
19390 executor.run_until_parked();
19391
19392 cx.assert_state_with_diff(
19393 r#"
19394 use some::mod1;
19395 use some::mod2;
19396
19397 const A: u32 = 42;
19398 + const B: u32 = 42;
19399 + const C: u32 = 42;
19400 + const D: u32 = 42;
19401 + const E: u32 = 42;
19402 ˇ
19403 fn main() {
19404 println!("hello");
19405
19406 println!("world");
19407 }
19408 "#
19409 .unindent(),
19410 );
19411
19412 cx.update_editor(|editor, window, cx| {
19413 editor.move_up(&MoveUp, window, cx);
19414 editor.delete_line(&DeleteLine, window, cx);
19415 editor.move_up(&MoveUp, window, cx);
19416 editor.delete_line(&DeleteLine, window, cx);
19417 editor.move_up(&MoveUp, window, cx);
19418 editor.delete_line(&DeleteLine, window, cx);
19419 });
19420 executor.run_until_parked();
19421 cx.assert_state_with_diff(
19422 r#"
19423 use some::mod1;
19424 use some::mod2;
19425
19426 const A: u32 = 42;
19427 + const B: u32 = 42;
19428 ˇ
19429 fn main() {
19430 println!("hello");
19431
19432 println!("world");
19433 }
19434 "#
19435 .unindent(),
19436 );
19437
19438 cx.update_editor(|editor, window, cx| {
19439 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19440 editor.delete_line(&DeleteLine, window, cx);
19441 });
19442 executor.run_until_parked();
19443 cx.assert_state_with_diff(
19444 r#"
19445 ˇ
19446 fn main() {
19447 println!("hello");
19448
19449 println!("world");
19450 }
19451 "#
19452 .unindent(),
19453 );
19454}
19455
19456#[gpui::test]
19457async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19458 init_test(cx, |_| {});
19459
19460 let mut cx = EditorTestContext::new(cx).await;
19461 cx.set_head_text(indoc! { "
19462 one
19463 two
19464 three
19465 four
19466 five
19467 "
19468 });
19469 cx.set_state(indoc! { "
19470 one
19471 ˇthree
19472 five
19473 "});
19474 cx.run_until_parked();
19475 cx.update_editor(|editor, window, cx| {
19476 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19477 });
19478 cx.assert_state_with_diff(
19479 indoc! { "
19480 one
19481 - two
19482 ˇthree
19483 - four
19484 five
19485 "}
19486 .to_string(),
19487 );
19488 cx.update_editor(|editor, window, cx| {
19489 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19490 });
19491
19492 cx.assert_state_with_diff(
19493 indoc! { "
19494 one
19495 ˇthree
19496 five
19497 "}
19498 .to_string(),
19499 );
19500
19501 cx.set_state(indoc! { "
19502 one
19503 ˇTWO
19504 three
19505 four
19506 five
19507 "});
19508 cx.run_until_parked();
19509 cx.update_editor(|editor, window, cx| {
19510 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19511 });
19512
19513 cx.assert_state_with_diff(
19514 indoc! { "
19515 one
19516 - two
19517 + ˇTWO
19518 three
19519 four
19520 five
19521 "}
19522 .to_string(),
19523 );
19524 cx.update_editor(|editor, window, cx| {
19525 editor.move_up(&Default::default(), window, cx);
19526 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19527 });
19528 cx.assert_state_with_diff(
19529 indoc! { "
19530 one
19531 ˇTWO
19532 three
19533 four
19534 five
19535 "}
19536 .to_string(),
19537 );
19538}
19539
19540#[gpui::test]
19541async fn test_edits_around_expanded_deletion_hunks(
19542 executor: BackgroundExecutor,
19543 cx: &mut TestAppContext,
19544) {
19545 init_test(cx, |_| {});
19546
19547 let mut cx = EditorTestContext::new(cx).await;
19548
19549 let diff_base = r#"
19550 use some::mod1;
19551 use some::mod2;
19552
19553 const A: u32 = 42;
19554 const B: u32 = 42;
19555 const C: u32 = 42;
19556
19557
19558 fn main() {
19559 println!("hello");
19560
19561 println!("world");
19562 }
19563 "#
19564 .unindent();
19565 executor.run_until_parked();
19566 cx.set_state(
19567 &r#"
19568 use some::mod1;
19569 use some::mod2;
19570
19571 ˇconst B: u32 = 42;
19572 const C: u32 = 42;
19573
19574
19575 fn main() {
19576 println!("hello");
19577
19578 println!("world");
19579 }
19580 "#
19581 .unindent(),
19582 );
19583
19584 cx.set_head_text(&diff_base);
19585 executor.run_until_parked();
19586
19587 cx.update_editor(|editor, window, cx| {
19588 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19589 });
19590 executor.run_until_parked();
19591
19592 cx.assert_state_with_diff(
19593 r#"
19594 use some::mod1;
19595 use some::mod2;
19596
19597 - const A: u32 = 42;
19598 ˇconst B: u32 = 42;
19599 const C: u32 = 42;
19600
19601
19602 fn main() {
19603 println!("hello");
19604
19605 println!("world");
19606 }
19607 "#
19608 .unindent(),
19609 );
19610
19611 cx.update_editor(|editor, window, cx| {
19612 editor.delete_line(&DeleteLine, window, cx);
19613 });
19614 executor.run_until_parked();
19615 cx.assert_state_with_diff(
19616 r#"
19617 use some::mod1;
19618 use some::mod2;
19619
19620 - const A: u32 = 42;
19621 - const B: u32 = 42;
19622 ˇconst C: u32 = 42;
19623
19624
19625 fn main() {
19626 println!("hello");
19627
19628 println!("world");
19629 }
19630 "#
19631 .unindent(),
19632 );
19633
19634 cx.update_editor(|editor, window, cx| {
19635 editor.delete_line(&DeleteLine, window, cx);
19636 });
19637 executor.run_until_parked();
19638 cx.assert_state_with_diff(
19639 r#"
19640 use some::mod1;
19641 use some::mod2;
19642
19643 - const A: u32 = 42;
19644 - const B: u32 = 42;
19645 - const C: u32 = 42;
19646 ˇ
19647
19648 fn main() {
19649 println!("hello");
19650
19651 println!("world");
19652 }
19653 "#
19654 .unindent(),
19655 );
19656
19657 cx.update_editor(|editor, window, cx| {
19658 editor.handle_input("replacement", window, cx);
19659 });
19660 executor.run_until_parked();
19661 cx.assert_state_with_diff(
19662 r#"
19663 use some::mod1;
19664 use some::mod2;
19665
19666 - const A: u32 = 42;
19667 - const B: u32 = 42;
19668 - const C: u32 = 42;
19669 -
19670 + replacementˇ
19671
19672 fn main() {
19673 println!("hello");
19674
19675 println!("world");
19676 }
19677 "#
19678 .unindent(),
19679 );
19680}
19681
19682#[gpui::test]
19683async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19684 init_test(cx, |_| {});
19685
19686 let mut cx = EditorTestContext::new(cx).await;
19687
19688 let base_text = r#"
19689 one
19690 two
19691 three
19692 four
19693 five
19694 "#
19695 .unindent();
19696 executor.run_until_parked();
19697 cx.set_state(
19698 &r#"
19699 one
19700 two
19701 fˇour
19702 five
19703 "#
19704 .unindent(),
19705 );
19706
19707 cx.set_head_text(&base_text);
19708 executor.run_until_parked();
19709
19710 cx.update_editor(|editor, window, cx| {
19711 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19712 });
19713 executor.run_until_parked();
19714
19715 cx.assert_state_with_diff(
19716 r#"
19717 one
19718 two
19719 - three
19720 fˇour
19721 five
19722 "#
19723 .unindent(),
19724 );
19725
19726 cx.update_editor(|editor, window, cx| {
19727 editor.backspace(&Backspace, window, cx);
19728 editor.backspace(&Backspace, window, cx);
19729 });
19730 executor.run_until_parked();
19731 cx.assert_state_with_diff(
19732 r#"
19733 one
19734 two
19735 - threeˇ
19736 - four
19737 + our
19738 five
19739 "#
19740 .unindent(),
19741 );
19742}
19743
19744#[gpui::test]
19745async fn test_edit_after_expanded_modification_hunk(
19746 executor: BackgroundExecutor,
19747 cx: &mut TestAppContext,
19748) {
19749 init_test(cx, |_| {});
19750
19751 let mut cx = EditorTestContext::new(cx).await;
19752
19753 let diff_base = r#"
19754 use some::mod1;
19755 use some::mod2;
19756
19757 const A: u32 = 42;
19758 const B: u32 = 42;
19759 const C: u32 = 42;
19760 const D: u32 = 42;
19761
19762
19763 fn main() {
19764 println!("hello");
19765
19766 println!("world");
19767 }"#
19768 .unindent();
19769
19770 cx.set_state(
19771 &r#"
19772 use some::mod1;
19773 use some::mod2;
19774
19775 const A: u32 = 42;
19776 const B: u32 = 42;
19777 const C: u32 = 43ˇ
19778 const D: u32 = 42;
19779
19780
19781 fn main() {
19782 println!("hello");
19783
19784 println!("world");
19785 }"#
19786 .unindent(),
19787 );
19788
19789 cx.set_head_text(&diff_base);
19790 executor.run_until_parked();
19791 cx.update_editor(|editor, window, cx| {
19792 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19793 });
19794 executor.run_until_parked();
19795
19796 cx.assert_state_with_diff(
19797 r#"
19798 use some::mod1;
19799 use some::mod2;
19800
19801 const A: u32 = 42;
19802 const B: u32 = 42;
19803 - const C: u32 = 42;
19804 + const C: u32 = 43ˇ
19805 const D: u32 = 42;
19806
19807
19808 fn main() {
19809 println!("hello");
19810
19811 println!("world");
19812 }"#
19813 .unindent(),
19814 );
19815
19816 cx.update_editor(|editor, window, cx| {
19817 editor.handle_input("\nnew_line\n", window, cx);
19818 });
19819 executor.run_until_parked();
19820
19821 cx.assert_state_with_diff(
19822 r#"
19823 use some::mod1;
19824 use some::mod2;
19825
19826 const A: u32 = 42;
19827 const B: u32 = 42;
19828 - const C: u32 = 42;
19829 + const C: u32 = 43
19830 + new_line
19831 + ˇ
19832 const D: u32 = 42;
19833
19834
19835 fn main() {
19836 println!("hello");
19837
19838 println!("world");
19839 }"#
19840 .unindent(),
19841 );
19842}
19843
19844#[gpui::test]
19845async fn test_stage_and_unstage_added_file_hunk(
19846 executor: BackgroundExecutor,
19847 cx: &mut TestAppContext,
19848) {
19849 init_test(cx, |_| {});
19850
19851 let mut cx = EditorTestContext::new(cx).await;
19852 cx.update_editor(|editor, _, cx| {
19853 editor.set_expand_all_diff_hunks(cx);
19854 });
19855
19856 let working_copy = r#"
19857 ˇfn main() {
19858 println!("hello, world!");
19859 }
19860 "#
19861 .unindent();
19862
19863 cx.set_state(&working_copy);
19864 executor.run_until_parked();
19865
19866 cx.assert_state_with_diff(
19867 r#"
19868 + ˇfn main() {
19869 + println!("hello, world!");
19870 + }
19871 "#
19872 .unindent(),
19873 );
19874 cx.assert_index_text(None);
19875
19876 cx.update_editor(|editor, window, cx| {
19877 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19878 });
19879 executor.run_until_parked();
19880 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19881 cx.assert_state_with_diff(
19882 r#"
19883 + ˇfn main() {
19884 + println!("hello, world!");
19885 + }
19886 "#
19887 .unindent(),
19888 );
19889
19890 cx.update_editor(|editor, window, cx| {
19891 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19892 });
19893 executor.run_until_parked();
19894 cx.assert_index_text(None);
19895}
19896
19897async fn setup_indent_guides_editor(
19898 text: &str,
19899 cx: &mut TestAppContext,
19900) -> (BufferId, EditorTestContext) {
19901 init_test(cx, |_| {});
19902
19903 let mut cx = EditorTestContext::new(cx).await;
19904
19905 let buffer_id = cx.update_editor(|editor, window, cx| {
19906 editor.set_text(text, window, cx);
19907 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19908
19909 buffer_ids[0]
19910 });
19911
19912 (buffer_id, cx)
19913}
19914
19915fn assert_indent_guides(
19916 range: Range<u32>,
19917 expected: Vec<IndentGuide>,
19918 active_indices: Option<Vec<usize>>,
19919 cx: &mut EditorTestContext,
19920) {
19921 let indent_guides = cx.update_editor(|editor, window, cx| {
19922 let snapshot = editor.snapshot(window, cx).display_snapshot;
19923 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19924 editor,
19925 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19926 true,
19927 &snapshot,
19928 cx,
19929 );
19930
19931 indent_guides.sort_by(|a, b| {
19932 a.depth.cmp(&b.depth).then(
19933 a.start_row
19934 .cmp(&b.start_row)
19935 .then(a.end_row.cmp(&b.end_row)),
19936 )
19937 });
19938 indent_guides
19939 });
19940
19941 if let Some(expected) = active_indices {
19942 let active_indices = cx.update_editor(|editor, window, cx| {
19943 let snapshot = editor.snapshot(window, cx).display_snapshot;
19944 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
19945 });
19946
19947 assert_eq!(
19948 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
19949 expected,
19950 "Active indent guide indices do not match"
19951 );
19952 }
19953
19954 assert_eq!(indent_guides, expected, "Indent guides do not match");
19955}
19956
19957fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
19958 IndentGuide {
19959 buffer_id,
19960 start_row: MultiBufferRow(start_row),
19961 end_row: MultiBufferRow(end_row),
19962 depth,
19963 tab_size: 4,
19964 settings: IndentGuideSettings {
19965 enabled: true,
19966 line_width: 1,
19967 active_line_width: 1,
19968 ..Default::default()
19969 },
19970 }
19971}
19972
19973#[gpui::test]
19974async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
19975 let (buffer_id, mut cx) = setup_indent_guides_editor(
19976 &"
19977 fn main() {
19978 let a = 1;
19979 }"
19980 .unindent(),
19981 cx,
19982 )
19983 .await;
19984
19985 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19986}
19987
19988#[gpui::test]
19989async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
19990 let (buffer_id, mut cx) = setup_indent_guides_editor(
19991 &"
19992 fn main() {
19993 let a = 1;
19994 let b = 2;
19995 }"
19996 .unindent(),
19997 cx,
19998 )
19999 .await;
20000
20001 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20002}
20003
20004#[gpui::test]
20005async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20006 let (buffer_id, mut cx) = setup_indent_guides_editor(
20007 &"
20008 fn main() {
20009 let a = 1;
20010 if a == 3 {
20011 let b = 2;
20012 } else {
20013 let c = 3;
20014 }
20015 }"
20016 .unindent(),
20017 cx,
20018 )
20019 .await;
20020
20021 assert_indent_guides(
20022 0..8,
20023 vec![
20024 indent_guide(buffer_id, 1, 6, 0),
20025 indent_guide(buffer_id, 3, 3, 1),
20026 indent_guide(buffer_id, 5, 5, 1),
20027 ],
20028 None,
20029 &mut cx,
20030 );
20031}
20032
20033#[gpui::test]
20034async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20035 let (buffer_id, mut cx) = setup_indent_guides_editor(
20036 &"
20037 fn main() {
20038 let a = 1;
20039 let b = 2;
20040 let c = 3;
20041 }"
20042 .unindent(),
20043 cx,
20044 )
20045 .await;
20046
20047 assert_indent_guides(
20048 0..5,
20049 vec![
20050 indent_guide(buffer_id, 1, 3, 0),
20051 indent_guide(buffer_id, 2, 2, 1),
20052 ],
20053 None,
20054 &mut cx,
20055 );
20056}
20057
20058#[gpui::test]
20059async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20060 let (buffer_id, mut cx) = setup_indent_guides_editor(
20061 &"
20062 fn main() {
20063 let a = 1;
20064
20065 let c = 3;
20066 }"
20067 .unindent(),
20068 cx,
20069 )
20070 .await;
20071
20072 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20073}
20074
20075#[gpui::test]
20076async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20077 let (buffer_id, mut cx) = setup_indent_guides_editor(
20078 &"
20079 fn main() {
20080 let a = 1;
20081
20082 let c = 3;
20083
20084 if a == 3 {
20085 let b = 2;
20086 } else {
20087 let c = 3;
20088 }
20089 }"
20090 .unindent(),
20091 cx,
20092 )
20093 .await;
20094
20095 assert_indent_guides(
20096 0..11,
20097 vec![
20098 indent_guide(buffer_id, 1, 9, 0),
20099 indent_guide(buffer_id, 6, 6, 1),
20100 indent_guide(buffer_id, 8, 8, 1),
20101 ],
20102 None,
20103 &mut cx,
20104 );
20105}
20106
20107#[gpui::test]
20108async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20109 let (buffer_id, mut cx) = setup_indent_guides_editor(
20110 &"
20111 fn main() {
20112 let a = 1;
20113
20114 let c = 3;
20115
20116 if a == 3 {
20117 let b = 2;
20118 } else {
20119 let c = 3;
20120 }
20121 }"
20122 .unindent(),
20123 cx,
20124 )
20125 .await;
20126
20127 assert_indent_guides(
20128 1..11,
20129 vec![
20130 indent_guide(buffer_id, 1, 9, 0),
20131 indent_guide(buffer_id, 6, 6, 1),
20132 indent_guide(buffer_id, 8, 8, 1),
20133 ],
20134 None,
20135 &mut cx,
20136 );
20137}
20138
20139#[gpui::test]
20140async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20141 let (buffer_id, mut cx) = setup_indent_guides_editor(
20142 &"
20143 fn main() {
20144 let a = 1;
20145
20146 let c = 3;
20147
20148 if a == 3 {
20149 let b = 2;
20150 } else {
20151 let c = 3;
20152 }
20153 }"
20154 .unindent(),
20155 cx,
20156 )
20157 .await;
20158
20159 assert_indent_guides(
20160 1..10,
20161 vec![
20162 indent_guide(buffer_id, 1, 9, 0),
20163 indent_guide(buffer_id, 6, 6, 1),
20164 indent_guide(buffer_id, 8, 8, 1),
20165 ],
20166 None,
20167 &mut cx,
20168 );
20169}
20170
20171#[gpui::test]
20172async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20173 let (buffer_id, mut cx) = setup_indent_guides_editor(
20174 &"
20175 fn main() {
20176 if a {
20177 b(
20178 c,
20179 d,
20180 )
20181 } else {
20182 e(
20183 f
20184 )
20185 }
20186 }"
20187 .unindent(),
20188 cx,
20189 )
20190 .await;
20191
20192 assert_indent_guides(
20193 0..11,
20194 vec![
20195 indent_guide(buffer_id, 1, 10, 0),
20196 indent_guide(buffer_id, 2, 5, 1),
20197 indent_guide(buffer_id, 7, 9, 1),
20198 indent_guide(buffer_id, 3, 4, 2),
20199 indent_guide(buffer_id, 8, 8, 2),
20200 ],
20201 None,
20202 &mut cx,
20203 );
20204
20205 cx.update_editor(|editor, window, cx| {
20206 editor.fold_at(MultiBufferRow(2), window, cx);
20207 assert_eq!(
20208 editor.display_text(cx),
20209 "
20210 fn main() {
20211 if a {
20212 b(⋯
20213 )
20214 } else {
20215 e(
20216 f
20217 )
20218 }
20219 }"
20220 .unindent()
20221 );
20222 });
20223
20224 assert_indent_guides(
20225 0..11,
20226 vec![
20227 indent_guide(buffer_id, 1, 10, 0),
20228 indent_guide(buffer_id, 2, 5, 1),
20229 indent_guide(buffer_id, 7, 9, 1),
20230 indent_guide(buffer_id, 8, 8, 2),
20231 ],
20232 None,
20233 &mut cx,
20234 );
20235}
20236
20237#[gpui::test]
20238async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20239 let (buffer_id, mut cx) = setup_indent_guides_editor(
20240 &"
20241 block1
20242 block2
20243 block3
20244 block4
20245 block2
20246 block1
20247 block1"
20248 .unindent(),
20249 cx,
20250 )
20251 .await;
20252
20253 assert_indent_guides(
20254 1..10,
20255 vec![
20256 indent_guide(buffer_id, 1, 4, 0),
20257 indent_guide(buffer_id, 2, 3, 1),
20258 indent_guide(buffer_id, 3, 3, 2),
20259 ],
20260 None,
20261 &mut cx,
20262 );
20263}
20264
20265#[gpui::test]
20266async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20267 let (buffer_id, mut cx) = setup_indent_guides_editor(
20268 &"
20269 block1
20270 block2
20271 block3
20272
20273 block1
20274 block1"
20275 .unindent(),
20276 cx,
20277 )
20278 .await;
20279
20280 assert_indent_guides(
20281 0..6,
20282 vec![
20283 indent_guide(buffer_id, 1, 2, 0),
20284 indent_guide(buffer_id, 2, 2, 1),
20285 ],
20286 None,
20287 &mut cx,
20288 );
20289}
20290
20291#[gpui::test]
20292async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20293 let (buffer_id, mut cx) = setup_indent_guides_editor(
20294 &"
20295 function component() {
20296 \treturn (
20297 \t\t\t
20298 \t\t<div>
20299 \t\t\t<abc></abc>
20300 \t\t</div>
20301 \t)
20302 }"
20303 .unindent(),
20304 cx,
20305 )
20306 .await;
20307
20308 assert_indent_guides(
20309 0..8,
20310 vec![
20311 indent_guide(buffer_id, 1, 6, 0),
20312 indent_guide(buffer_id, 2, 5, 1),
20313 indent_guide(buffer_id, 4, 4, 2),
20314 ],
20315 None,
20316 &mut cx,
20317 );
20318}
20319
20320#[gpui::test]
20321async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20322 let (buffer_id, mut cx) = setup_indent_guides_editor(
20323 &"
20324 function component() {
20325 \treturn (
20326 \t
20327 \t\t<div>
20328 \t\t\t<abc></abc>
20329 \t\t</div>
20330 \t)
20331 }"
20332 .unindent(),
20333 cx,
20334 )
20335 .await;
20336
20337 assert_indent_guides(
20338 0..8,
20339 vec![
20340 indent_guide(buffer_id, 1, 6, 0),
20341 indent_guide(buffer_id, 2, 5, 1),
20342 indent_guide(buffer_id, 4, 4, 2),
20343 ],
20344 None,
20345 &mut cx,
20346 );
20347}
20348
20349#[gpui::test]
20350async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20351 let (buffer_id, mut cx) = setup_indent_guides_editor(
20352 &"
20353 block1
20354
20355
20356
20357 block2
20358 "
20359 .unindent(),
20360 cx,
20361 )
20362 .await;
20363
20364 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20365}
20366
20367#[gpui::test]
20368async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20369 let (buffer_id, mut cx) = setup_indent_guides_editor(
20370 &"
20371 def a:
20372 \tb = 3
20373 \tif True:
20374 \t\tc = 4
20375 \t\td = 5
20376 \tprint(b)
20377 "
20378 .unindent(),
20379 cx,
20380 )
20381 .await;
20382
20383 assert_indent_guides(
20384 0..6,
20385 vec![
20386 indent_guide(buffer_id, 1, 5, 0),
20387 indent_guide(buffer_id, 3, 4, 1),
20388 ],
20389 None,
20390 &mut cx,
20391 );
20392}
20393
20394#[gpui::test]
20395async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20396 let (buffer_id, mut cx) = setup_indent_guides_editor(
20397 &"
20398 fn main() {
20399 let a = 1;
20400 }"
20401 .unindent(),
20402 cx,
20403 )
20404 .await;
20405
20406 cx.update_editor(|editor, window, cx| {
20407 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20408 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20409 });
20410 });
20411
20412 assert_indent_guides(
20413 0..3,
20414 vec![indent_guide(buffer_id, 1, 1, 0)],
20415 Some(vec![0]),
20416 &mut cx,
20417 );
20418}
20419
20420#[gpui::test]
20421async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20422 let (buffer_id, mut cx) = setup_indent_guides_editor(
20423 &"
20424 fn main() {
20425 if 1 == 2 {
20426 let a = 1;
20427 }
20428 }"
20429 .unindent(),
20430 cx,
20431 )
20432 .await;
20433
20434 cx.update_editor(|editor, window, cx| {
20435 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20436 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20437 });
20438 });
20439
20440 assert_indent_guides(
20441 0..4,
20442 vec![
20443 indent_guide(buffer_id, 1, 3, 0),
20444 indent_guide(buffer_id, 2, 2, 1),
20445 ],
20446 Some(vec![1]),
20447 &mut cx,
20448 );
20449
20450 cx.update_editor(|editor, window, cx| {
20451 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20452 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20453 });
20454 });
20455
20456 assert_indent_guides(
20457 0..4,
20458 vec![
20459 indent_guide(buffer_id, 1, 3, 0),
20460 indent_guide(buffer_id, 2, 2, 1),
20461 ],
20462 Some(vec![1]),
20463 &mut cx,
20464 );
20465
20466 cx.update_editor(|editor, window, cx| {
20467 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20468 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20469 });
20470 });
20471
20472 assert_indent_guides(
20473 0..4,
20474 vec![
20475 indent_guide(buffer_id, 1, 3, 0),
20476 indent_guide(buffer_id, 2, 2, 1),
20477 ],
20478 Some(vec![0]),
20479 &mut cx,
20480 );
20481}
20482
20483#[gpui::test]
20484async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20485 let (buffer_id, mut cx) = setup_indent_guides_editor(
20486 &"
20487 fn main() {
20488 let a = 1;
20489
20490 let b = 2;
20491 }"
20492 .unindent(),
20493 cx,
20494 )
20495 .await;
20496
20497 cx.update_editor(|editor, window, cx| {
20498 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20499 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20500 });
20501 });
20502
20503 assert_indent_guides(
20504 0..5,
20505 vec![indent_guide(buffer_id, 1, 3, 0)],
20506 Some(vec![0]),
20507 &mut cx,
20508 );
20509}
20510
20511#[gpui::test]
20512async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20513 let (buffer_id, mut cx) = setup_indent_guides_editor(
20514 &"
20515 def m:
20516 a = 1
20517 pass"
20518 .unindent(),
20519 cx,
20520 )
20521 .await;
20522
20523 cx.update_editor(|editor, window, cx| {
20524 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20525 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20526 });
20527 });
20528
20529 assert_indent_guides(
20530 0..3,
20531 vec![indent_guide(buffer_id, 1, 2, 0)],
20532 Some(vec![0]),
20533 &mut cx,
20534 );
20535}
20536
20537#[gpui::test]
20538async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20539 init_test(cx, |_| {});
20540 let mut cx = EditorTestContext::new(cx).await;
20541 let text = indoc! {
20542 "
20543 impl A {
20544 fn b() {
20545 0;
20546 3;
20547 5;
20548 6;
20549 7;
20550 }
20551 }
20552 "
20553 };
20554 let base_text = indoc! {
20555 "
20556 impl A {
20557 fn b() {
20558 0;
20559 1;
20560 2;
20561 3;
20562 4;
20563 }
20564 fn c() {
20565 5;
20566 6;
20567 7;
20568 }
20569 }
20570 "
20571 };
20572
20573 cx.update_editor(|editor, window, cx| {
20574 editor.set_text(text, window, cx);
20575
20576 editor.buffer().update(cx, |multibuffer, cx| {
20577 let buffer = multibuffer.as_singleton().unwrap();
20578 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20579
20580 multibuffer.set_all_diff_hunks_expanded(cx);
20581 multibuffer.add_diff(diff, cx);
20582
20583 buffer.read(cx).remote_id()
20584 })
20585 });
20586 cx.run_until_parked();
20587
20588 cx.assert_state_with_diff(
20589 indoc! { "
20590 impl A {
20591 fn b() {
20592 0;
20593 - 1;
20594 - 2;
20595 3;
20596 - 4;
20597 - }
20598 - fn c() {
20599 5;
20600 6;
20601 7;
20602 }
20603 }
20604 ˇ"
20605 }
20606 .to_string(),
20607 );
20608
20609 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20610 editor
20611 .snapshot(window, cx)
20612 .buffer_snapshot
20613 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20614 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20615 .collect::<Vec<_>>()
20616 });
20617 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20618 assert_eq!(
20619 actual_guides,
20620 vec![
20621 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20622 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20623 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20624 ]
20625 );
20626}
20627
20628#[gpui::test]
20629async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20630 init_test(cx, |_| {});
20631 let mut cx = EditorTestContext::new(cx).await;
20632
20633 let diff_base = r#"
20634 a
20635 b
20636 c
20637 "#
20638 .unindent();
20639
20640 cx.set_state(
20641 &r#"
20642 ˇA
20643 b
20644 C
20645 "#
20646 .unindent(),
20647 );
20648 cx.set_head_text(&diff_base);
20649 cx.update_editor(|editor, window, cx| {
20650 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20651 });
20652 executor.run_until_parked();
20653
20654 let both_hunks_expanded = r#"
20655 - a
20656 + ˇA
20657 b
20658 - c
20659 + C
20660 "#
20661 .unindent();
20662
20663 cx.assert_state_with_diff(both_hunks_expanded.clone());
20664
20665 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20666 let snapshot = editor.snapshot(window, cx);
20667 let hunks = editor
20668 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20669 .collect::<Vec<_>>();
20670 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20671 let buffer_id = hunks[0].buffer_id;
20672 hunks
20673 .into_iter()
20674 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20675 .collect::<Vec<_>>()
20676 });
20677 assert_eq!(hunk_ranges.len(), 2);
20678
20679 cx.update_editor(|editor, _, cx| {
20680 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20681 });
20682 executor.run_until_parked();
20683
20684 let second_hunk_expanded = r#"
20685 ˇA
20686 b
20687 - c
20688 + C
20689 "#
20690 .unindent();
20691
20692 cx.assert_state_with_diff(second_hunk_expanded);
20693
20694 cx.update_editor(|editor, _, cx| {
20695 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20696 });
20697 executor.run_until_parked();
20698
20699 cx.assert_state_with_diff(both_hunks_expanded.clone());
20700
20701 cx.update_editor(|editor, _, cx| {
20702 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20703 });
20704 executor.run_until_parked();
20705
20706 let first_hunk_expanded = r#"
20707 - a
20708 + ˇA
20709 b
20710 C
20711 "#
20712 .unindent();
20713
20714 cx.assert_state_with_diff(first_hunk_expanded);
20715
20716 cx.update_editor(|editor, _, cx| {
20717 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20718 });
20719 executor.run_until_parked();
20720
20721 cx.assert_state_with_diff(both_hunks_expanded);
20722
20723 cx.set_state(
20724 &r#"
20725 ˇA
20726 b
20727 "#
20728 .unindent(),
20729 );
20730 cx.run_until_parked();
20731
20732 // TODO this cursor position seems bad
20733 cx.assert_state_with_diff(
20734 r#"
20735 - ˇa
20736 + A
20737 b
20738 "#
20739 .unindent(),
20740 );
20741
20742 cx.update_editor(|editor, window, cx| {
20743 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20744 });
20745
20746 cx.assert_state_with_diff(
20747 r#"
20748 - ˇa
20749 + A
20750 b
20751 - c
20752 "#
20753 .unindent(),
20754 );
20755
20756 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20757 let snapshot = editor.snapshot(window, cx);
20758 let hunks = editor
20759 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20760 .collect::<Vec<_>>();
20761 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20762 let buffer_id = hunks[0].buffer_id;
20763 hunks
20764 .into_iter()
20765 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20766 .collect::<Vec<_>>()
20767 });
20768 assert_eq!(hunk_ranges.len(), 2);
20769
20770 cx.update_editor(|editor, _, cx| {
20771 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20772 });
20773 executor.run_until_parked();
20774
20775 cx.assert_state_with_diff(
20776 r#"
20777 - ˇa
20778 + A
20779 b
20780 "#
20781 .unindent(),
20782 );
20783}
20784
20785#[gpui::test]
20786async fn test_toggle_deletion_hunk_at_start_of_file(
20787 executor: BackgroundExecutor,
20788 cx: &mut TestAppContext,
20789) {
20790 init_test(cx, |_| {});
20791 let mut cx = EditorTestContext::new(cx).await;
20792
20793 let diff_base = r#"
20794 a
20795 b
20796 c
20797 "#
20798 .unindent();
20799
20800 cx.set_state(
20801 &r#"
20802 ˇb
20803 c
20804 "#
20805 .unindent(),
20806 );
20807 cx.set_head_text(&diff_base);
20808 cx.update_editor(|editor, window, cx| {
20809 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20810 });
20811 executor.run_until_parked();
20812
20813 let hunk_expanded = r#"
20814 - a
20815 ˇb
20816 c
20817 "#
20818 .unindent();
20819
20820 cx.assert_state_with_diff(hunk_expanded.clone());
20821
20822 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20823 let snapshot = editor.snapshot(window, cx);
20824 let hunks = editor
20825 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20826 .collect::<Vec<_>>();
20827 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20828 let buffer_id = hunks[0].buffer_id;
20829 hunks
20830 .into_iter()
20831 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20832 .collect::<Vec<_>>()
20833 });
20834 assert_eq!(hunk_ranges.len(), 1);
20835
20836 cx.update_editor(|editor, _, cx| {
20837 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20838 });
20839 executor.run_until_parked();
20840
20841 let hunk_collapsed = r#"
20842 ˇb
20843 c
20844 "#
20845 .unindent();
20846
20847 cx.assert_state_with_diff(hunk_collapsed);
20848
20849 cx.update_editor(|editor, _, cx| {
20850 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20851 });
20852 executor.run_until_parked();
20853
20854 cx.assert_state_with_diff(hunk_expanded);
20855}
20856
20857#[gpui::test]
20858async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20859 init_test(cx, |_| {});
20860
20861 let fs = FakeFs::new(cx.executor());
20862 fs.insert_tree(
20863 path!("/test"),
20864 json!({
20865 ".git": {},
20866 "file-1": "ONE\n",
20867 "file-2": "TWO\n",
20868 "file-3": "THREE\n",
20869 }),
20870 )
20871 .await;
20872
20873 fs.set_head_for_repo(
20874 path!("/test/.git").as_ref(),
20875 &[
20876 ("file-1".into(), "one\n".into()),
20877 ("file-2".into(), "two\n".into()),
20878 ("file-3".into(), "three\n".into()),
20879 ],
20880 "deadbeef",
20881 );
20882
20883 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20884 let mut buffers = vec![];
20885 for i in 1..=3 {
20886 let buffer = project
20887 .update(cx, |project, cx| {
20888 let path = format!(path!("/test/file-{}"), i);
20889 project.open_local_buffer(path, cx)
20890 })
20891 .await
20892 .unwrap();
20893 buffers.push(buffer);
20894 }
20895
20896 let multibuffer = cx.new(|cx| {
20897 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20898 multibuffer.set_all_diff_hunks_expanded(cx);
20899 for buffer in &buffers {
20900 let snapshot = buffer.read(cx).snapshot();
20901 multibuffer.set_excerpts_for_path(
20902 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20903 buffer.clone(),
20904 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20905 2,
20906 cx,
20907 );
20908 }
20909 multibuffer
20910 });
20911
20912 let editor = cx.add_window(|window, cx| {
20913 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20914 });
20915 cx.run_until_parked();
20916
20917 let snapshot = editor
20918 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20919 .unwrap();
20920 let hunks = snapshot
20921 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20922 .map(|hunk| match hunk {
20923 DisplayDiffHunk::Unfolded {
20924 display_row_range, ..
20925 } => display_row_range,
20926 DisplayDiffHunk::Folded { .. } => unreachable!(),
20927 })
20928 .collect::<Vec<_>>();
20929 assert_eq!(
20930 hunks,
20931 [
20932 DisplayRow(2)..DisplayRow(4),
20933 DisplayRow(7)..DisplayRow(9),
20934 DisplayRow(12)..DisplayRow(14),
20935 ]
20936 );
20937}
20938
20939#[gpui::test]
20940async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
20941 init_test(cx, |_| {});
20942
20943 let mut cx = EditorTestContext::new(cx).await;
20944 cx.set_head_text(indoc! { "
20945 one
20946 two
20947 three
20948 four
20949 five
20950 "
20951 });
20952 cx.set_index_text(indoc! { "
20953 one
20954 two
20955 three
20956 four
20957 five
20958 "
20959 });
20960 cx.set_state(indoc! {"
20961 one
20962 TWO
20963 ˇTHREE
20964 FOUR
20965 five
20966 "});
20967 cx.run_until_parked();
20968 cx.update_editor(|editor, window, cx| {
20969 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20970 });
20971 cx.run_until_parked();
20972 cx.assert_index_text(Some(indoc! {"
20973 one
20974 TWO
20975 THREE
20976 FOUR
20977 five
20978 "}));
20979 cx.set_state(indoc! { "
20980 one
20981 TWO
20982 ˇTHREE-HUNDRED
20983 FOUR
20984 five
20985 "});
20986 cx.run_until_parked();
20987 cx.update_editor(|editor, window, cx| {
20988 let snapshot = editor.snapshot(window, cx);
20989 let hunks = editor
20990 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20991 .collect::<Vec<_>>();
20992 assert_eq!(hunks.len(), 1);
20993 assert_eq!(
20994 hunks[0].status(),
20995 DiffHunkStatus {
20996 kind: DiffHunkStatusKind::Modified,
20997 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
20998 }
20999 );
21000
21001 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21002 });
21003 cx.run_until_parked();
21004 cx.assert_index_text(Some(indoc! {"
21005 one
21006 TWO
21007 THREE-HUNDRED
21008 FOUR
21009 five
21010 "}));
21011}
21012
21013#[gpui::test]
21014fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21015 init_test(cx, |_| {});
21016
21017 let editor = cx.add_window(|window, cx| {
21018 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21019 build_editor(buffer, window, cx)
21020 });
21021
21022 let render_args = Arc::new(Mutex::new(None));
21023 let snapshot = editor
21024 .update(cx, |editor, window, cx| {
21025 let snapshot = editor.buffer().read(cx).snapshot(cx);
21026 let range =
21027 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21028
21029 struct RenderArgs {
21030 row: MultiBufferRow,
21031 folded: bool,
21032 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21033 }
21034
21035 let crease = Crease::inline(
21036 range,
21037 FoldPlaceholder::test(),
21038 {
21039 let toggle_callback = render_args.clone();
21040 move |row, folded, callback, _window, _cx| {
21041 *toggle_callback.lock() = Some(RenderArgs {
21042 row,
21043 folded,
21044 callback,
21045 });
21046 div()
21047 }
21048 },
21049 |_row, _folded, _window, _cx| div(),
21050 );
21051
21052 editor.insert_creases(Some(crease), cx);
21053 let snapshot = editor.snapshot(window, cx);
21054 let _div =
21055 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21056 snapshot
21057 })
21058 .unwrap();
21059
21060 let render_args = render_args.lock().take().unwrap();
21061 assert_eq!(render_args.row, MultiBufferRow(1));
21062 assert!(!render_args.folded);
21063 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21064
21065 cx.update_window(*editor, |_, window, cx| {
21066 (render_args.callback)(true, window, cx)
21067 })
21068 .unwrap();
21069 let snapshot = editor
21070 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21071 .unwrap();
21072 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21073
21074 cx.update_window(*editor, |_, window, cx| {
21075 (render_args.callback)(false, window, cx)
21076 })
21077 .unwrap();
21078 let snapshot = editor
21079 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21080 .unwrap();
21081 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21082}
21083
21084#[gpui::test]
21085async fn test_input_text(cx: &mut TestAppContext) {
21086 init_test(cx, |_| {});
21087 let mut cx = EditorTestContext::new(cx).await;
21088
21089 cx.set_state(
21090 &r#"ˇone
21091 two
21092
21093 three
21094 fourˇ
21095 five
21096
21097 siˇx"#
21098 .unindent(),
21099 );
21100
21101 cx.dispatch_action(HandleInput(String::new()));
21102 cx.assert_editor_state(
21103 &r#"ˇone
21104 two
21105
21106 three
21107 fourˇ
21108 five
21109
21110 siˇx"#
21111 .unindent(),
21112 );
21113
21114 cx.dispatch_action(HandleInput("AAAA".to_string()));
21115 cx.assert_editor_state(
21116 &r#"AAAAˇone
21117 two
21118
21119 three
21120 fourAAAAˇ
21121 five
21122
21123 siAAAAˇx"#
21124 .unindent(),
21125 );
21126}
21127
21128#[gpui::test]
21129async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21130 init_test(cx, |_| {});
21131
21132 let mut cx = EditorTestContext::new(cx).await;
21133 cx.set_state(
21134 r#"let foo = 1;
21135let foo = 2;
21136let foo = 3;
21137let fooˇ = 4;
21138let foo = 5;
21139let foo = 6;
21140let foo = 7;
21141let foo = 8;
21142let foo = 9;
21143let foo = 10;
21144let foo = 11;
21145let foo = 12;
21146let foo = 13;
21147let foo = 14;
21148let foo = 15;"#,
21149 );
21150
21151 cx.update_editor(|e, window, cx| {
21152 assert_eq!(
21153 e.next_scroll_position,
21154 NextScrollCursorCenterTopBottom::Center,
21155 "Default next scroll direction is center",
21156 );
21157
21158 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21159 assert_eq!(
21160 e.next_scroll_position,
21161 NextScrollCursorCenterTopBottom::Top,
21162 "After center, next scroll direction should be top",
21163 );
21164
21165 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21166 assert_eq!(
21167 e.next_scroll_position,
21168 NextScrollCursorCenterTopBottom::Bottom,
21169 "After top, next scroll direction should be bottom",
21170 );
21171
21172 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21173 assert_eq!(
21174 e.next_scroll_position,
21175 NextScrollCursorCenterTopBottom::Center,
21176 "After bottom, scrolling should start over",
21177 );
21178
21179 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21180 assert_eq!(
21181 e.next_scroll_position,
21182 NextScrollCursorCenterTopBottom::Top,
21183 "Scrolling continues if retriggered fast enough"
21184 );
21185 });
21186
21187 cx.executor()
21188 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21189 cx.executor().run_until_parked();
21190 cx.update_editor(|e, _, _| {
21191 assert_eq!(
21192 e.next_scroll_position,
21193 NextScrollCursorCenterTopBottom::Center,
21194 "If scrolling is not triggered fast enough, it should reset"
21195 );
21196 });
21197}
21198
21199#[gpui::test]
21200async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21201 init_test(cx, |_| {});
21202 let mut cx = EditorLspTestContext::new_rust(
21203 lsp::ServerCapabilities {
21204 definition_provider: Some(lsp::OneOf::Left(true)),
21205 references_provider: Some(lsp::OneOf::Left(true)),
21206 ..lsp::ServerCapabilities::default()
21207 },
21208 cx,
21209 )
21210 .await;
21211
21212 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21213 let go_to_definition = cx
21214 .lsp
21215 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21216 move |params, _| async move {
21217 if empty_go_to_definition {
21218 Ok(None)
21219 } else {
21220 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21221 uri: params.text_document_position_params.text_document.uri,
21222 range: lsp::Range::new(
21223 lsp::Position::new(4, 3),
21224 lsp::Position::new(4, 6),
21225 ),
21226 })))
21227 }
21228 },
21229 );
21230 let references = cx
21231 .lsp
21232 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21233 Ok(Some(vec![lsp::Location {
21234 uri: params.text_document_position.text_document.uri,
21235 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21236 }]))
21237 });
21238 (go_to_definition, references)
21239 };
21240
21241 cx.set_state(
21242 &r#"fn one() {
21243 let mut a = ˇtwo();
21244 }
21245
21246 fn two() {}"#
21247 .unindent(),
21248 );
21249 set_up_lsp_handlers(false, &mut cx);
21250 let navigated = cx
21251 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21252 .await
21253 .expect("Failed to navigate to definition");
21254 assert_eq!(
21255 navigated,
21256 Navigated::Yes,
21257 "Should have navigated to definition from the GetDefinition response"
21258 );
21259 cx.assert_editor_state(
21260 &r#"fn one() {
21261 let mut a = two();
21262 }
21263
21264 fn «twoˇ»() {}"#
21265 .unindent(),
21266 );
21267
21268 let editors = cx.update_workspace(|workspace, _, cx| {
21269 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21270 });
21271 cx.update_editor(|_, _, test_editor_cx| {
21272 assert_eq!(
21273 editors.len(),
21274 1,
21275 "Initially, only one, test, editor should be open in the workspace"
21276 );
21277 assert_eq!(
21278 test_editor_cx.entity(),
21279 editors.last().expect("Asserted len is 1").clone()
21280 );
21281 });
21282
21283 set_up_lsp_handlers(true, &mut cx);
21284 let navigated = cx
21285 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21286 .await
21287 .expect("Failed to navigate to lookup references");
21288 assert_eq!(
21289 navigated,
21290 Navigated::Yes,
21291 "Should have navigated to references as a fallback after empty GoToDefinition response"
21292 );
21293 // We should not change the selections in the existing file,
21294 // if opening another milti buffer with the references
21295 cx.assert_editor_state(
21296 &r#"fn one() {
21297 let mut a = two();
21298 }
21299
21300 fn «twoˇ»() {}"#
21301 .unindent(),
21302 );
21303 let editors = cx.update_workspace(|workspace, _, cx| {
21304 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21305 });
21306 cx.update_editor(|_, _, test_editor_cx| {
21307 assert_eq!(
21308 editors.len(),
21309 2,
21310 "After falling back to references search, we open a new editor with the results"
21311 );
21312 let references_fallback_text = editors
21313 .into_iter()
21314 .find(|new_editor| *new_editor != test_editor_cx.entity())
21315 .expect("Should have one non-test editor now")
21316 .read(test_editor_cx)
21317 .text(test_editor_cx);
21318 assert_eq!(
21319 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21320 "Should use the range from the references response and not the GoToDefinition one"
21321 );
21322 });
21323}
21324
21325#[gpui::test]
21326async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21327 init_test(cx, |_| {});
21328 cx.update(|cx| {
21329 let mut editor_settings = EditorSettings::get_global(cx).clone();
21330 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21331 EditorSettings::override_global(editor_settings, cx);
21332 });
21333 let mut cx = EditorLspTestContext::new_rust(
21334 lsp::ServerCapabilities {
21335 definition_provider: Some(lsp::OneOf::Left(true)),
21336 references_provider: Some(lsp::OneOf::Left(true)),
21337 ..lsp::ServerCapabilities::default()
21338 },
21339 cx,
21340 )
21341 .await;
21342 let original_state = r#"fn one() {
21343 let mut a = ˇtwo();
21344 }
21345
21346 fn two() {}"#
21347 .unindent();
21348 cx.set_state(&original_state);
21349
21350 let mut go_to_definition = cx
21351 .lsp
21352 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21353 move |_, _| async move { Ok(None) },
21354 );
21355 let _references = cx
21356 .lsp
21357 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21358 panic!("Should not call for references with no go to definition fallback")
21359 });
21360
21361 let navigated = cx
21362 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21363 .await
21364 .expect("Failed to navigate to lookup references");
21365 go_to_definition
21366 .next()
21367 .await
21368 .expect("Should have called the go_to_definition handler");
21369
21370 assert_eq!(
21371 navigated,
21372 Navigated::No,
21373 "Should have navigated to references as a fallback after empty GoToDefinition response"
21374 );
21375 cx.assert_editor_state(&original_state);
21376 let editors = cx.update_workspace(|workspace, _, cx| {
21377 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21378 });
21379 cx.update_editor(|_, _, _| {
21380 assert_eq!(
21381 editors.len(),
21382 1,
21383 "After unsuccessful fallback, no other editor should have been opened"
21384 );
21385 });
21386}
21387
21388#[gpui::test]
21389async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21390 init_test(cx, |_| {});
21391 let mut cx = EditorLspTestContext::new_rust(
21392 lsp::ServerCapabilities {
21393 references_provider: Some(lsp::OneOf::Left(true)),
21394 ..lsp::ServerCapabilities::default()
21395 },
21396 cx,
21397 )
21398 .await;
21399
21400 cx.set_state(
21401 &r#"
21402 fn one() {
21403 let mut a = two();
21404 }
21405
21406 fn ˇtwo() {}"#
21407 .unindent(),
21408 );
21409 cx.lsp
21410 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21411 Ok(Some(vec![
21412 lsp::Location {
21413 uri: params.text_document_position.text_document.uri.clone(),
21414 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21415 },
21416 lsp::Location {
21417 uri: params.text_document_position.text_document.uri,
21418 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21419 },
21420 ]))
21421 });
21422 let navigated = cx
21423 .update_editor(|editor, window, cx| {
21424 editor.find_all_references(&FindAllReferences, window, cx)
21425 })
21426 .unwrap()
21427 .await
21428 .expect("Failed to navigate to references");
21429 assert_eq!(
21430 navigated,
21431 Navigated::Yes,
21432 "Should have navigated to references from the FindAllReferences response"
21433 );
21434 cx.assert_editor_state(
21435 &r#"fn one() {
21436 let mut a = two();
21437 }
21438
21439 fn ˇtwo() {}"#
21440 .unindent(),
21441 );
21442
21443 let editors = cx.update_workspace(|workspace, _, cx| {
21444 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21445 });
21446 cx.update_editor(|_, _, _| {
21447 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21448 });
21449
21450 cx.set_state(
21451 &r#"fn one() {
21452 let mut a = ˇtwo();
21453 }
21454
21455 fn two() {}"#
21456 .unindent(),
21457 );
21458 let navigated = cx
21459 .update_editor(|editor, window, cx| {
21460 editor.find_all_references(&FindAllReferences, window, cx)
21461 })
21462 .unwrap()
21463 .await
21464 .expect("Failed to navigate to references");
21465 assert_eq!(
21466 navigated,
21467 Navigated::Yes,
21468 "Should have navigated to references from the FindAllReferences response"
21469 );
21470 cx.assert_editor_state(
21471 &r#"fn one() {
21472 let mut a = ˇtwo();
21473 }
21474
21475 fn two() {}"#
21476 .unindent(),
21477 );
21478 let editors = cx.update_workspace(|workspace, _, cx| {
21479 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21480 });
21481 cx.update_editor(|_, _, _| {
21482 assert_eq!(
21483 editors.len(),
21484 2,
21485 "should have re-used the previous multibuffer"
21486 );
21487 });
21488
21489 cx.set_state(
21490 &r#"fn one() {
21491 let mut a = ˇtwo();
21492 }
21493 fn three() {}
21494 fn two() {}"#
21495 .unindent(),
21496 );
21497 cx.lsp
21498 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21499 Ok(Some(vec![
21500 lsp::Location {
21501 uri: params.text_document_position.text_document.uri.clone(),
21502 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21503 },
21504 lsp::Location {
21505 uri: params.text_document_position.text_document.uri,
21506 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21507 },
21508 ]))
21509 });
21510 let navigated = cx
21511 .update_editor(|editor, window, cx| {
21512 editor.find_all_references(&FindAllReferences, window, cx)
21513 })
21514 .unwrap()
21515 .await
21516 .expect("Failed to navigate to references");
21517 assert_eq!(
21518 navigated,
21519 Navigated::Yes,
21520 "Should have navigated to references from the FindAllReferences response"
21521 );
21522 cx.assert_editor_state(
21523 &r#"fn one() {
21524 let mut a = ˇtwo();
21525 }
21526 fn three() {}
21527 fn two() {}"#
21528 .unindent(),
21529 );
21530 let editors = cx.update_workspace(|workspace, _, cx| {
21531 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21532 });
21533 cx.update_editor(|_, _, _| {
21534 assert_eq!(
21535 editors.len(),
21536 3,
21537 "should have used a new multibuffer as offsets changed"
21538 );
21539 });
21540}
21541#[gpui::test]
21542async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21543 init_test(cx, |_| {});
21544
21545 let language = Arc::new(Language::new(
21546 LanguageConfig::default(),
21547 Some(tree_sitter_rust::LANGUAGE.into()),
21548 ));
21549
21550 let text = r#"
21551 #[cfg(test)]
21552 mod tests() {
21553 #[test]
21554 fn runnable_1() {
21555 let a = 1;
21556 }
21557
21558 #[test]
21559 fn runnable_2() {
21560 let a = 1;
21561 let b = 2;
21562 }
21563 }
21564 "#
21565 .unindent();
21566
21567 let fs = FakeFs::new(cx.executor());
21568 fs.insert_file("/file.rs", Default::default()).await;
21569
21570 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21571 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21572 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21573 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21574 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21575
21576 let editor = cx.new_window_entity(|window, cx| {
21577 Editor::new(
21578 EditorMode::full(),
21579 multi_buffer,
21580 Some(project.clone()),
21581 window,
21582 cx,
21583 )
21584 });
21585
21586 editor.update_in(cx, |editor, window, cx| {
21587 let snapshot = editor.buffer().read(cx).snapshot(cx);
21588 editor.tasks.insert(
21589 (buffer.read(cx).remote_id(), 3),
21590 RunnableTasks {
21591 templates: vec![],
21592 offset: snapshot.anchor_before(43),
21593 column: 0,
21594 extra_variables: HashMap::default(),
21595 context_range: BufferOffset(43)..BufferOffset(85),
21596 },
21597 );
21598 editor.tasks.insert(
21599 (buffer.read(cx).remote_id(), 8),
21600 RunnableTasks {
21601 templates: vec![],
21602 offset: snapshot.anchor_before(86),
21603 column: 0,
21604 extra_variables: HashMap::default(),
21605 context_range: BufferOffset(86)..BufferOffset(191),
21606 },
21607 );
21608
21609 // Test finding task when cursor is inside function body
21610 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21611 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21612 });
21613 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21614 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21615
21616 // Test finding task when cursor is on function name
21617 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21618 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21619 });
21620 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21621 assert_eq!(row, 8, "Should find task when cursor is on function name");
21622 });
21623}
21624
21625#[gpui::test]
21626async fn test_folding_buffers(cx: &mut TestAppContext) {
21627 init_test(cx, |_| {});
21628
21629 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21630 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21631 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21632
21633 let fs = FakeFs::new(cx.executor());
21634 fs.insert_tree(
21635 path!("/a"),
21636 json!({
21637 "first.rs": sample_text_1,
21638 "second.rs": sample_text_2,
21639 "third.rs": sample_text_3,
21640 }),
21641 )
21642 .await;
21643 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21644 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21645 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21646 let worktree = project.update(cx, |project, cx| {
21647 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21648 assert_eq!(worktrees.len(), 1);
21649 worktrees.pop().unwrap()
21650 });
21651 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21652
21653 let buffer_1 = project
21654 .update(cx, |project, cx| {
21655 project.open_buffer((worktree_id, "first.rs"), cx)
21656 })
21657 .await
21658 .unwrap();
21659 let buffer_2 = project
21660 .update(cx, |project, cx| {
21661 project.open_buffer((worktree_id, "second.rs"), cx)
21662 })
21663 .await
21664 .unwrap();
21665 let buffer_3 = project
21666 .update(cx, |project, cx| {
21667 project.open_buffer((worktree_id, "third.rs"), cx)
21668 })
21669 .await
21670 .unwrap();
21671
21672 let multi_buffer = cx.new(|cx| {
21673 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21674 multi_buffer.push_excerpts(
21675 buffer_1.clone(),
21676 [
21677 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21678 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21679 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21680 ],
21681 cx,
21682 );
21683 multi_buffer.push_excerpts(
21684 buffer_2.clone(),
21685 [
21686 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21687 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21688 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21689 ],
21690 cx,
21691 );
21692 multi_buffer.push_excerpts(
21693 buffer_3.clone(),
21694 [
21695 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21696 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21697 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21698 ],
21699 cx,
21700 );
21701 multi_buffer
21702 });
21703 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21704 Editor::new(
21705 EditorMode::full(),
21706 multi_buffer.clone(),
21707 Some(project.clone()),
21708 window,
21709 cx,
21710 )
21711 });
21712
21713 assert_eq!(
21714 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21715 "\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",
21716 );
21717
21718 multi_buffer_editor.update(cx, |editor, cx| {
21719 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21720 });
21721 assert_eq!(
21722 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21723 "\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",
21724 "After folding the first buffer, its text should not be displayed"
21725 );
21726
21727 multi_buffer_editor.update(cx, |editor, cx| {
21728 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21729 });
21730 assert_eq!(
21731 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21732 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21733 "After folding the second buffer, its text should not be displayed"
21734 );
21735
21736 multi_buffer_editor.update(cx, |editor, cx| {
21737 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21738 });
21739 assert_eq!(
21740 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21741 "\n\n\n\n\n",
21742 "After folding the third buffer, its text should not be displayed"
21743 );
21744
21745 // Emulate selection inside the fold logic, that should work
21746 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21747 editor
21748 .snapshot(window, cx)
21749 .next_line_boundary(Point::new(0, 4));
21750 });
21751
21752 multi_buffer_editor.update(cx, |editor, cx| {
21753 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21754 });
21755 assert_eq!(
21756 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21757 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21758 "After unfolding the second buffer, its text should be displayed"
21759 );
21760
21761 // Typing inside of buffer 1 causes that buffer to be unfolded.
21762 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21763 assert_eq!(
21764 multi_buffer
21765 .read(cx)
21766 .snapshot(cx)
21767 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21768 .collect::<String>(),
21769 "bbbb"
21770 );
21771 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21772 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21773 });
21774 editor.handle_input("B", window, cx);
21775 });
21776
21777 assert_eq!(
21778 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21779 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21780 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21781 );
21782
21783 multi_buffer_editor.update(cx, |editor, cx| {
21784 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21785 });
21786 assert_eq!(
21787 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21788 "\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",
21789 "After unfolding the all buffers, all original text should be displayed"
21790 );
21791}
21792
21793#[gpui::test]
21794async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21795 init_test(cx, |_| {});
21796
21797 let sample_text_1 = "1111\n2222\n3333".to_string();
21798 let sample_text_2 = "4444\n5555\n6666".to_string();
21799 let sample_text_3 = "7777\n8888\n9999".to_string();
21800
21801 let fs = FakeFs::new(cx.executor());
21802 fs.insert_tree(
21803 path!("/a"),
21804 json!({
21805 "first.rs": sample_text_1,
21806 "second.rs": sample_text_2,
21807 "third.rs": sample_text_3,
21808 }),
21809 )
21810 .await;
21811 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21812 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21813 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21814 let worktree = project.update(cx, |project, cx| {
21815 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21816 assert_eq!(worktrees.len(), 1);
21817 worktrees.pop().unwrap()
21818 });
21819 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21820
21821 let buffer_1 = project
21822 .update(cx, |project, cx| {
21823 project.open_buffer((worktree_id, "first.rs"), cx)
21824 })
21825 .await
21826 .unwrap();
21827 let buffer_2 = project
21828 .update(cx, |project, cx| {
21829 project.open_buffer((worktree_id, "second.rs"), cx)
21830 })
21831 .await
21832 .unwrap();
21833 let buffer_3 = project
21834 .update(cx, |project, cx| {
21835 project.open_buffer((worktree_id, "third.rs"), cx)
21836 })
21837 .await
21838 .unwrap();
21839
21840 let multi_buffer = cx.new(|cx| {
21841 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21842 multi_buffer.push_excerpts(
21843 buffer_1.clone(),
21844 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21845 cx,
21846 );
21847 multi_buffer.push_excerpts(
21848 buffer_2.clone(),
21849 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21850 cx,
21851 );
21852 multi_buffer.push_excerpts(
21853 buffer_3.clone(),
21854 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21855 cx,
21856 );
21857 multi_buffer
21858 });
21859
21860 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21861 Editor::new(
21862 EditorMode::full(),
21863 multi_buffer,
21864 Some(project.clone()),
21865 window,
21866 cx,
21867 )
21868 });
21869
21870 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21871 assert_eq!(
21872 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21873 full_text,
21874 );
21875
21876 multi_buffer_editor.update(cx, |editor, cx| {
21877 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21878 });
21879 assert_eq!(
21880 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21881 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21882 "After folding the first buffer, its text should not be displayed"
21883 );
21884
21885 multi_buffer_editor.update(cx, |editor, cx| {
21886 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21887 });
21888
21889 assert_eq!(
21890 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21891 "\n\n\n\n\n\n7777\n8888\n9999",
21892 "After folding the second buffer, its text should not be displayed"
21893 );
21894
21895 multi_buffer_editor.update(cx, |editor, cx| {
21896 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21897 });
21898 assert_eq!(
21899 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21900 "\n\n\n\n\n",
21901 "After folding the third buffer, its text should not be displayed"
21902 );
21903
21904 multi_buffer_editor.update(cx, |editor, cx| {
21905 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21906 });
21907 assert_eq!(
21908 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21909 "\n\n\n\n4444\n5555\n6666\n\n",
21910 "After unfolding the second buffer, its text should be displayed"
21911 );
21912
21913 multi_buffer_editor.update(cx, |editor, cx| {
21914 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21915 });
21916 assert_eq!(
21917 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21918 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21919 "After unfolding the first buffer, its text should be displayed"
21920 );
21921
21922 multi_buffer_editor.update(cx, |editor, cx| {
21923 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21924 });
21925 assert_eq!(
21926 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21927 full_text,
21928 "After unfolding all buffers, all original text should be displayed"
21929 );
21930}
21931
21932#[gpui::test]
21933async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21934 init_test(cx, |_| {});
21935
21936 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21937
21938 let fs = FakeFs::new(cx.executor());
21939 fs.insert_tree(
21940 path!("/a"),
21941 json!({
21942 "main.rs": sample_text,
21943 }),
21944 )
21945 .await;
21946 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21947 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21948 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21949 let worktree = project.update(cx, |project, cx| {
21950 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21951 assert_eq!(worktrees.len(), 1);
21952 worktrees.pop().unwrap()
21953 });
21954 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21955
21956 let buffer_1 = project
21957 .update(cx, |project, cx| {
21958 project.open_buffer((worktree_id, "main.rs"), cx)
21959 })
21960 .await
21961 .unwrap();
21962
21963 let multi_buffer = cx.new(|cx| {
21964 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21965 multi_buffer.push_excerpts(
21966 buffer_1.clone(),
21967 [ExcerptRange::new(
21968 Point::new(0, 0)
21969 ..Point::new(
21970 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
21971 0,
21972 ),
21973 )],
21974 cx,
21975 );
21976 multi_buffer
21977 });
21978 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21979 Editor::new(
21980 EditorMode::full(),
21981 multi_buffer,
21982 Some(project.clone()),
21983 window,
21984 cx,
21985 )
21986 });
21987
21988 let selection_range = Point::new(1, 0)..Point::new(2, 0);
21989 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21990 enum TestHighlight {}
21991 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
21992 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
21993 editor.highlight_text::<TestHighlight>(
21994 vec![highlight_range.clone()],
21995 HighlightStyle::color(Hsla::green()),
21996 cx,
21997 );
21998 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21999 s.select_ranges(Some(highlight_range))
22000 });
22001 });
22002
22003 let full_text = format!("\n\n{sample_text}");
22004 assert_eq!(
22005 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22006 full_text,
22007 );
22008}
22009
22010#[gpui::test]
22011async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22012 init_test(cx, |_| {});
22013 cx.update(|cx| {
22014 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22015 "keymaps/default-linux.json",
22016 cx,
22017 )
22018 .unwrap();
22019 cx.bind_keys(default_key_bindings);
22020 });
22021
22022 let (editor, cx) = cx.add_window_view(|window, cx| {
22023 let multi_buffer = MultiBuffer::build_multi(
22024 [
22025 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22026 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22027 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22028 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22029 ],
22030 cx,
22031 );
22032 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22033
22034 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22035 // fold all but the second buffer, so that we test navigating between two
22036 // adjacent folded buffers, as well as folded buffers at the start and
22037 // end the multibuffer
22038 editor.fold_buffer(buffer_ids[0], cx);
22039 editor.fold_buffer(buffer_ids[2], cx);
22040 editor.fold_buffer(buffer_ids[3], cx);
22041
22042 editor
22043 });
22044 cx.simulate_resize(size(px(1000.), px(1000.)));
22045
22046 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22047 cx.assert_excerpts_with_selections(indoc! {"
22048 [EXCERPT]
22049 ˇ[FOLDED]
22050 [EXCERPT]
22051 a1
22052 b1
22053 [EXCERPT]
22054 [FOLDED]
22055 [EXCERPT]
22056 [FOLDED]
22057 "
22058 });
22059 cx.simulate_keystroke("down");
22060 cx.assert_excerpts_with_selections(indoc! {"
22061 [EXCERPT]
22062 [FOLDED]
22063 [EXCERPT]
22064 ˇa1
22065 b1
22066 [EXCERPT]
22067 [FOLDED]
22068 [EXCERPT]
22069 [FOLDED]
22070 "
22071 });
22072 cx.simulate_keystroke("down");
22073 cx.assert_excerpts_with_selections(indoc! {"
22074 [EXCERPT]
22075 [FOLDED]
22076 [EXCERPT]
22077 a1
22078 ˇb1
22079 [EXCERPT]
22080 [FOLDED]
22081 [EXCERPT]
22082 [FOLDED]
22083 "
22084 });
22085 cx.simulate_keystroke("down");
22086 cx.assert_excerpts_with_selections(indoc! {"
22087 [EXCERPT]
22088 [FOLDED]
22089 [EXCERPT]
22090 a1
22091 b1
22092 ˇ[EXCERPT]
22093 [FOLDED]
22094 [EXCERPT]
22095 [FOLDED]
22096 "
22097 });
22098 cx.simulate_keystroke("down");
22099 cx.assert_excerpts_with_selections(indoc! {"
22100 [EXCERPT]
22101 [FOLDED]
22102 [EXCERPT]
22103 a1
22104 b1
22105 [EXCERPT]
22106 ˇ[FOLDED]
22107 [EXCERPT]
22108 [FOLDED]
22109 "
22110 });
22111 for _ in 0..5 {
22112 cx.simulate_keystroke("down");
22113 cx.assert_excerpts_with_selections(indoc! {"
22114 [EXCERPT]
22115 [FOLDED]
22116 [EXCERPT]
22117 a1
22118 b1
22119 [EXCERPT]
22120 [FOLDED]
22121 [EXCERPT]
22122 ˇ[FOLDED]
22123 "
22124 });
22125 }
22126
22127 cx.simulate_keystroke("up");
22128 cx.assert_excerpts_with_selections(indoc! {"
22129 [EXCERPT]
22130 [FOLDED]
22131 [EXCERPT]
22132 a1
22133 b1
22134 [EXCERPT]
22135 ˇ[FOLDED]
22136 [EXCERPT]
22137 [FOLDED]
22138 "
22139 });
22140 cx.simulate_keystroke("up");
22141 cx.assert_excerpts_with_selections(indoc! {"
22142 [EXCERPT]
22143 [FOLDED]
22144 [EXCERPT]
22145 a1
22146 b1
22147 ˇ[EXCERPT]
22148 [FOLDED]
22149 [EXCERPT]
22150 [FOLDED]
22151 "
22152 });
22153 cx.simulate_keystroke("up");
22154 cx.assert_excerpts_with_selections(indoc! {"
22155 [EXCERPT]
22156 [FOLDED]
22157 [EXCERPT]
22158 a1
22159 ˇb1
22160 [EXCERPT]
22161 [FOLDED]
22162 [EXCERPT]
22163 [FOLDED]
22164 "
22165 });
22166 cx.simulate_keystroke("up");
22167 cx.assert_excerpts_with_selections(indoc! {"
22168 [EXCERPT]
22169 [FOLDED]
22170 [EXCERPT]
22171 ˇa1
22172 b1
22173 [EXCERPT]
22174 [FOLDED]
22175 [EXCERPT]
22176 [FOLDED]
22177 "
22178 });
22179 for _ in 0..5 {
22180 cx.simulate_keystroke("up");
22181 cx.assert_excerpts_with_selections(indoc! {"
22182 [EXCERPT]
22183 ˇ[FOLDED]
22184 [EXCERPT]
22185 a1
22186 b1
22187 [EXCERPT]
22188 [FOLDED]
22189 [EXCERPT]
22190 [FOLDED]
22191 "
22192 });
22193 }
22194}
22195
22196#[gpui::test]
22197async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22198 init_test(cx, |_| {});
22199
22200 // Simple insertion
22201 assert_highlighted_edits(
22202 "Hello, world!",
22203 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22204 true,
22205 cx,
22206 |highlighted_edits, cx| {
22207 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22208 assert_eq!(highlighted_edits.highlights.len(), 1);
22209 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22210 assert_eq!(
22211 highlighted_edits.highlights[0].1.background_color,
22212 Some(cx.theme().status().created_background)
22213 );
22214 },
22215 )
22216 .await;
22217
22218 // Replacement
22219 assert_highlighted_edits(
22220 "This is a test.",
22221 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22222 false,
22223 cx,
22224 |highlighted_edits, cx| {
22225 assert_eq!(highlighted_edits.text, "That is a test.");
22226 assert_eq!(highlighted_edits.highlights.len(), 1);
22227 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22228 assert_eq!(
22229 highlighted_edits.highlights[0].1.background_color,
22230 Some(cx.theme().status().created_background)
22231 );
22232 },
22233 )
22234 .await;
22235
22236 // Multiple edits
22237 assert_highlighted_edits(
22238 "Hello, world!",
22239 vec![
22240 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22241 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22242 ],
22243 false,
22244 cx,
22245 |highlighted_edits, cx| {
22246 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22247 assert_eq!(highlighted_edits.highlights.len(), 2);
22248 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22249 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22250 assert_eq!(
22251 highlighted_edits.highlights[0].1.background_color,
22252 Some(cx.theme().status().created_background)
22253 );
22254 assert_eq!(
22255 highlighted_edits.highlights[1].1.background_color,
22256 Some(cx.theme().status().created_background)
22257 );
22258 },
22259 )
22260 .await;
22261
22262 // Multiple lines with edits
22263 assert_highlighted_edits(
22264 "First line\nSecond line\nThird line\nFourth line",
22265 vec![
22266 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22267 (
22268 Point::new(2, 0)..Point::new(2, 10),
22269 "New third line".to_string(),
22270 ),
22271 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22272 ],
22273 false,
22274 cx,
22275 |highlighted_edits, cx| {
22276 assert_eq!(
22277 highlighted_edits.text,
22278 "Second modified\nNew third line\nFourth updated line"
22279 );
22280 assert_eq!(highlighted_edits.highlights.len(), 3);
22281 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22282 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22283 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22284 for highlight in &highlighted_edits.highlights {
22285 assert_eq!(
22286 highlight.1.background_color,
22287 Some(cx.theme().status().created_background)
22288 );
22289 }
22290 },
22291 )
22292 .await;
22293}
22294
22295#[gpui::test]
22296async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22297 init_test(cx, |_| {});
22298
22299 // Deletion
22300 assert_highlighted_edits(
22301 "Hello, world!",
22302 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22303 true,
22304 cx,
22305 |highlighted_edits, cx| {
22306 assert_eq!(highlighted_edits.text, "Hello, world!");
22307 assert_eq!(highlighted_edits.highlights.len(), 1);
22308 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22309 assert_eq!(
22310 highlighted_edits.highlights[0].1.background_color,
22311 Some(cx.theme().status().deleted_background)
22312 );
22313 },
22314 )
22315 .await;
22316
22317 // Insertion
22318 assert_highlighted_edits(
22319 "Hello, world!",
22320 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22321 true,
22322 cx,
22323 |highlighted_edits, cx| {
22324 assert_eq!(highlighted_edits.highlights.len(), 1);
22325 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22326 assert_eq!(
22327 highlighted_edits.highlights[0].1.background_color,
22328 Some(cx.theme().status().created_background)
22329 );
22330 },
22331 )
22332 .await;
22333}
22334
22335async fn assert_highlighted_edits(
22336 text: &str,
22337 edits: Vec<(Range<Point>, String)>,
22338 include_deletions: bool,
22339 cx: &mut TestAppContext,
22340 assertion_fn: impl Fn(HighlightedText, &App),
22341) {
22342 let window = cx.add_window(|window, cx| {
22343 let buffer = MultiBuffer::build_simple(text, cx);
22344 Editor::new(EditorMode::full(), buffer, None, window, cx)
22345 });
22346 let cx = &mut VisualTestContext::from_window(*window, cx);
22347
22348 let (buffer, snapshot) = window
22349 .update(cx, |editor, _window, cx| {
22350 (
22351 editor.buffer().clone(),
22352 editor.buffer().read(cx).snapshot(cx),
22353 )
22354 })
22355 .unwrap();
22356
22357 let edits = edits
22358 .into_iter()
22359 .map(|(range, edit)| {
22360 (
22361 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22362 edit,
22363 )
22364 })
22365 .collect::<Vec<_>>();
22366
22367 let text_anchor_edits = edits
22368 .clone()
22369 .into_iter()
22370 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22371 .collect::<Vec<_>>();
22372
22373 let edit_preview = window
22374 .update(cx, |_, _window, cx| {
22375 buffer
22376 .read(cx)
22377 .as_singleton()
22378 .unwrap()
22379 .read(cx)
22380 .preview_edits(text_anchor_edits.into(), cx)
22381 })
22382 .unwrap()
22383 .await;
22384
22385 cx.update(|_window, cx| {
22386 let highlighted_edits = edit_prediction_edit_text(
22387 snapshot.as_singleton().unwrap().2,
22388 &edits,
22389 &edit_preview,
22390 include_deletions,
22391 cx,
22392 );
22393 assertion_fn(highlighted_edits, cx)
22394 });
22395}
22396
22397#[track_caller]
22398fn assert_breakpoint(
22399 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22400 path: &Arc<Path>,
22401 expected: Vec<(u32, Breakpoint)>,
22402) {
22403 if expected.is_empty() {
22404 assert!(!breakpoints.contains_key(path), "{}", path.display());
22405 } else {
22406 let mut breakpoint = breakpoints
22407 .get(path)
22408 .unwrap()
22409 .iter()
22410 .map(|breakpoint| {
22411 (
22412 breakpoint.row,
22413 Breakpoint {
22414 message: breakpoint.message.clone(),
22415 state: breakpoint.state,
22416 condition: breakpoint.condition.clone(),
22417 hit_condition: breakpoint.hit_condition.clone(),
22418 },
22419 )
22420 })
22421 .collect::<Vec<_>>();
22422
22423 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22424
22425 assert_eq!(expected, breakpoint);
22426 }
22427}
22428
22429fn add_log_breakpoint_at_cursor(
22430 editor: &mut Editor,
22431 log_message: &str,
22432 window: &mut Window,
22433 cx: &mut Context<Editor>,
22434) {
22435 let (anchor, bp) = editor
22436 .breakpoints_at_cursors(window, cx)
22437 .first()
22438 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22439 .unwrap_or_else(|| {
22440 let cursor_position: Point = editor.selections.newest(cx).head();
22441
22442 let breakpoint_position = editor
22443 .snapshot(window, cx)
22444 .display_snapshot
22445 .buffer_snapshot
22446 .anchor_before(Point::new(cursor_position.row, 0));
22447
22448 (breakpoint_position, Breakpoint::new_log(log_message))
22449 });
22450
22451 editor.edit_breakpoint_at_anchor(
22452 anchor,
22453 bp,
22454 BreakpointEditAction::EditLogMessage(log_message.into()),
22455 cx,
22456 );
22457}
22458
22459#[gpui::test]
22460async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22461 init_test(cx, |_| {});
22462
22463 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22464 let fs = FakeFs::new(cx.executor());
22465 fs.insert_tree(
22466 path!("/a"),
22467 json!({
22468 "main.rs": sample_text,
22469 }),
22470 )
22471 .await;
22472 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22473 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22474 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22475
22476 let fs = FakeFs::new(cx.executor());
22477 fs.insert_tree(
22478 path!("/a"),
22479 json!({
22480 "main.rs": sample_text,
22481 }),
22482 )
22483 .await;
22484 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22485 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22486 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22487 let worktree_id = workspace
22488 .update(cx, |workspace, _window, cx| {
22489 workspace.project().update(cx, |project, cx| {
22490 project.worktrees(cx).next().unwrap().read(cx).id()
22491 })
22492 })
22493 .unwrap();
22494
22495 let buffer = project
22496 .update(cx, |project, cx| {
22497 project.open_buffer((worktree_id, "main.rs"), cx)
22498 })
22499 .await
22500 .unwrap();
22501
22502 let (editor, cx) = cx.add_window_view(|window, cx| {
22503 Editor::new(
22504 EditorMode::full(),
22505 MultiBuffer::build_from_buffer(buffer, cx),
22506 Some(project.clone()),
22507 window,
22508 cx,
22509 )
22510 });
22511
22512 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22513 let abs_path = project.read_with(cx, |project, cx| {
22514 project
22515 .absolute_path(&project_path, cx)
22516 .map(Arc::from)
22517 .unwrap()
22518 });
22519
22520 // assert we can add breakpoint on the first line
22521 editor.update_in(cx, |editor, window, cx| {
22522 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22523 editor.move_to_end(&MoveToEnd, window, cx);
22524 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22525 });
22526
22527 let breakpoints = editor.update(cx, |editor, cx| {
22528 editor
22529 .breakpoint_store()
22530 .as_ref()
22531 .unwrap()
22532 .read(cx)
22533 .all_source_breakpoints(cx)
22534 });
22535
22536 assert_eq!(1, breakpoints.len());
22537 assert_breakpoint(
22538 &breakpoints,
22539 &abs_path,
22540 vec![
22541 (0, Breakpoint::new_standard()),
22542 (3, Breakpoint::new_standard()),
22543 ],
22544 );
22545
22546 editor.update_in(cx, |editor, window, cx| {
22547 editor.move_to_beginning(&MoveToBeginning, window, cx);
22548 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22549 });
22550
22551 let breakpoints = editor.update(cx, |editor, cx| {
22552 editor
22553 .breakpoint_store()
22554 .as_ref()
22555 .unwrap()
22556 .read(cx)
22557 .all_source_breakpoints(cx)
22558 });
22559
22560 assert_eq!(1, breakpoints.len());
22561 assert_breakpoint(
22562 &breakpoints,
22563 &abs_path,
22564 vec![(3, Breakpoint::new_standard())],
22565 );
22566
22567 editor.update_in(cx, |editor, window, cx| {
22568 editor.move_to_end(&MoveToEnd, window, cx);
22569 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22570 });
22571
22572 let breakpoints = editor.update(cx, |editor, cx| {
22573 editor
22574 .breakpoint_store()
22575 .as_ref()
22576 .unwrap()
22577 .read(cx)
22578 .all_source_breakpoints(cx)
22579 });
22580
22581 assert_eq!(0, breakpoints.len());
22582 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22583}
22584
22585#[gpui::test]
22586async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22587 init_test(cx, |_| {});
22588
22589 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22590
22591 let fs = FakeFs::new(cx.executor());
22592 fs.insert_tree(
22593 path!("/a"),
22594 json!({
22595 "main.rs": sample_text,
22596 }),
22597 )
22598 .await;
22599 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22600 let (workspace, cx) =
22601 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22602
22603 let worktree_id = workspace.update(cx, |workspace, cx| {
22604 workspace.project().update(cx, |project, cx| {
22605 project.worktrees(cx).next().unwrap().read(cx).id()
22606 })
22607 });
22608
22609 let buffer = project
22610 .update(cx, |project, cx| {
22611 project.open_buffer((worktree_id, "main.rs"), cx)
22612 })
22613 .await
22614 .unwrap();
22615
22616 let (editor, cx) = cx.add_window_view(|window, cx| {
22617 Editor::new(
22618 EditorMode::full(),
22619 MultiBuffer::build_from_buffer(buffer, cx),
22620 Some(project.clone()),
22621 window,
22622 cx,
22623 )
22624 });
22625
22626 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22627 let abs_path = project.read_with(cx, |project, cx| {
22628 project
22629 .absolute_path(&project_path, cx)
22630 .map(Arc::from)
22631 .unwrap()
22632 });
22633
22634 editor.update_in(cx, |editor, window, cx| {
22635 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22636 });
22637
22638 let breakpoints = editor.update(cx, |editor, cx| {
22639 editor
22640 .breakpoint_store()
22641 .as_ref()
22642 .unwrap()
22643 .read(cx)
22644 .all_source_breakpoints(cx)
22645 });
22646
22647 assert_breakpoint(
22648 &breakpoints,
22649 &abs_path,
22650 vec![(0, Breakpoint::new_log("hello world"))],
22651 );
22652
22653 // Removing a log message from a log breakpoint should remove it
22654 editor.update_in(cx, |editor, window, cx| {
22655 add_log_breakpoint_at_cursor(editor, "", window, cx);
22656 });
22657
22658 let breakpoints = editor.update(cx, |editor, cx| {
22659 editor
22660 .breakpoint_store()
22661 .as_ref()
22662 .unwrap()
22663 .read(cx)
22664 .all_source_breakpoints(cx)
22665 });
22666
22667 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22668
22669 editor.update_in(cx, |editor, window, cx| {
22670 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22671 editor.move_to_end(&MoveToEnd, window, cx);
22672 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22673 // Not adding a log message to a standard breakpoint shouldn't remove it
22674 add_log_breakpoint_at_cursor(editor, "", window, cx);
22675 });
22676
22677 let breakpoints = editor.update(cx, |editor, cx| {
22678 editor
22679 .breakpoint_store()
22680 .as_ref()
22681 .unwrap()
22682 .read(cx)
22683 .all_source_breakpoints(cx)
22684 });
22685
22686 assert_breakpoint(
22687 &breakpoints,
22688 &abs_path,
22689 vec![
22690 (0, Breakpoint::new_standard()),
22691 (3, Breakpoint::new_standard()),
22692 ],
22693 );
22694
22695 editor.update_in(cx, |editor, window, cx| {
22696 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22697 });
22698
22699 let breakpoints = editor.update(cx, |editor, cx| {
22700 editor
22701 .breakpoint_store()
22702 .as_ref()
22703 .unwrap()
22704 .read(cx)
22705 .all_source_breakpoints(cx)
22706 });
22707
22708 assert_breakpoint(
22709 &breakpoints,
22710 &abs_path,
22711 vec![
22712 (0, Breakpoint::new_standard()),
22713 (3, Breakpoint::new_log("hello world")),
22714 ],
22715 );
22716
22717 editor.update_in(cx, |editor, window, cx| {
22718 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22719 });
22720
22721 let breakpoints = editor.update(cx, |editor, cx| {
22722 editor
22723 .breakpoint_store()
22724 .as_ref()
22725 .unwrap()
22726 .read(cx)
22727 .all_source_breakpoints(cx)
22728 });
22729
22730 assert_breakpoint(
22731 &breakpoints,
22732 &abs_path,
22733 vec![
22734 (0, Breakpoint::new_standard()),
22735 (3, Breakpoint::new_log("hello Earth!!")),
22736 ],
22737 );
22738}
22739
22740/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22741/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22742/// or when breakpoints were placed out of order. This tests for a regression too
22743#[gpui::test]
22744async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22745 init_test(cx, |_| {});
22746
22747 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22748 let fs = FakeFs::new(cx.executor());
22749 fs.insert_tree(
22750 path!("/a"),
22751 json!({
22752 "main.rs": sample_text,
22753 }),
22754 )
22755 .await;
22756 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22757 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22758 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22759
22760 let fs = FakeFs::new(cx.executor());
22761 fs.insert_tree(
22762 path!("/a"),
22763 json!({
22764 "main.rs": sample_text,
22765 }),
22766 )
22767 .await;
22768 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22769 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22770 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22771 let worktree_id = workspace
22772 .update(cx, |workspace, _window, cx| {
22773 workspace.project().update(cx, |project, cx| {
22774 project.worktrees(cx).next().unwrap().read(cx).id()
22775 })
22776 })
22777 .unwrap();
22778
22779 let buffer = project
22780 .update(cx, |project, cx| {
22781 project.open_buffer((worktree_id, "main.rs"), cx)
22782 })
22783 .await
22784 .unwrap();
22785
22786 let (editor, cx) = cx.add_window_view(|window, cx| {
22787 Editor::new(
22788 EditorMode::full(),
22789 MultiBuffer::build_from_buffer(buffer, cx),
22790 Some(project.clone()),
22791 window,
22792 cx,
22793 )
22794 });
22795
22796 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22797 let abs_path = project.read_with(cx, |project, cx| {
22798 project
22799 .absolute_path(&project_path, cx)
22800 .map(Arc::from)
22801 .unwrap()
22802 });
22803
22804 // assert we can add breakpoint on the first line
22805 editor.update_in(cx, |editor, window, cx| {
22806 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22807 editor.move_to_end(&MoveToEnd, window, cx);
22808 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22809 editor.move_up(&MoveUp, window, cx);
22810 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22811 });
22812
22813 let breakpoints = editor.update(cx, |editor, cx| {
22814 editor
22815 .breakpoint_store()
22816 .as_ref()
22817 .unwrap()
22818 .read(cx)
22819 .all_source_breakpoints(cx)
22820 });
22821
22822 assert_eq!(1, breakpoints.len());
22823 assert_breakpoint(
22824 &breakpoints,
22825 &abs_path,
22826 vec![
22827 (0, Breakpoint::new_standard()),
22828 (2, Breakpoint::new_standard()),
22829 (3, Breakpoint::new_standard()),
22830 ],
22831 );
22832
22833 editor.update_in(cx, |editor, window, cx| {
22834 editor.move_to_beginning(&MoveToBeginning, window, cx);
22835 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22836 editor.move_to_end(&MoveToEnd, window, cx);
22837 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22838 // Disabling a breakpoint that doesn't exist should do nothing
22839 editor.move_up(&MoveUp, window, cx);
22840 editor.move_up(&MoveUp, window, cx);
22841 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22842 });
22843
22844 let breakpoints = editor.update(cx, |editor, cx| {
22845 editor
22846 .breakpoint_store()
22847 .as_ref()
22848 .unwrap()
22849 .read(cx)
22850 .all_source_breakpoints(cx)
22851 });
22852
22853 let disable_breakpoint = {
22854 let mut bp = Breakpoint::new_standard();
22855 bp.state = BreakpointState::Disabled;
22856 bp
22857 };
22858
22859 assert_eq!(1, breakpoints.len());
22860 assert_breakpoint(
22861 &breakpoints,
22862 &abs_path,
22863 vec![
22864 (0, disable_breakpoint.clone()),
22865 (2, Breakpoint::new_standard()),
22866 (3, disable_breakpoint.clone()),
22867 ],
22868 );
22869
22870 editor.update_in(cx, |editor, window, cx| {
22871 editor.move_to_beginning(&MoveToBeginning, window, cx);
22872 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22873 editor.move_to_end(&MoveToEnd, window, cx);
22874 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22875 editor.move_up(&MoveUp, window, cx);
22876 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22877 });
22878
22879 let breakpoints = editor.update(cx, |editor, cx| {
22880 editor
22881 .breakpoint_store()
22882 .as_ref()
22883 .unwrap()
22884 .read(cx)
22885 .all_source_breakpoints(cx)
22886 });
22887
22888 assert_eq!(1, breakpoints.len());
22889 assert_breakpoint(
22890 &breakpoints,
22891 &abs_path,
22892 vec![
22893 (0, Breakpoint::new_standard()),
22894 (2, disable_breakpoint),
22895 (3, Breakpoint::new_standard()),
22896 ],
22897 );
22898}
22899
22900#[gpui::test]
22901async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22902 init_test(cx, |_| {});
22903 let capabilities = lsp::ServerCapabilities {
22904 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22905 prepare_provider: Some(true),
22906 work_done_progress_options: Default::default(),
22907 })),
22908 ..Default::default()
22909 };
22910 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22911
22912 cx.set_state(indoc! {"
22913 struct Fˇoo {}
22914 "});
22915
22916 cx.update_editor(|editor, _, cx| {
22917 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22918 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22919 editor.highlight_background::<DocumentHighlightRead>(
22920 &[highlight_range],
22921 |theme| theme.colors().editor_document_highlight_read_background,
22922 cx,
22923 );
22924 });
22925
22926 let mut prepare_rename_handler = cx
22927 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22928 move |_, _, _| async move {
22929 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22930 start: lsp::Position {
22931 line: 0,
22932 character: 7,
22933 },
22934 end: lsp::Position {
22935 line: 0,
22936 character: 10,
22937 },
22938 })))
22939 },
22940 );
22941 let prepare_rename_task = cx
22942 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22943 .expect("Prepare rename was not started");
22944 prepare_rename_handler.next().await.unwrap();
22945 prepare_rename_task.await.expect("Prepare rename failed");
22946
22947 let mut rename_handler =
22948 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22949 let edit = lsp::TextEdit {
22950 range: lsp::Range {
22951 start: lsp::Position {
22952 line: 0,
22953 character: 7,
22954 },
22955 end: lsp::Position {
22956 line: 0,
22957 character: 10,
22958 },
22959 },
22960 new_text: "FooRenamed".to_string(),
22961 };
22962 Ok(Some(lsp::WorkspaceEdit::new(
22963 // Specify the same edit twice
22964 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
22965 )))
22966 });
22967 let rename_task = cx
22968 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22969 .expect("Confirm rename was not started");
22970 rename_handler.next().await.unwrap();
22971 rename_task.await.expect("Confirm rename failed");
22972 cx.run_until_parked();
22973
22974 // Despite two edits, only one is actually applied as those are identical
22975 cx.assert_editor_state(indoc! {"
22976 struct FooRenamedˇ {}
22977 "});
22978}
22979
22980#[gpui::test]
22981async fn test_rename_without_prepare(cx: &mut TestAppContext) {
22982 init_test(cx, |_| {});
22983 // These capabilities indicate that the server does not support prepare rename.
22984 let capabilities = lsp::ServerCapabilities {
22985 rename_provider: Some(lsp::OneOf::Left(true)),
22986 ..Default::default()
22987 };
22988 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22989
22990 cx.set_state(indoc! {"
22991 struct Fˇoo {}
22992 "});
22993
22994 cx.update_editor(|editor, _window, cx| {
22995 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22996 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22997 editor.highlight_background::<DocumentHighlightRead>(
22998 &[highlight_range],
22999 |theme| theme.colors().editor_document_highlight_read_background,
23000 cx,
23001 );
23002 });
23003
23004 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23005 .expect("Prepare rename was not started")
23006 .await
23007 .expect("Prepare rename failed");
23008
23009 let mut rename_handler =
23010 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23011 let edit = lsp::TextEdit {
23012 range: lsp::Range {
23013 start: lsp::Position {
23014 line: 0,
23015 character: 7,
23016 },
23017 end: lsp::Position {
23018 line: 0,
23019 character: 10,
23020 },
23021 },
23022 new_text: "FooRenamed".to_string(),
23023 };
23024 Ok(Some(lsp::WorkspaceEdit::new(
23025 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23026 )))
23027 });
23028 let rename_task = cx
23029 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23030 .expect("Confirm rename was not started");
23031 rename_handler.next().await.unwrap();
23032 rename_task.await.expect("Confirm rename failed");
23033 cx.run_until_parked();
23034
23035 // Correct range is renamed, as `surrounding_word` is used to find it.
23036 cx.assert_editor_state(indoc! {"
23037 struct FooRenamedˇ {}
23038 "});
23039}
23040
23041#[gpui::test]
23042async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23043 init_test(cx, |_| {});
23044 let mut cx = EditorTestContext::new(cx).await;
23045
23046 let language = Arc::new(
23047 Language::new(
23048 LanguageConfig::default(),
23049 Some(tree_sitter_html::LANGUAGE.into()),
23050 )
23051 .with_brackets_query(
23052 r#"
23053 ("<" @open "/>" @close)
23054 ("</" @open ">" @close)
23055 ("<" @open ">" @close)
23056 ("\"" @open "\"" @close)
23057 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23058 "#,
23059 )
23060 .unwrap(),
23061 );
23062 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23063
23064 cx.set_state(indoc! {"
23065 <span>ˇ</span>
23066 "});
23067 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23068 cx.assert_editor_state(indoc! {"
23069 <span>
23070 ˇ
23071 </span>
23072 "});
23073
23074 cx.set_state(indoc! {"
23075 <span><span></span>ˇ</span>
23076 "});
23077 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23078 cx.assert_editor_state(indoc! {"
23079 <span><span></span>
23080 ˇ</span>
23081 "});
23082
23083 cx.set_state(indoc! {"
23084 <span>ˇ
23085 </span>
23086 "});
23087 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23088 cx.assert_editor_state(indoc! {"
23089 <span>
23090 ˇ
23091 </span>
23092 "});
23093}
23094
23095#[gpui::test(iterations = 10)]
23096async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23097 init_test(cx, |_| {});
23098
23099 let fs = FakeFs::new(cx.executor());
23100 fs.insert_tree(
23101 path!("/dir"),
23102 json!({
23103 "a.ts": "a",
23104 }),
23105 )
23106 .await;
23107
23108 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23109 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23110 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23111
23112 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23113 language_registry.add(Arc::new(Language::new(
23114 LanguageConfig {
23115 name: "TypeScript".into(),
23116 matcher: LanguageMatcher {
23117 path_suffixes: vec!["ts".to_string()],
23118 ..Default::default()
23119 },
23120 ..Default::default()
23121 },
23122 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23123 )));
23124 let mut fake_language_servers = language_registry.register_fake_lsp(
23125 "TypeScript",
23126 FakeLspAdapter {
23127 capabilities: lsp::ServerCapabilities {
23128 code_lens_provider: Some(lsp::CodeLensOptions {
23129 resolve_provider: Some(true),
23130 }),
23131 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23132 commands: vec!["_the/command".to_string()],
23133 ..lsp::ExecuteCommandOptions::default()
23134 }),
23135 ..lsp::ServerCapabilities::default()
23136 },
23137 ..FakeLspAdapter::default()
23138 },
23139 );
23140
23141 let editor = workspace
23142 .update(cx, |workspace, window, cx| {
23143 workspace.open_abs_path(
23144 PathBuf::from(path!("/dir/a.ts")),
23145 OpenOptions::default(),
23146 window,
23147 cx,
23148 )
23149 })
23150 .unwrap()
23151 .await
23152 .unwrap()
23153 .downcast::<Editor>()
23154 .unwrap();
23155 cx.executor().run_until_parked();
23156
23157 let fake_server = fake_language_servers.next().await.unwrap();
23158
23159 let buffer = editor.update(cx, |editor, cx| {
23160 editor
23161 .buffer()
23162 .read(cx)
23163 .as_singleton()
23164 .expect("have opened a single file by path")
23165 });
23166
23167 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23168 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23169 drop(buffer_snapshot);
23170 let actions = cx
23171 .update_window(*workspace, |_, window, cx| {
23172 project.code_actions(&buffer, anchor..anchor, window, cx)
23173 })
23174 .unwrap();
23175
23176 fake_server
23177 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23178 Ok(Some(vec![
23179 lsp::CodeLens {
23180 range: lsp::Range::default(),
23181 command: Some(lsp::Command {
23182 title: "Code lens command".to_owned(),
23183 command: "_the/command".to_owned(),
23184 arguments: None,
23185 }),
23186 data: None,
23187 },
23188 lsp::CodeLens {
23189 range: lsp::Range::default(),
23190 command: Some(lsp::Command {
23191 title: "Command not in capabilities".to_owned(),
23192 command: "not in capabilities".to_owned(),
23193 arguments: None,
23194 }),
23195 data: None,
23196 },
23197 lsp::CodeLens {
23198 range: lsp::Range {
23199 start: lsp::Position {
23200 line: 1,
23201 character: 1,
23202 },
23203 end: lsp::Position {
23204 line: 1,
23205 character: 1,
23206 },
23207 },
23208 command: Some(lsp::Command {
23209 title: "Command not in range".to_owned(),
23210 command: "_the/command".to_owned(),
23211 arguments: None,
23212 }),
23213 data: None,
23214 },
23215 ]))
23216 })
23217 .next()
23218 .await;
23219
23220 let actions = actions.await.unwrap();
23221 assert_eq!(
23222 actions.len(),
23223 1,
23224 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23225 );
23226 let action = actions[0].clone();
23227 let apply = project.update(cx, |project, cx| {
23228 project.apply_code_action(buffer.clone(), action, true, cx)
23229 });
23230
23231 // Resolving the code action does not populate its edits. In absence of
23232 // edits, we must execute the given command.
23233 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23234 |mut lens, _| async move {
23235 let lens_command = lens.command.as_mut().expect("should have a command");
23236 assert_eq!(lens_command.title, "Code lens command");
23237 lens_command.arguments = Some(vec![json!("the-argument")]);
23238 Ok(lens)
23239 },
23240 );
23241
23242 // While executing the command, the language server sends the editor
23243 // a `workspaceEdit` request.
23244 fake_server
23245 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23246 let fake = fake_server.clone();
23247 move |params, _| {
23248 assert_eq!(params.command, "_the/command");
23249 let fake = fake.clone();
23250 async move {
23251 fake.server
23252 .request::<lsp::request::ApplyWorkspaceEdit>(
23253 lsp::ApplyWorkspaceEditParams {
23254 label: None,
23255 edit: lsp::WorkspaceEdit {
23256 changes: Some(
23257 [(
23258 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23259 vec![lsp::TextEdit {
23260 range: lsp::Range::new(
23261 lsp::Position::new(0, 0),
23262 lsp::Position::new(0, 0),
23263 ),
23264 new_text: "X".into(),
23265 }],
23266 )]
23267 .into_iter()
23268 .collect(),
23269 ),
23270 ..lsp::WorkspaceEdit::default()
23271 },
23272 },
23273 )
23274 .await
23275 .into_response()
23276 .unwrap();
23277 Ok(Some(json!(null)))
23278 }
23279 }
23280 })
23281 .next()
23282 .await;
23283
23284 // Applying the code lens command returns a project transaction containing the edits
23285 // sent by the language server in its `workspaceEdit` request.
23286 let transaction = apply.await.unwrap();
23287 assert!(transaction.0.contains_key(&buffer));
23288 buffer.update(cx, |buffer, cx| {
23289 assert_eq!(buffer.text(), "Xa");
23290 buffer.undo(cx);
23291 assert_eq!(buffer.text(), "a");
23292 });
23293
23294 let actions_after_edits = cx
23295 .update_window(*workspace, |_, window, cx| {
23296 project.code_actions(&buffer, anchor..anchor, window, cx)
23297 })
23298 .unwrap()
23299 .await
23300 .unwrap();
23301 assert_eq!(
23302 actions, actions_after_edits,
23303 "For the same selection, same code lens actions should be returned"
23304 );
23305
23306 let _responses =
23307 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23308 panic!("No more code lens requests are expected");
23309 });
23310 editor.update_in(cx, |editor, window, cx| {
23311 editor.select_all(&SelectAll, window, cx);
23312 });
23313 cx.executor().run_until_parked();
23314 let new_actions = cx
23315 .update_window(*workspace, |_, window, cx| {
23316 project.code_actions(&buffer, anchor..anchor, window, cx)
23317 })
23318 .unwrap()
23319 .await
23320 .unwrap();
23321 assert_eq!(
23322 actions, new_actions,
23323 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23324 );
23325}
23326
23327#[gpui::test]
23328async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23329 init_test(cx, |_| {});
23330
23331 let fs = FakeFs::new(cx.executor());
23332 let main_text = r#"fn main() {
23333println!("1");
23334println!("2");
23335println!("3");
23336println!("4");
23337println!("5");
23338}"#;
23339 let lib_text = "mod foo {}";
23340 fs.insert_tree(
23341 path!("/a"),
23342 json!({
23343 "lib.rs": lib_text,
23344 "main.rs": main_text,
23345 }),
23346 )
23347 .await;
23348
23349 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23350 let (workspace, cx) =
23351 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23352 let worktree_id = workspace.update(cx, |workspace, cx| {
23353 workspace.project().update(cx, |project, cx| {
23354 project.worktrees(cx).next().unwrap().read(cx).id()
23355 })
23356 });
23357
23358 let expected_ranges = vec![
23359 Point::new(0, 0)..Point::new(0, 0),
23360 Point::new(1, 0)..Point::new(1, 1),
23361 Point::new(2, 0)..Point::new(2, 2),
23362 Point::new(3, 0)..Point::new(3, 3),
23363 ];
23364
23365 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23366 let editor_1 = workspace
23367 .update_in(cx, |workspace, window, cx| {
23368 workspace.open_path(
23369 (worktree_id, "main.rs"),
23370 Some(pane_1.downgrade()),
23371 true,
23372 window,
23373 cx,
23374 )
23375 })
23376 .unwrap()
23377 .await
23378 .downcast::<Editor>()
23379 .unwrap();
23380 pane_1.update(cx, |pane, cx| {
23381 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23382 open_editor.update(cx, |editor, cx| {
23383 assert_eq!(
23384 editor.display_text(cx),
23385 main_text,
23386 "Original main.rs text on initial open",
23387 );
23388 assert_eq!(
23389 editor
23390 .selections
23391 .all::<Point>(cx)
23392 .into_iter()
23393 .map(|s| s.range())
23394 .collect::<Vec<_>>(),
23395 vec![Point::zero()..Point::zero()],
23396 "Default selections on initial open",
23397 );
23398 })
23399 });
23400 editor_1.update_in(cx, |editor, window, cx| {
23401 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23402 s.select_ranges(expected_ranges.clone());
23403 });
23404 });
23405
23406 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23407 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23408 });
23409 let editor_2 = workspace
23410 .update_in(cx, |workspace, window, cx| {
23411 workspace.open_path(
23412 (worktree_id, "main.rs"),
23413 Some(pane_2.downgrade()),
23414 true,
23415 window,
23416 cx,
23417 )
23418 })
23419 .unwrap()
23420 .await
23421 .downcast::<Editor>()
23422 .unwrap();
23423 pane_2.update(cx, |pane, cx| {
23424 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23425 open_editor.update(cx, |editor, cx| {
23426 assert_eq!(
23427 editor.display_text(cx),
23428 main_text,
23429 "Original main.rs text on initial open in another panel",
23430 );
23431 assert_eq!(
23432 editor
23433 .selections
23434 .all::<Point>(cx)
23435 .into_iter()
23436 .map(|s| s.range())
23437 .collect::<Vec<_>>(),
23438 vec![Point::zero()..Point::zero()],
23439 "Default selections on initial open in another panel",
23440 );
23441 })
23442 });
23443
23444 editor_2.update_in(cx, |editor, window, cx| {
23445 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23446 });
23447
23448 let _other_editor_1 = workspace
23449 .update_in(cx, |workspace, window, cx| {
23450 workspace.open_path(
23451 (worktree_id, "lib.rs"),
23452 Some(pane_1.downgrade()),
23453 true,
23454 window,
23455 cx,
23456 )
23457 })
23458 .unwrap()
23459 .await
23460 .downcast::<Editor>()
23461 .unwrap();
23462 pane_1
23463 .update_in(cx, |pane, window, cx| {
23464 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23465 })
23466 .await
23467 .unwrap();
23468 drop(editor_1);
23469 pane_1.update(cx, |pane, cx| {
23470 pane.active_item()
23471 .unwrap()
23472 .downcast::<Editor>()
23473 .unwrap()
23474 .update(cx, |editor, cx| {
23475 assert_eq!(
23476 editor.display_text(cx),
23477 lib_text,
23478 "Other file should be open and active",
23479 );
23480 });
23481 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23482 });
23483
23484 let _other_editor_2 = workspace
23485 .update_in(cx, |workspace, window, cx| {
23486 workspace.open_path(
23487 (worktree_id, "lib.rs"),
23488 Some(pane_2.downgrade()),
23489 true,
23490 window,
23491 cx,
23492 )
23493 })
23494 .unwrap()
23495 .await
23496 .downcast::<Editor>()
23497 .unwrap();
23498 pane_2
23499 .update_in(cx, |pane, window, cx| {
23500 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23501 })
23502 .await
23503 .unwrap();
23504 drop(editor_2);
23505 pane_2.update(cx, |pane, cx| {
23506 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23507 open_editor.update(cx, |editor, cx| {
23508 assert_eq!(
23509 editor.display_text(cx),
23510 lib_text,
23511 "Other file should be open and active in another panel too",
23512 );
23513 });
23514 assert_eq!(
23515 pane.items().count(),
23516 1,
23517 "No other editors should be open in another pane",
23518 );
23519 });
23520
23521 let _editor_1_reopened = workspace
23522 .update_in(cx, |workspace, window, cx| {
23523 workspace.open_path(
23524 (worktree_id, "main.rs"),
23525 Some(pane_1.downgrade()),
23526 true,
23527 window,
23528 cx,
23529 )
23530 })
23531 .unwrap()
23532 .await
23533 .downcast::<Editor>()
23534 .unwrap();
23535 let _editor_2_reopened = workspace
23536 .update_in(cx, |workspace, window, cx| {
23537 workspace.open_path(
23538 (worktree_id, "main.rs"),
23539 Some(pane_2.downgrade()),
23540 true,
23541 window,
23542 cx,
23543 )
23544 })
23545 .unwrap()
23546 .await
23547 .downcast::<Editor>()
23548 .unwrap();
23549 pane_1.update(cx, |pane, cx| {
23550 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23551 open_editor.update(cx, |editor, cx| {
23552 assert_eq!(
23553 editor.display_text(cx),
23554 main_text,
23555 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23556 );
23557 assert_eq!(
23558 editor
23559 .selections
23560 .all::<Point>(cx)
23561 .into_iter()
23562 .map(|s| s.range())
23563 .collect::<Vec<_>>(),
23564 expected_ranges,
23565 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23566 );
23567 })
23568 });
23569 pane_2.update(cx, |pane, cx| {
23570 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23571 open_editor.update(cx, |editor, cx| {
23572 assert_eq!(
23573 editor.display_text(cx),
23574 r#"fn main() {
23575⋯rintln!("1");
23576⋯intln!("2");
23577⋯ntln!("3");
23578println!("4");
23579println!("5");
23580}"#,
23581 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23582 );
23583 assert_eq!(
23584 editor
23585 .selections
23586 .all::<Point>(cx)
23587 .into_iter()
23588 .map(|s| s.range())
23589 .collect::<Vec<_>>(),
23590 vec![Point::zero()..Point::zero()],
23591 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23592 );
23593 })
23594 });
23595}
23596
23597#[gpui::test]
23598async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23599 init_test(cx, |_| {});
23600
23601 let fs = FakeFs::new(cx.executor());
23602 let main_text = r#"fn main() {
23603println!("1");
23604println!("2");
23605println!("3");
23606println!("4");
23607println!("5");
23608}"#;
23609 let lib_text = "mod foo {}";
23610 fs.insert_tree(
23611 path!("/a"),
23612 json!({
23613 "lib.rs": lib_text,
23614 "main.rs": main_text,
23615 }),
23616 )
23617 .await;
23618
23619 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23620 let (workspace, cx) =
23621 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23622 let worktree_id = workspace.update(cx, |workspace, cx| {
23623 workspace.project().update(cx, |project, cx| {
23624 project.worktrees(cx).next().unwrap().read(cx).id()
23625 })
23626 });
23627
23628 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23629 let editor = workspace
23630 .update_in(cx, |workspace, window, cx| {
23631 workspace.open_path(
23632 (worktree_id, "main.rs"),
23633 Some(pane.downgrade()),
23634 true,
23635 window,
23636 cx,
23637 )
23638 })
23639 .unwrap()
23640 .await
23641 .downcast::<Editor>()
23642 .unwrap();
23643 pane.update(cx, |pane, cx| {
23644 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23645 open_editor.update(cx, |editor, cx| {
23646 assert_eq!(
23647 editor.display_text(cx),
23648 main_text,
23649 "Original main.rs text on initial open",
23650 );
23651 })
23652 });
23653 editor.update_in(cx, |editor, window, cx| {
23654 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23655 });
23656
23657 cx.update_global(|store: &mut SettingsStore, cx| {
23658 store.update_user_settings(cx, |s| {
23659 s.workspace.restore_on_file_reopen = Some(false);
23660 });
23661 });
23662 editor.update_in(cx, |editor, window, cx| {
23663 editor.fold_ranges(
23664 vec![
23665 Point::new(1, 0)..Point::new(1, 1),
23666 Point::new(2, 0)..Point::new(2, 2),
23667 Point::new(3, 0)..Point::new(3, 3),
23668 ],
23669 false,
23670 window,
23671 cx,
23672 );
23673 });
23674 pane.update_in(cx, |pane, window, cx| {
23675 pane.close_all_items(&CloseAllItems::default(), window, cx)
23676 })
23677 .await
23678 .unwrap();
23679 pane.update(cx, |pane, _| {
23680 assert!(pane.active_item().is_none());
23681 });
23682 cx.update_global(|store: &mut SettingsStore, cx| {
23683 store.update_user_settings(cx, |s| {
23684 s.workspace.restore_on_file_reopen = Some(true);
23685 });
23686 });
23687
23688 let _editor_reopened = workspace
23689 .update_in(cx, |workspace, window, cx| {
23690 workspace.open_path(
23691 (worktree_id, "main.rs"),
23692 Some(pane.downgrade()),
23693 true,
23694 window,
23695 cx,
23696 )
23697 })
23698 .unwrap()
23699 .await
23700 .downcast::<Editor>()
23701 .unwrap();
23702 pane.update(cx, |pane, cx| {
23703 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23704 open_editor.update(cx, |editor, cx| {
23705 assert_eq!(
23706 editor.display_text(cx),
23707 main_text,
23708 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23709 );
23710 })
23711 });
23712}
23713
23714#[gpui::test]
23715async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23716 struct EmptyModalView {
23717 focus_handle: gpui::FocusHandle,
23718 }
23719 impl EventEmitter<DismissEvent> for EmptyModalView {}
23720 impl Render for EmptyModalView {
23721 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23722 div()
23723 }
23724 }
23725 impl Focusable for EmptyModalView {
23726 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23727 self.focus_handle.clone()
23728 }
23729 }
23730 impl workspace::ModalView for EmptyModalView {}
23731 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23732 EmptyModalView {
23733 focus_handle: cx.focus_handle(),
23734 }
23735 }
23736
23737 init_test(cx, |_| {});
23738
23739 let fs = FakeFs::new(cx.executor());
23740 let project = Project::test(fs, [], cx).await;
23741 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23742 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23743 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23744 let editor = cx.new_window_entity(|window, cx| {
23745 Editor::new(
23746 EditorMode::full(),
23747 buffer,
23748 Some(project.clone()),
23749 window,
23750 cx,
23751 )
23752 });
23753 workspace
23754 .update(cx, |workspace, window, cx| {
23755 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23756 })
23757 .unwrap();
23758 editor.update_in(cx, |editor, window, cx| {
23759 editor.open_context_menu(&OpenContextMenu, window, cx);
23760 assert!(editor.mouse_context_menu.is_some());
23761 });
23762 workspace
23763 .update(cx, |workspace, window, cx| {
23764 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23765 })
23766 .unwrap();
23767 cx.read(|cx| {
23768 assert!(editor.read(cx).mouse_context_menu.is_none());
23769 });
23770}
23771
23772#[gpui::test]
23773async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23774 init_test(cx, |_| {});
23775
23776 let fs = FakeFs::new(cx.executor());
23777 fs.insert_file(path!("/file.html"), Default::default())
23778 .await;
23779
23780 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23781
23782 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23783 let html_language = Arc::new(Language::new(
23784 LanguageConfig {
23785 name: "HTML".into(),
23786 matcher: LanguageMatcher {
23787 path_suffixes: vec!["html".to_string()],
23788 ..LanguageMatcher::default()
23789 },
23790 brackets: BracketPairConfig {
23791 pairs: vec![BracketPair {
23792 start: "<".into(),
23793 end: ">".into(),
23794 close: true,
23795 ..Default::default()
23796 }],
23797 ..Default::default()
23798 },
23799 ..Default::default()
23800 },
23801 Some(tree_sitter_html::LANGUAGE.into()),
23802 ));
23803 language_registry.add(html_language);
23804 let mut fake_servers = language_registry.register_fake_lsp(
23805 "HTML",
23806 FakeLspAdapter {
23807 capabilities: lsp::ServerCapabilities {
23808 completion_provider: Some(lsp::CompletionOptions {
23809 resolve_provider: Some(true),
23810 ..Default::default()
23811 }),
23812 ..Default::default()
23813 },
23814 ..Default::default()
23815 },
23816 );
23817
23818 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23819 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23820
23821 let worktree_id = workspace
23822 .update(cx, |workspace, _window, cx| {
23823 workspace.project().update(cx, |project, cx| {
23824 project.worktrees(cx).next().unwrap().read(cx).id()
23825 })
23826 })
23827 .unwrap();
23828 project
23829 .update(cx, |project, cx| {
23830 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23831 })
23832 .await
23833 .unwrap();
23834 let editor = workspace
23835 .update(cx, |workspace, window, cx| {
23836 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23837 })
23838 .unwrap()
23839 .await
23840 .unwrap()
23841 .downcast::<Editor>()
23842 .unwrap();
23843
23844 let fake_server = fake_servers.next().await.unwrap();
23845 editor.update_in(cx, |editor, window, cx| {
23846 editor.set_text("<ad></ad>", window, cx);
23847 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23848 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23849 });
23850 let Some((buffer, _)) = editor
23851 .buffer
23852 .read(cx)
23853 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23854 else {
23855 panic!("Failed to get buffer for selection position");
23856 };
23857 let buffer = buffer.read(cx);
23858 let buffer_id = buffer.remote_id();
23859 let opening_range =
23860 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
23861 let closing_range =
23862 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
23863 let mut linked_ranges = HashMap::default();
23864 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23865 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23866 });
23867 let mut completion_handle =
23868 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23869 Ok(Some(lsp::CompletionResponse::Array(vec![
23870 lsp::CompletionItem {
23871 label: "head".to_string(),
23872 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23873 lsp::InsertReplaceEdit {
23874 new_text: "head".to_string(),
23875 insert: lsp::Range::new(
23876 lsp::Position::new(0, 1),
23877 lsp::Position::new(0, 3),
23878 ),
23879 replace: lsp::Range::new(
23880 lsp::Position::new(0, 1),
23881 lsp::Position::new(0, 3),
23882 ),
23883 },
23884 )),
23885 ..Default::default()
23886 },
23887 ])))
23888 });
23889 editor.update_in(cx, |editor, window, cx| {
23890 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23891 });
23892 cx.run_until_parked();
23893 completion_handle.next().await.unwrap();
23894 editor.update(cx, |editor, _| {
23895 assert!(
23896 editor.context_menu_visible(),
23897 "Completion menu should be visible"
23898 );
23899 });
23900 editor.update_in(cx, |editor, window, cx| {
23901 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23902 });
23903 cx.executor().run_until_parked();
23904 editor.update(cx, |editor, cx| {
23905 assert_eq!(editor.text(cx), "<head></head>");
23906 });
23907}
23908
23909#[gpui::test]
23910async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
23911 init_test(cx, |_| {});
23912
23913 let fs = FakeFs::new(cx.executor());
23914 fs.insert_tree(
23915 path!("/root"),
23916 json!({
23917 "a": {
23918 "main.rs": "fn main() {}",
23919 },
23920 "foo": {
23921 "bar": {
23922 "external_file.rs": "pub mod external {}",
23923 }
23924 }
23925 }),
23926 )
23927 .await;
23928
23929 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
23930 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23931 language_registry.add(rust_lang());
23932 let _fake_servers = language_registry.register_fake_lsp(
23933 "Rust",
23934 FakeLspAdapter {
23935 ..FakeLspAdapter::default()
23936 },
23937 );
23938 let (workspace, cx) =
23939 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23940 let worktree_id = workspace.update(cx, |workspace, cx| {
23941 workspace.project().update(cx, |project, cx| {
23942 project.worktrees(cx).next().unwrap().read(cx).id()
23943 })
23944 });
23945
23946 let assert_language_servers_count =
23947 |expected: usize, context: &str, cx: &mut VisualTestContext| {
23948 project.update(cx, |project, cx| {
23949 let current = project
23950 .lsp_store()
23951 .read(cx)
23952 .as_local()
23953 .unwrap()
23954 .language_servers
23955 .len();
23956 assert_eq!(expected, current, "{context}");
23957 });
23958 };
23959
23960 assert_language_servers_count(
23961 0,
23962 "No servers should be running before any file is open",
23963 cx,
23964 );
23965 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23966 let main_editor = workspace
23967 .update_in(cx, |workspace, window, cx| {
23968 workspace.open_path(
23969 (worktree_id, "main.rs"),
23970 Some(pane.downgrade()),
23971 true,
23972 window,
23973 cx,
23974 )
23975 })
23976 .unwrap()
23977 .await
23978 .downcast::<Editor>()
23979 .unwrap();
23980 pane.update(cx, |pane, cx| {
23981 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23982 open_editor.update(cx, |editor, cx| {
23983 assert_eq!(
23984 editor.display_text(cx),
23985 "fn main() {}",
23986 "Original main.rs text on initial open",
23987 );
23988 });
23989 assert_eq!(open_editor, main_editor);
23990 });
23991 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
23992
23993 let external_editor = workspace
23994 .update_in(cx, |workspace, window, cx| {
23995 workspace.open_abs_path(
23996 PathBuf::from("/root/foo/bar/external_file.rs"),
23997 OpenOptions::default(),
23998 window,
23999 cx,
24000 )
24001 })
24002 .await
24003 .expect("opening external file")
24004 .downcast::<Editor>()
24005 .expect("downcasted external file's open element to editor");
24006 pane.update(cx, |pane, cx| {
24007 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24008 open_editor.update(cx, |editor, cx| {
24009 assert_eq!(
24010 editor.display_text(cx),
24011 "pub mod external {}",
24012 "External file is open now",
24013 );
24014 });
24015 assert_eq!(open_editor, external_editor);
24016 });
24017 assert_language_servers_count(
24018 1,
24019 "Second, external, *.rs file should join the existing server",
24020 cx,
24021 );
24022
24023 pane.update_in(cx, |pane, window, cx| {
24024 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24025 })
24026 .await
24027 .unwrap();
24028 pane.update_in(cx, |pane, window, cx| {
24029 pane.navigate_backward(&Default::default(), window, cx);
24030 });
24031 cx.run_until_parked();
24032 pane.update(cx, |pane, cx| {
24033 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24034 open_editor.update(cx, |editor, cx| {
24035 assert_eq!(
24036 editor.display_text(cx),
24037 "pub mod external {}",
24038 "External file is open now",
24039 );
24040 });
24041 });
24042 assert_language_servers_count(
24043 1,
24044 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24045 cx,
24046 );
24047
24048 cx.update(|_, cx| {
24049 workspace::reload(cx);
24050 });
24051 assert_language_servers_count(
24052 1,
24053 "After reloading the worktree with local and external files opened, only one project should be started",
24054 cx,
24055 );
24056}
24057
24058#[gpui::test]
24059async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24060 init_test(cx, |_| {});
24061
24062 let mut cx = EditorTestContext::new(cx).await;
24063 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24064 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24065
24066 // test cursor move to start of each line on tab
24067 // for `if`, `elif`, `else`, `while`, `with` and `for`
24068 cx.set_state(indoc! {"
24069 def main():
24070 ˇ for item in items:
24071 ˇ while item.active:
24072 ˇ if item.value > 10:
24073 ˇ continue
24074 ˇ elif item.value < 0:
24075 ˇ break
24076 ˇ else:
24077 ˇ with item.context() as ctx:
24078 ˇ yield count
24079 ˇ else:
24080 ˇ log('while else')
24081 ˇ else:
24082 ˇ log('for else')
24083 "});
24084 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24085 cx.assert_editor_state(indoc! {"
24086 def main():
24087 ˇfor item in items:
24088 ˇwhile item.active:
24089 ˇif item.value > 10:
24090 ˇcontinue
24091 ˇelif item.value < 0:
24092 ˇbreak
24093 ˇelse:
24094 ˇwith item.context() as ctx:
24095 ˇyield count
24096 ˇelse:
24097 ˇlog('while else')
24098 ˇelse:
24099 ˇlog('for else')
24100 "});
24101 // test relative indent is preserved when tab
24102 // for `if`, `elif`, `else`, `while`, `with` and `for`
24103 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24104 cx.assert_editor_state(indoc! {"
24105 def main():
24106 ˇfor item in items:
24107 ˇwhile item.active:
24108 ˇif item.value > 10:
24109 ˇcontinue
24110 ˇelif item.value < 0:
24111 ˇbreak
24112 ˇelse:
24113 ˇwith item.context() as ctx:
24114 ˇyield count
24115 ˇelse:
24116 ˇlog('while else')
24117 ˇelse:
24118 ˇlog('for else')
24119 "});
24120
24121 // test cursor move to start of each line on tab
24122 // for `try`, `except`, `else`, `finally`, `match` and `def`
24123 cx.set_state(indoc! {"
24124 def main():
24125 ˇ try:
24126 ˇ fetch()
24127 ˇ except ValueError:
24128 ˇ handle_error()
24129 ˇ else:
24130 ˇ match value:
24131 ˇ case _:
24132 ˇ finally:
24133 ˇ def status():
24134 ˇ return 0
24135 "});
24136 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24137 cx.assert_editor_state(indoc! {"
24138 def main():
24139 ˇtry:
24140 ˇfetch()
24141 ˇexcept ValueError:
24142 ˇhandle_error()
24143 ˇelse:
24144 ˇmatch value:
24145 ˇcase _:
24146 ˇfinally:
24147 ˇdef status():
24148 ˇreturn 0
24149 "});
24150 // test relative indent is preserved when tab
24151 // for `try`, `except`, `else`, `finally`, `match` and `def`
24152 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24153 cx.assert_editor_state(indoc! {"
24154 def main():
24155 ˇtry:
24156 ˇfetch()
24157 ˇexcept ValueError:
24158 ˇhandle_error()
24159 ˇelse:
24160 ˇmatch value:
24161 ˇcase _:
24162 ˇfinally:
24163 ˇdef status():
24164 ˇreturn 0
24165 "});
24166}
24167
24168#[gpui::test]
24169async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24170 init_test(cx, |_| {});
24171
24172 let mut cx = EditorTestContext::new(cx).await;
24173 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24174 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24175
24176 // test `else` auto outdents when typed inside `if` block
24177 cx.set_state(indoc! {"
24178 def main():
24179 if i == 2:
24180 return
24181 ˇ
24182 "});
24183 cx.update_editor(|editor, window, cx| {
24184 editor.handle_input("else:", window, cx);
24185 });
24186 cx.assert_editor_state(indoc! {"
24187 def main():
24188 if i == 2:
24189 return
24190 else:ˇ
24191 "});
24192
24193 // test `except` auto outdents when typed inside `try` block
24194 cx.set_state(indoc! {"
24195 def main():
24196 try:
24197 i = 2
24198 ˇ
24199 "});
24200 cx.update_editor(|editor, window, cx| {
24201 editor.handle_input("except:", window, cx);
24202 });
24203 cx.assert_editor_state(indoc! {"
24204 def main():
24205 try:
24206 i = 2
24207 except:ˇ
24208 "});
24209
24210 // test `else` auto outdents when typed inside `except` block
24211 cx.set_state(indoc! {"
24212 def main():
24213 try:
24214 i = 2
24215 except:
24216 j = 2
24217 ˇ
24218 "});
24219 cx.update_editor(|editor, window, cx| {
24220 editor.handle_input("else:", window, cx);
24221 });
24222 cx.assert_editor_state(indoc! {"
24223 def main():
24224 try:
24225 i = 2
24226 except:
24227 j = 2
24228 else:ˇ
24229 "});
24230
24231 // test `finally` auto outdents when typed inside `else` block
24232 cx.set_state(indoc! {"
24233 def main():
24234 try:
24235 i = 2
24236 except:
24237 j = 2
24238 else:
24239 k = 2
24240 ˇ
24241 "});
24242 cx.update_editor(|editor, window, cx| {
24243 editor.handle_input("finally:", window, cx);
24244 });
24245 cx.assert_editor_state(indoc! {"
24246 def main():
24247 try:
24248 i = 2
24249 except:
24250 j = 2
24251 else:
24252 k = 2
24253 finally:ˇ
24254 "});
24255
24256 // test `else` does not outdents when typed inside `except` block right after for block
24257 cx.set_state(indoc! {"
24258 def main():
24259 try:
24260 i = 2
24261 except:
24262 for i in range(n):
24263 pass
24264 ˇ
24265 "});
24266 cx.update_editor(|editor, window, cx| {
24267 editor.handle_input("else:", window, cx);
24268 });
24269 cx.assert_editor_state(indoc! {"
24270 def main():
24271 try:
24272 i = 2
24273 except:
24274 for i in range(n):
24275 pass
24276 else:ˇ
24277 "});
24278
24279 // test `finally` auto outdents when typed inside `else` block right after for block
24280 cx.set_state(indoc! {"
24281 def main():
24282 try:
24283 i = 2
24284 except:
24285 j = 2
24286 else:
24287 for i in range(n):
24288 pass
24289 ˇ
24290 "});
24291 cx.update_editor(|editor, window, cx| {
24292 editor.handle_input("finally:", window, cx);
24293 });
24294 cx.assert_editor_state(indoc! {"
24295 def main():
24296 try:
24297 i = 2
24298 except:
24299 j = 2
24300 else:
24301 for i in range(n):
24302 pass
24303 finally:ˇ
24304 "});
24305
24306 // test `except` outdents to inner "try" block
24307 cx.set_state(indoc! {"
24308 def main():
24309 try:
24310 i = 2
24311 if i == 2:
24312 try:
24313 i = 3
24314 ˇ
24315 "});
24316 cx.update_editor(|editor, window, cx| {
24317 editor.handle_input("except:", window, cx);
24318 });
24319 cx.assert_editor_state(indoc! {"
24320 def main():
24321 try:
24322 i = 2
24323 if i == 2:
24324 try:
24325 i = 3
24326 except:ˇ
24327 "});
24328
24329 // test `except` outdents to outer "try" block
24330 cx.set_state(indoc! {"
24331 def main():
24332 try:
24333 i = 2
24334 if i == 2:
24335 try:
24336 i = 3
24337 ˇ
24338 "});
24339 cx.update_editor(|editor, window, cx| {
24340 editor.handle_input("except:", window, cx);
24341 });
24342 cx.assert_editor_state(indoc! {"
24343 def main():
24344 try:
24345 i = 2
24346 if i == 2:
24347 try:
24348 i = 3
24349 except:ˇ
24350 "});
24351
24352 // test `else` stays at correct indent when typed after `for` block
24353 cx.set_state(indoc! {"
24354 def main():
24355 for i in range(10):
24356 if i == 3:
24357 break
24358 ˇ
24359 "});
24360 cx.update_editor(|editor, window, cx| {
24361 editor.handle_input("else:", window, cx);
24362 });
24363 cx.assert_editor_state(indoc! {"
24364 def main():
24365 for i in range(10):
24366 if i == 3:
24367 break
24368 else:ˇ
24369 "});
24370
24371 // test does not outdent on typing after line with square brackets
24372 cx.set_state(indoc! {"
24373 def f() -> list[str]:
24374 ˇ
24375 "});
24376 cx.update_editor(|editor, window, cx| {
24377 editor.handle_input("a", window, cx);
24378 });
24379 cx.assert_editor_state(indoc! {"
24380 def f() -> list[str]:
24381 aˇ
24382 "});
24383
24384 // test does not outdent on typing : after case keyword
24385 cx.set_state(indoc! {"
24386 match 1:
24387 caseˇ
24388 "});
24389 cx.update_editor(|editor, window, cx| {
24390 editor.handle_input(":", window, cx);
24391 });
24392 cx.assert_editor_state(indoc! {"
24393 match 1:
24394 case:ˇ
24395 "});
24396}
24397
24398#[gpui::test]
24399async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24400 init_test(cx, |_| {});
24401 update_test_language_settings(cx, |settings| {
24402 settings.defaults.extend_comment_on_newline = Some(false);
24403 });
24404 let mut cx = EditorTestContext::new(cx).await;
24405 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24406 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24407
24408 // test correct indent after newline on comment
24409 cx.set_state(indoc! {"
24410 # COMMENT:ˇ
24411 "});
24412 cx.update_editor(|editor, window, cx| {
24413 editor.newline(&Newline, window, cx);
24414 });
24415 cx.assert_editor_state(indoc! {"
24416 # COMMENT:
24417 ˇ
24418 "});
24419
24420 // test correct indent after newline in brackets
24421 cx.set_state(indoc! {"
24422 {ˇ}
24423 "});
24424 cx.update_editor(|editor, window, cx| {
24425 editor.newline(&Newline, window, cx);
24426 });
24427 cx.run_until_parked();
24428 cx.assert_editor_state(indoc! {"
24429 {
24430 ˇ
24431 }
24432 "});
24433
24434 cx.set_state(indoc! {"
24435 (ˇ)
24436 "});
24437 cx.update_editor(|editor, window, cx| {
24438 editor.newline(&Newline, window, cx);
24439 });
24440 cx.run_until_parked();
24441 cx.assert_editor_state(indoc! {"
24442 (
24443 ˇ
24444 )
24445 "});
24446
24447 // do not indent after empty lists or dictionaries
24448 cx.set_state(indoc! {"
24449 a = []ˇ
24450 "});
24451 cx.update_editor(|editor, window, cx| {
24452 editor.newline(&Newline, window, cx);
24453 });
24454 cx.run_until_parked();
24455 cx.assert_editor_state(indoc! {"
24456 a = []
24457 ˇ
24458 "});
24459}
24460
24461#[gpui::test]
24462async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24463 init_test(cx, |_| {});
24464
24465 let mut cx = EditorTestContext::new(cx).await;
24466 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24467 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24468
24469 // test cursor move to start of each line on tab
24470 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24471 cx.set_state(indoc! {"
24472 function main() {
24473 ˇ for item in $items; do
24474 ˇ while [ -n \"$item\" ]; do
24475 ˇ if [ \"$value\" -gt 10 ]; then
24476 ˇ continue
24477 ˇ elif [ \"$value\" -lt 0 ]; then
24478 ˇ break
24479 ˇ else
24480 ˇ echo \"$item\"
24481 ˇ fi
24482 ˇ done
24483 ˇ done
24484 ˇ}
24485 "});
24486 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24487 cx.assert_editor_state(indoc! {"
24488 function main() {
24489 ˇfor item in $items; do
24490 ˇwhile [ -n \"$item\" ]; do
24491 ˇif [ \"$value\" -gt 10 ]; then
24492 ˇcontinue
24493 ˇelif [ \"$value\" -lt 0 ]; then
24494 ˇbreak
24495 ˇelse
24496 ˇecho \"$item\"
24497 ˇfi
24498 ˇdone
24499 ˇdone
24500 ˇ}
24501 "});
24502 // test relative indent is preserved when tab
24503 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24504 cx.assert_editor_state(indoc! {"
24505 function main() {
24506 ˇfor item in $items; do
24507 ˇwhile [ -n \"$item\" ]; do
24508 ˇif [ \"$value\" -gt 10 ]; then
24509 ˇcontinue
24510 ˇelif [ \"$value\" -lt 0 ]; then
24511 ˇbreak
24512 ˇelse
24513 ˇecho \"$item\"
24514 ˇfi
24515 ˇdone
24516 ˇdone
24517 ˇ}
24518 "});
24519
24520 // test cursor move to start of each line on tab
24521 // for `case` statement with patterns
24522 cx.set_state(indoc! {"
24523 function handle() {
24524 ˇ case \"$1\" in
24525 ˇ start)
24526 ˇ echo \"a\"
24527 ˇ ;;
24528 ˇ stop)
24529 ˇ echo \"b\"
24530 ˇ ;;
24531 ˇ *)
24532 ˇ echo \"c\"
24533 ˇ ;;
24534 ˇ esac
24535 ˇ}
24536 "});
24537 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24538 cx.assert_editor_state(indoc! {"
24539 function handle() {
24540 ˇcase \"$1\" in
24541 ˇstart)
24542 ˇecho \"a\"
24543 ˇ;;
24544 ˇstop)
24545 ˇecho \"b\"
24546 ˇ;;
24547 ˇ*)
24548 ˇecho \"c\"
24549 ˇ;;
24550 ˇesac
24551 ˇ}
24552 "});
24553}
24554
24555#[gpui::test]
24556async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24557 init_test(cx, |_| {});
24558
24559 let mut cx = EditorTestContext::new(cx).await;
24560 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24561 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24562
24563 // test indents on comment insert
24564 cx.set_state(indoc! {"
24565 function main() {
24566 ˇ for item in $items; do
24567 ˇ while [ -n \"$item\" ]; do
24568 ˇ if [ \"$value\" -gt 10 ]; then
24569 ˇ continue
24570 ˇ elif [ \"$value\" -lt 0 ]; then
24571 ˇ break
24572 ˇ else
24573 ˇ echo \"$item\"
24574 ˇ fi
24575 ˇ done
24576 ˇ done
24577 ˇ}
24578 "});
24579 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24580 cx.assert_editor_state(indoc! {"
24581 function main() {
24582 #ˇ for item in $items; do
24583 #ˇ while [ -n \"$item\" ]; do
24584 #ˇ if [ \"$value\" -gt 10 ]; then
24585 #ˇ continue
24586 #ˇ elif [ \"$value\" -lt 0 ]; then
24587 #ˇ break
24588 #ˇ else
24589 #ˇ echo \"$item\"
24590 #ˇ fi
24591 #ˇ done
24592 #ˇ done
24593 #ˇ}
24594 "});
24595}
24596
24597#[gpui::test]
24598async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24599 init_test(cx, |_| {});
24600
24601 let mut cx = EditorTestContext::new(cx).await;
24602 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24603 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24604
24605 // test `else` auto outdents when typed inside `if` block
24606 cx.set_state(indoc! {"
24607 if [ \"$1\" = \"test\" ]; then
24608 echo \"foo bar\"
24609 ˇ
24610 "});
24611 cx.update_editor(|editor, window, cx| {
24612 editor.handle_input("else", window, cx);
24613 });
24614 cx.assert_editor_state(indoc! {"
24615 if [ \"$1\" = \"test\" ]; then
24616 echo \"foo bar\"
24617 elseˇ
24618 "});
24619
24620 // test `elif` auto outdents when typed inside `if` block
24621 cx.set_state(indoc! {"
24622 if [ \"$1\" = \"test\" ]; then
24623 echo \"foo bar\"
24624 ˇ
24625 "});
24626 cx.update_editor(|editor, window, cx| {
24627 editor.handle_input("elif", window, cx);
24628 });
24629 cx.assert_editor_state(indoc! {"
24630 if [ \"$1\" = \"test\" ]; then
24631 echo \"foo bar\"
24632 elifˇ
24633 "});
24634
24635 // test `fi` auto outdents when typed inside `else` block
24636 cx.set_state(indoc! {"
24637 if [ \"$1\" = \"test\" ]; then
24638 echo \"foo bar\"
24639 else
24640 echo \"bar baz\"
24641 ˇ
24642 "});
24643 cx.update_editor(|editor, window, cx| {
24644 editor.handle_input("fi", window, cx);
24645 });
24646 cx.assert_editor_state(indoc! {"
24647 if [ \"$1\" = \"test\" ]; then
24648 echo \"foo bar\"
24649 else
24650 echo \"bar baz\"
24651 fiˇ
24652 "});
24653
24654 // test `done` auto outdents when typed inside `while` block
24655 cx.set_state(indoc! {"
24656 while read line; do
24657 echo \"$line\"
24658 ˇ
24659 "});
24660 cx.update_editor(|editor, window, cx| {
24661 editor.handle_input("done", window, cx);
24662 });
24663 cx.assert_editor_state(indoc! {"
24664 while read line; do
24665 echo \"$line\"
24666 doneˇ
24667 "});
24668
24669 // test `done` auto outdents when typed inside `for` block
24670 cx.set_state(indoc! {"
24671 for file in *.txt; do
24672 cat \"$file\"
24673 ˇ
24674 "});
24675 cx.update_editor(|editor, window, cx| {
24676 editor.handle_input("done", window, cx);
24677 });
24678 cx.assert_editor_state(indoc! {"
24679 for file in *.txt; do
24680 cat \"$file\"
24681 doneˇ
24682 "});
24683
24684 // test `esac` auto outdents when typed inside `case` block
24685 cx.set_state(indoc! {"
24686 case \"$1\" in
24687 start)
24688 echo \"foo bar\"
24689 ;;
24690 stop)
24691 echo \"bar baz\"
24692 ;;
24693 ˇ
24694 "});
24695 cx.update_editor(|editor, window, cx| {
24696 editor.handle_input("esac", window, cx);
24697 });
24698 cx.assert_editor_state(indoc! {"
24699 case \"$1\" in
24700 start)
24701 echo \"foo bar\"
24702 ;;
24703 stop)
24704 echo \"bar baz\"
24705 ;;
24706 esacˇ
24707 "});
24708
24709 // test `*)` auto outdents when typed inside `case` block
24710 cx.set_state(indoc! {"
24711 case \"$1\" in
24712 start)
24713 echo \"foo bar\"
24714 ;;
24715 ˇ
24716 "});
24717 cx.update_editor(|editor, window, cx| {
24718 editor.handle_input("*)", window, cx);
24719 });
24720 cx.assert_editor_state(indoc! {"
24721 case \"$1\" in
24722 start)
24723 echo \"foo bar\"
24724 ;;
24725 *)ˇ
24726 "});
24727
24728 // test `fi` outdents to correct level with nested if blocks
24729 cx.set_state(indoc! {"
24730 if [ \"$1\" = \"test\" ]; then
24731 echo \"outer if\"
24732 if [ \"$2\" = \"debug\" ]; then
24733 echo \"inner if\"
24734 ˇ
24735 "});
24736 cx.update_editor(|editor, window, cx| {
24737 editor.handle_input("fi", window, cx);
24738 });
24739 cx.assert_editor_state(indoc! {"
24740 if [ \"$1\" = \"test\" ]; then
24741 echo \"outer if\"
24742 if [ \"$2\" = \"debug\" ]; then
24743 echo \"inner if\"
24744 fiˇ
24745 "});
24746}
24747
24748#[gpui::test]
24749async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24750 init_test(cx, |_| {});
24751 update_test_language_settings(cx, |settings| {
24752 settings.defaults.extend_comment_on_newline = Some(false);
24753 });
24754 let mut cx = EditorTestContext::new(cx).await;
24755 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24756 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24757
24758 // test correct indent after newline on comment
24759 cx.set_state(indoc! {"
24760 # COMMENT:ˇ
24761 "});
24762 cx.update_editor(|editor, window, cx| {
24763 editor.newline(&Newline, window, cx);
24764 });
24765 cx.assert_editor_state(indoc! {"
24766 # COMMENT:
24767 ˇ
24768 "});
24769
24770 // test correct indent after newline after `then`
24771 cx.set_state(indoc! {"
24772
24773 if [ \"$1\" = \"test\" ]; thenˇ
24774 "});
24775 cx.update_editor(|editor, window, cx| {
24776 editor.newline(&Newline, window, cx);
24777 });
24778 cx.run_until_parked();
24779 cx.assert_editor_state(indoc! {"
24780
24781 if [ \"$1\" = \"test\" ]; then
24782 ˇ
24783 "});
24784
24785 // test correct indent after newline after `else`
24786 cx.set_state(indoc! {"
24787 if [ \"$1\" = \"test\" ]; then
24788 elseˇ
24789 "});
24790 cx.update_editor(|editor, window, cx| {
24791 editor.newline(&Newline, window, cx);
24792 });
24793 cx.run_until_parked();
24794 cx.assert_editor_state(indoc! {"
24795 if [ \"$1\" = \"test\" ]; then
24796 else
24797 ˇ
24798 "});
24799
24800 // test correct indent after newline after `elif`
24801 cx.set_state(indoc! {"
24802 if [ \"$1\" = \"test\" ]; then
24803 elifˇ
24804 "});
24805 cx.update_editor(|editor, window, cx| {
24806 editor.newline(&Newline, window, cx);
24807 });
24808 cx.run_until_parked();
24809 cx.assert_editor_state(indoc! {"
24810 if [ \"$1\" = \"test\" ]; then
24811 elif
24812 ˇ
24813 "});
24814
24815 // test correct indent after newline after `do`
24816 cx.set_state(indoc! {"
24817 for file in *.txt; doˇ
24818 "});
24819 cx.update_editor(|editor, window, cx| {
24820 editor.newline(&Newline, window, cx);
24821 });
24822 cx.run_until_parked();
24823 cx.assert_editor_state(indoc! {"
24824 for file in *.txt; do
24825 ˇ
24826 "});
24827
24828 // test correct indent after newline after case pattern
24829 cx.set_state(indoc! {"
24830 case \"$1\" in
24831 start)ˇ
24832 "});
24833 cx.update_editor(|editor, window, cx| {
24834 editor.newline(&Newline, window, cx);
24835 });
24836 cx.run_until_parked();
24837 cx.assert_editor_state(indoc! {"
24838 case \"$1\" in
24839 start)
24840 ˇ
24841 "});
24842
24843 // test correct indent after newline after case pattern
24844 cx.set_state(indoc! {"
24845 case \"$1\" in
24846 start)
24847 ;;
24848 *)ˇ
24849 "});
24850 cx.update_editor(|editor, window, cx| {
24851 editor.newline(&Newline, window, cx);
24852 });
24853 cx.run_until_parked();
24854 cx.assert_editor_state(indoc! {"
24855 case \"$1\" in
24856 start)
24857 ;;
24858 *)
24859 ˇ
24860 "});
24861
24862 // test correct indent after newline after function opening brace
24863 cx.set_state(indoc! {"
24864 function test() {ˇ}
24865 "});
24866 cx.update_editor(|editor, window, cx| {
24867 editor.newline(&Newline, window, cx);
24868 });
24869 cx.run_until_parked();
24870 cx.assert_editor_state(indoc! {"
24871 function test() {
24872 ˇ
24873 }
24874 "});
24875
24876 // test no extra indent after semicolon on same line
24877 cx.set_state(indoc! {"
24878 echo \"test\";ˇ
24879 "});
24880 cx.update_editor(|editor, window, cx| {
24881 editor.newline(&Newline, window, cx);
24882 });
24883 cx.run_until_parked();
24884 cx.assert_editor_state(indoc! {"
24885 echo \"test\";
24886 ˇ
24887 "});
24888}
24889
24890fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24891 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24892 point..point
24893}
24894
24895#[track_caller]
24896fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24897 let (text, ranges) = marked_text_ranges(marked_text, true);
24898 assert_eq!(editor.text(cx), text);
24899 assert_eq!(
24900 editor.selections.ranges(cx),
24901 ranges,
24902 "Assert selections are {}",
24903 marked_text
24904 );
24905}
24906
24907pub fn handle_signature_help_request(
24908 cx: &mut EditorLspTestContext,
24909 mocked_response: lsp::SignatureHelp,
24910) -> impl Future<Output = ()> + use<> {
24911 let mut request =
24912 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
24913 let mocked_response = mocked_response.clone();
24914 async move { Ok(Some(mocked_response)) }
24915 });
24916
24917 async move {
24918 request.next().await;
24919 }
24920}
24921
24922#[track_caller]
24923pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
24924 cx.update_editor(|editor, _, _| {
24925 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
24926 let entries = menu.entries.borrow();
24927 let entries = entries
24928 .iter()
24929 .map(|entry| entry.string.as_str())
24930 .collect::<Vec<_>>();
24931 assert_eq!(entries, expected);
24932 } else {
24933 panic!("Expected completions menu");
24934 }
24935 });
24936}
24937
24938/// Handle completion request passing a marked string specifying where the completion
24939/// should be triggered from using '|' character, what range should be replaced, and what completions
24940/// should be returned using '<' and '>' to delimit the range.
24941///
24942/// Also see `handle_completion_request_with_insert_and_replace`.
24943#[track_caller]
24944pub fn handle_completion_request(
24945 marked_string: &str,
24946 completions: Vec<&'static str>,
24947 is_incomplete: bool,
24948 counter: Arc<AtomicUsize>,
24949 cx: &mut EditorLspTestContext,
24950) -> impl Future<Output = ()> {
24951 let complete_from_marker: TextRangeMarker = '|'.into();
24952 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24953 let (_, mut marked_ranges) = marked_text_ranges_by(
24954 marked_string,
24955 vec![complete_from_marker.clone(), replace_range_marker.clone()],
24956 );
24957
24958 let complete_from_position =
24959 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24960 let replace_range =
24961 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24962
24963 let mut request =
24964 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24965 let completions = completions.clone();
24966 counter.fetch_add(1, atomic::Ordering::Release);
24967 async move {
24968 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24969 assert_eq!(
24970 params.text_document_position.position,
24971 complete_from_position
24972 );
24973 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
24974 is_incomplete,
24975 item_defaults: None,
24976 items: completions
24977 .iter()
24978 .map(|completion_text| lsp::CompletionItem {
24979 label: completion_text.to_string(),
24980 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
24981 range: replace_range,
24982 new_text: completion_text.to_string(),
24983 })),
24984 ..Default::default()
24985 })
24986 .collect(),
24987 })))
24988 }
24989 });
24990
24991 async move {
24992 request.next().await;
24993 }
24994}
24995
24996/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
24997/// given instead, which also contains an `insert` range.
24998///
24999/// This function uses markers to define ranges:
25000/// - `|` marks the cursor position
25001/// - `<>` marks the replace range
25002/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25003pub fn handle_completion_request_with_insert_and_replace(
25004 cx: &mut EditorLspTestContext,
25005 marked_string: &str,
25006 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25007 counter: Arc<AtomicUsize>,
25008) -> impl Future<Output = ()> {
25009 let complete_from_marker: TextRangeMarker = '|'.into();
25010 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25011 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25012
25013 let (_, mut marked_ranges) = marked_text_ranges_by(
25014 marked_string,
25015 vec![
25016 complete_from_marker.clone(),
25017 replace_range_marker.clone(),
25018 insert_range_marker.clone(),
25019 ],
25020 );
25021
25022 let complete_from_position =
25023 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25024 let replace_range =
25025 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25026
25027 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25028 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25029 _ => lsp::Range {
25030 start: replace_range.start,
25031 end: complete_from_position,
25032 },
25033 };
25034
25035 let mut request =
25036 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25037 let completions = completions.clone();
25038 counter.fetch_add(1, atomic::Ordering::Release);
25039 async move {
25040 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25041 assert_eq!(
25042 params.text_document_position.position, complete_from_position,
25043 "marker `|` position doesn't match",
25044 );
25045 Ok(Some(lsp::CompletionResponse::Array(
25046 completions
25047 .iter()
25048 .map(|(label, new_text)| lsp::CompletionItem {
25049 label: label.to_string(),
25050 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25051 lsp::InsertReplaceEdit {
25052 insert: insert_range,
25053 replace: replace_range,
25054 new_text: new_text.to_string(),
25055 },
25056 )),
25057 ..Default::default()
25058 })
25059 .collect(),
25060 )))
25061 }
25062 });
25063
25064 async move {
25065 request.next().await;
25066 }
25067}
25068
25069fn handle_resolve_completion_request(
25070 cx: &mut EditorLspTestContext,
25071 edits: Option<Vec<(&'static str, &'static str)>>,
25072) -> impl Future<Output = ()> {
25073 let edits = edits.map(|edits| {
25074 edits
25075 .iter()
25076 .map(|(marked_string, new_text)| {
25077 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25078 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25079 lsp::TextEdit::new(replace_range, new_text.to_string())
25080 })
25081 .collect::<Vec<_>>()
25082 });
25083
25084 let mut request =
25085 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25086 let edits = edits.clone();
25087 async move {
25088 Ok(lsp::CompletionItem {
25089 additional_text_edits: edits,
25090 ..Default::default()
25091 })
25092 }
25093 });
25094
25095 async move {
25096 request.next().await;
25097 }
25098}
25099
25100pub(crate) fn update_test_language_settings(
25101 cx: &mut TestAppContext,
25102 f: impl Fn(&mut AllLanguageSettingsContent),
25103) {
25104 cx.update(|cx| {
25105 SettingsStore::update_global(cx, |store, cx| {
25106 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25107 });
25108 });
25109}
25110
25111pub(crate) fn update_test_project_settings(
25112 cx: &mut TestAppContext,
25113 f: impl Fn(&mut ProjectSettingsContent),
25114) {
25115 cx.update(|cx| {
25116 SettingsStore::update_global(cx, |store, cx| {
25117 store.update_user_settings(cx, |settings| f(&mut settings.project));
25118 });
25119 });
25120}
25121
25122pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25123 cx.update(|cx| {
25124 assets::Assets.load_test_fonts(cx);
25125 let store = SettingsStore::test(cx);
25126 cx.set_global(store);
25127 theme::init(theme::LoadThemes::JustBase, cx);
25128 release_channel::init(SemanticVersion::default(), cx);
25129 client::init_settings(cx);
25130 language::init(cx);
25131 Project::init_settings(cx);
25132 workspace::init_settings(cx);
25133 crate::init(cx);
25134 });
25135 zlog::init_test();
25136 update_test_language_settings(cx, f);
25137}
25138
25139#[track_caller]
25140fn assert_hunk_revert(
25141 not_reverted_text_with_selections: &str,
25142 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25143 expected_reverted_text_with_selections: &str,
25144 base_text: &str,
25145 cx: &mut EditorLspTestContext,
25146) {
25147 cx.set_state(not_reverted_text_with_selections);
25148 cx.set_head_text(base_text);
25149 cx.executor().run_until_parked();
25150
25151 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25152 let snapshot = editor.snapshot(window, cx);
25153 let reverted_hunk_statuses = snapshot
25154 .buffer_snapshot
25155 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
25156 .map(|hunk| hunk.status().kind)
25157 .collect::<Vec<_>>();
25158
25159 editor.git_restore(&Default::default(), window, cx);
25160 reverted_hunk_statuses
25161 });
25162 cx.executor().run_until_parked();
25163 cx.assert_editor_state(expected_reverted_text_with_selections);
25164 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25165}
25166
25167#[gpui::test(iterations = 10)]
25168async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25169 init_test(cx, |_| {});
25170
25171 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25172 let counter = diagnostic_requests.clone();
25173
25174 let fs = FakeFs::new(cx.executor());
25175 fs.insert_tree(
25176 path!("/a"),
25177 json!({
25178 "first.rs": "fn main() { let a = 5; }",
25179 "second.rs": "// Test file",
25180 }),
25181 )
25182 .await;
25183
25184 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25185 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25186 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25187
25188 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25189 language_registry.add(rust_lang());
25190 let mut fake_servers = language_registry.register_fake_lsp(
25191 "Rust",
25192 FakeLspAdapter {
25193 capabilities: lsp::ServerCapabilities {
25194 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25195 lsp::DiagnosticOptions {
25196 identifier: None,
25197 inter_file_dependencies: true,
25198 workspace_diagnostics: true,
25199 work_done_progress_options: Default::default(),
25200 },
25201 )),
25202 ..Default::default()
25203 },
25204 ..Default::default()
25205 },
25206 );
25207
25208 let editor = workspace
25209 .update(cx, |workspace, window, cx| {
25210 workspace.open_abs_path(
25211 PathBuf::from(path!("/a/first.rs")),
25212 OpenOptions::default(),
25213 window,
25214 cx,
25215 )
25216 })
25217 .unwrap()
25218 .await
25219 .unwrap()
25220 .downcast::<Editor>()
25221 .unwrap();
25222 let fake_server = fake_servers.next().await.unwrap();
25223 let server_id = fake_server.server.server_id();
25224 let mut first_request = fake_server
25225 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25226 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25227 let result_id = Some(new_result_id.to_string());
25228 assert_eq!(
25229 params.text_document.uri,
25230 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25231 );
25232 async move {
25233 Ok(lsp::DocumentDiagnosticReportResult::Report(
25234 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25235 related_documents: None,
25236 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25237 items: Vec::new(),
25238 result_id,
25239 },
25240 }),
25241 ))
25242 }
25243 });
25244
25245 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25246 project.update(cx, |project, cx| {
25247 let buffer_id = editor
25248 .read(cx)
25249 .buffer()
25250 .read(cx)
25251 .as_singleton()
25252 .expect("created a singleton buffer")
25253 .read(cx)
25254 .remote_id();
25255 let buffer_result_id = project
25256 .lsp_store()
25257 .read(cx)
25258 .result_id(server_id, buffer_id, cx);
25259 assert_eq!(expected, buffer_result_id);
25260 });
25261 };
25262
25263 ensure_result_id(None, cx);
25264 cx.executor().advance_clock(Duration::from_millis(60));
25265 cx.executor().run_until_parked();
25266 assert_eq!(
25267 diagnostic_requests.load(atomic::Ordering::Acquire),
25268 1,
25269 "Opening file should trigger diagnostic request"
25270 );
25271 first_request
25272 .next()
25273 .await
25274 .expect("should have sent the first diagnostics pull request");
25275 ensure_result_id(Some("1".to_string()), cx);
25276
25277 // Editing should trigger diagnostics
25278 editor.update_in(cx, |editor, window, cx| {
25279 editor.handle_input("2", window, cx)
25280 });
25281 cx.executor().advance_clock(Duration::from_millis(60));
25282 cx.executor().run_until_parked();
25283 assert_eq!(
25284 diagnostic_requests.load(atomic::Ordering::Acquire),
25285 2,
25286 "Editing should trigger diagnostic request"
25287 );
25288 ensure_result_id(Some("2".to_string()), cx);
25289
25290 // Moving cursor should not trigger diagnostic request
25291 editor.update_in(cx, |editor, window, cx| {
25292 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25293 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25294 });
25295 });
25296 cx.executor().advance_clock(Duration::from_millis(60));
25297 cx.executor().run_until_parked();
25298 assert_eq!(
25299 diagnostic_requests.load(atomic::Ordering::Acquire),
25300 2,
25301 "Cursor movement should not trigger diagnostic request"
25302 );
25303 ensure_result_id(Some("2".to_string()), cx);
25304 // Multiple rapid edits should be debounced
25305 for _ in 0..5 {
25306 editor.update_in(cx, |editor, window, cx| {
25307 editor.handle_input("x", window, cx)
25308 });
25309 }
25310 cx.executor().advance_clock(Duration::from_millis(60));
25311 cx.executor().run_until_parked();
25312
25313 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25314 assert!(
25315 final_requests <= 4,
25316 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25317 );
25318 ensure_result_id(Some(final_requests.to_string()), cx);
25319}
25320
25321#[gpui::test]
25322async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25323 // Regression test for issue #11671
25324 // Previously, adding a cursor after moving multiple cursors would reset
25325 // the cursor count instead of adding to the existing cursors.
25326 init_test(cx, |_| {});
25327 let mut cx = EditorTestContext::new(cx).await;
25328
25329 // Create a simple buffer with cursor at start
25330 cx.set_state(indoc! {"
25331 ˇaaaa
25332 bbbb
25333 cccc
25334 dddd
25335 eeee
25336 ffff
25337 gggg
25338 hhhh"});
25339
25340 // Add 2 cursors below (so we have 3 total)
25341 cx.update_editor(|editor, window, cx| {
25342 editor.add_selection_below(&Default::default(), window, cx);
25343 editor.add_selection_below(&Default::default(), window, cx);
25344 });
25345
25346 // Verify we have 3 cursors
25347 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25348 assert_eq!(
25349 initial_count, 3,
25350 "Should have 3 cursors after adding 2 below"
25351 );
25352
25353 // Move down one line
25354 cx.update_editor(|editor, window, cx| {
25355 editor.move_down(&MoveDown, window, cx);
25356 });
25357
25358 // Add another cursor below
25359 cx.update_editor(|editor, window, cx| {
25360 editor.add_selection_below(&Default::default(), window, cx);
25361 });
25362
25363 // Should now have 4 cursors (3 original + 1 new)
25364 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25365 assert_eq!(
25366 final_count, 4,
25367 "Should have 4 cursors after moving and adding another"
25368 );
25369}
25370
25371#[gpui::test(iterations = 10)]
25372async fn test_document_colors(cx: &mut TestAppContext) {
25373 let expected_color = Rgba {
25374 r: 0.33,
25375 g: 0.33,
25376 b: 0.33,
25377 a: 0.33,
25378 };
25379
25380 init_test(cx, |_| {});
25381
25382 let fs = FakeFs::new(cx.executor());
25383 fs.insert_tree(
25384 path!("/a"),
25385 json!({
25386 "first.rs": "fn main() { let a = 5; }",
25387 }),
25388 )
25389 .await;
25390
25391 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25392 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25393 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25394
25395 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25396 language_registry.add(rust_lang());
25397 let mut fake_servers = language_registry.register_fake_lsp(
25398 "Rust",
25399 FakeLspAdapter {
25400 capabilities: lsp::ServerCapabilities {
25401 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25402 ..lsp::ServerCapabilities::default()
25403 },
25404 name: "rust-analyzer",
25405 ..FakeLspAdapter::default()
25406 },
25407 );
25408 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25409 "Rust",
25410 FakeLspAdapter {
25411 capabilities: lsp::ServerCapabilities {
25412 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25413 ..lsp::ServerCapabilities::default()
25414 },
25415 name: "not-rust-analyzer",
25416 ..FakeLspAdapter::default()
25417 },
25418 );
25419
25420 let editor = workspace
25421 .update(cx, |workspace, window, cx| {
25422 workspace.open_abs_path(
25423 PathBuf::from(path!("/a/first.rs")),
25424 OpenOptions::default(),
25425 window,
25426 cx,
25427 )
25428 })
25429 .unwrap()
25430 .await
25431 .unwrap()
25432 .downcast::<Editor>()
25433 .unwrap();
25434 let fake_language_server = fake_servers.next().await.unwrap();
25435 let fake_language_server_without_capabilities =
25436 fake_servers_without_capabilities.next().await.unwrap();
25437 let requests_made = Arc::new(AtomicUsize::new(0));
25438 let closure_requests_made = Arc::clone(&requests_made);
25439 let mut color_request_handle = fake_language_server
25440 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25441 let requests_made = Arc::clone(&closure_requests_made);
25442 async move {
25443 assert_eq!(
25444 params.text_document.uri,
25445 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25446 );
25447 requests_made.fetch_add(1, atomic::Ordering::Release);
25448 Ok(vec![
25449 lsp::ColorInformation {
25450 range: lsp::Range {
25451 start: lsp::Position {
25452 line: 0,
25453 character: 0,
25454 },
25455 end: lsp::Position {
25456 line: 0,
25457 character: 1,
25458 },
25459 },
25460 color: lsp::Color {
25461 red: 0.33,
25462 green: 0.33,
25463 blue: 0.33,
25464 alpha: 0.33,
25465 },
25466 },
25467 lsp::ColorInformation {
25468 range: lsp::Range {
25469 start: lsp::Position {
25470 line: 0,
25471 character: 0,
25472 },
25473 end: lsp::Position {
25474 line: 0,
25475 character: 1,
25476 },
25477 },
25478 color: lsp::Color {
25479 red: 0.33,
25480 green: 0.33,
25481 blue: 0.33,
25482 alpha: 0.33,
25483 },
25484 },
25485 ])
25486 }
25487 });
25488
25489 let _handle = fake_language_server_without_capabilities
25490 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25491 panic!("Should not be called");
25492 });
25493 cx.executor().advance_clock(Duration::from_millis(100));
25494 color_request_handle.next().await.unwrap();
25495 cx.run_until_parked();
25496 assert_eq!(
25497 1,
25498 requests_made.load(atomic::Ordering::Acquire),
25499 "Should query for colors once per editor open"
25500 );
25501 editor.update_in(cx, |editor, _, cx| {
25502 assert_eq!(
25503 vec![expected_color],
25504 extract_color_inlays(editor, cx),
25505 "Should have an initial inlay"
25506 );
25507 });
25508
25509 // opening another file in a split should not influence the LSP query counter
25510 workspace
25511 .update(cx, |workspace, window, cx| {
25512 assert_eq!(
25513 workspace.panes().len(),
25514 1,
25515 "Should have one pane with one editor"
25516 );
25517 workspace.move_item_to_pane_in_direction(
25518 &MoveItemToPaneInDirection {
25519 direction: SplitDirection::Right,
25520 focus: false,
25521 clone: true,
25522 },
25523 window,
25524 cx,
25525 );
25526 })
25527 .unwrap();
25528 cx.run_until_parked();
25529 workspace
25530 .update(cx, |workspace, _, cx| {
25531 let panes = workspace.panes();
25532 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25533 for pane in panes {
25534 let editor = pane
25535 .read(cx)
25536 .active_item()
25537 .and_then(|item| item.downcast::<Editor>())
25538 .expect("Should have opened an editor in each split");
25539 let editor_file = editor
25540 .read(cx)
25541 .buffer()
25542 .read(cx)
25543 .as_singleton()
25544 .expect("test deals with singleton buffers")
25545 .read(cx)
25546 .file()
25547 .expect("test buffese should have a file")
25548 .path();
25549 assert_eq!(
25550 editor_file.as_ref(),
25551 Path::new("first.rs"),
25552 "Both editors should be opened for the same file"
25553 )
25554 }
25555 })
25556 .unwrap();
25557
25558 cx.executor().advance_clock(Duration::from_millis(500));
25559 let save = editor.update_in(cx, |editor, window, cx| {
25560 editor.move_to_end(&MoveToEnd, window, cx);
25561 editor.handle_input("dirty", window, cx);
25562 editor.save(
25563 SaveOptions {
25564 format: true,
25565 autosave: true,
25566 },
25567 project.clone(),
25568 window,
25569 cx,
25570 )
25571 });
25572 save.await.unwrap();
25573
25574 color_request_handle.next().await.unwrap();
25575 cx.run_until_parked();
25576 assert_eq!(
25577 3,
25578 requests_made.load(atomic::Ordering::Acquire),
25579 "Should query for colors once per save and once per formatting after save"
25580 );
25581
25582 drop(editor);
25583 let close = workspace
25584 .update(cx, |workspace, window, cx| {
25585 workspace.active_pane().update(cx, |pane, cx| {
25586 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25587 })
25588 })
25589 .unwrap();
25590 close.await.unwrap();
25591 let close = workspace
25592 .update(cx, |workspace, window, cx| {
25593 workspace.active_pane().update(cx, |pane, cx| {
25594 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25595 })
25596 })
25597 .unwrap();
25598 close.await.unwrap();
25599 assert_eq!(
25600 3,
25601 requests_made.load(atomic::Ordering::Acquire),
25602 "After saving and closing all editors, no extra requests should be made"
25603 );
25604 workspace
25605 .update(cx, |workspace, _, cx| {
25606 assert!(
25607 workspace.active_item(cx).is_none(),
25608 "Should close all editors"
25609 )
25610 })
25611 .unwrap();
25612
25613 workspace
25614 .update(cx, |workspace, window, cx| {
25615 workspace.active_pane().update(cx, |pane, cx| {
25616 pane.navigate_backward(&Default::default(), window, cx);
25617 })
25618 })
25619 .unwrap();
25620 cx.executor().advance_clock(Duration::from_millis(100));
25621 cx.run_until_parked();
25622 let editor = workspace
25623 .update(cx, |workspace, _, cx| {
25624 workspace
25625 .active_item(cx)
25626 .expect("Should have reopened the editor again after navigating back")
25627 .downcast::<Editor>()
25628 .expect("Should be an editor")
25629 })
25630 .unwrap();
25631 color_request_handle.next().await.unwrap();
25632 assert_eq!(
25633 3,
25634 requests_made.load(atomic::Ordering::Acquire),
25635 "Cache should be reused on buffer close and reopen"
25636 );
25637 editor.update(cx, |editor, cx| {
25638 assert_eq!(
25639 vec![expected_color],
25640 extract_color_inlays(editor, cx),
25641 "Should have an initial inlay"
25642 );
25643 });
25644}
25645
25646#[gpui::test]
25647async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25648 init_test(cx, |_| {});
25649 let (editor, cx) = cx.add_window_view(Editor::single_line);
25650 editor.update_in(cx, |editor, window, cx| {
25651 editor.set_text("oops\n\nwow\n", window, cx)
25652 });
25653 cx.run_until_parked();
25654 editor.update(cx, |editor, cx| {
25655 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25656 });
25657 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25658 cx.run_until_parked();
25659 editor.update(cx, |editor, cx| {
25660 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25661 });
25662}
25663
25664#[gpui::test]
25665async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25666 init_test(cx, |_| {});
25667
25668 cx.update(|cx| {
25669 register_project_item::<Editor>(cx);
25670 });
25671
25672 let fs = FakeFs::new(cx.executor());
25673 fs.insert_tree("/root1", json!({})).await;
25674 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25675 .await;
25676
25677 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25678 let (workspace, cx) =
25679 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25680
25681 let worktree_id = project.update(cx, |project, cx| {
25682 project.worktrees(cx).next().unwrap().read(cx).id()
25683 });
25684
25685 let handle = workspace
25686 .update_in(cx, |workspace, window, cx| {
25687 let project_path = (worktree_id, "one.pdf");
25688 workspace.open_path(project_path, None, true, window, cx)
25689 })
25690 .await
25691 .unwrap();
25692
25693 assert_eq!(
25694 handle.to_any().entity_type(),
25695 TypeId::of::<InvalidBufferView>()
25696 );
25697}
25698
25699#[gpui::test]
25700async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25701 init_test(cx, |_| {});
25702
25703 let language = Arc::new(Language::new(
25704 LanguageConfig::default(),
25705 Some(tree_sitter_rust::LANGUAGE.into()),
25706 ));
25707
25708 // Test hierarchical sibling navigation
25709 let text = r#"
25710 fn outer() {
25711 if condition {
25712 let a = 1;
25713 }
25714 let b = 2;
25715 }
25716
25717 fn another() {
25718 let c = 3;
25719 }
25720 "#;
25721
25722 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25723 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25724 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25725
25726 // Wait for parsing to complete
25727 editor
25728 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25729 .await;
25730
25731 editor.update_in(cx, |editor, window, cx| {
25732 // Start by selecting "let a = 1;" inside the if block
25733 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25734 s.select_display_ranges([
25735 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25736 ]);
25737 });
25738
25739 let initial_selection = editor.selections.display_ranges(cx);
25740 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25741
25742 // Test select next sibling - should move up levels to find the next sibling
25743 // Since "let a = 1;" has no siblings in the if block, it should move up
25744 // to find "let b = 2;" which is a sibling of the if block
25745 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25746 let next_selection = editor.selections.display_ranges(cx);
25747
25748 // Should have a selection and it should be different from the initial
25749 assert_eq!(
25750 next_selection.len(),
25751 1,
25752 "Should have one selection after next"
25753 );
25754 assert_ne!(
25755 next_selection[0], initial_selection[0],
25756 "Next sibling selection should be different"
25757 );
25758
25759 // Test hierarchical navigation by going to the end of the current function
25760 // and trying to navigate to the next function
25761 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25762 s.select_display_ranges([
25763 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25764 ]);
25765 });
25766
25767 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25768 let function_next_selection = editor.selections.display_ranges(cx);
25769
25770 // Should move to the next function
25771 assert_eq!(
25772 function_next_selection.len(),
25773 1,
25774 "Should have one selection after function next"
25775 );
25776
25777 // Test select previous sibling navigation
25778 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25779 let prev_selection = editor.selections.display_ranges(cx);
25780
25781 // Should have a selection and it should be different
25782 assert_eq!(
25783 prev_selection.len(),
25784 1,
25785 "Should have one selection after prev"
25786 );
25787 assert_ne!(
25788 prev_selection[0], function_next_selection[0],
25789 "Previous sibling selection should be different from next"
25790 );
25791 });
25792}
25793
25794#[gpui::test]
25795async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
25796 init_test(cx, |_| {});
25797
25798 let mut cx = EditorTestContext::new(cx).await;
25799 cx.set_state(
25800 "let ˇvariable = 42;
25801let another = variable + 1;
25802let result = variable * 2;",
25803 );
25804
25805 // Set up document highlights manually (simulating LSP response)
25806 cx.update_editor(|editor, _window, cx| {
25807 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
25808
25809 // Create highlights for "variable" occurrences
25810 let highlight_ranges = [
25811 Point::new(0, 4)..Point::new(0, 12), // First "variable"
25812 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
25813 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
25814 ];
25815
25816 let anchor_ranges: Vec<_> = highlight_ranges
25817 .iter()
25818 .map(|range| range.clone().to_anchors(&buffer_snapshot))
25819 .collect();
25820
25821 editor.highlight_background::<DocumentHighlightRead>(
25822 &anchor_ranges,
25823 |theme| theme.colors().editor_document_highlight_read_background,
25824 cx,
25825 );
25826 });
25827
25828 // Go to next highlight - should move to second "variable"
25829 cx.update_editor(|editor, window, cx| {
25830 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25831 });
25832 cx.assert_editor_state(
25833 "let variable = 42;
25834let another = ˇvariable + 1;
25835let result = variable * 2;",
25836 );
25837
25838 // Go to next highlight - should move to third "variable"
25839 cx.update_editor(|editor, window, cx| {
25840 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25841 });
25842 cx.assert_editor_state(
25843 "let variable = 42;
25844let another = variable + 1;
25845let result = ˇvariable * 2;",
25846 );
25847
25848 // Go to next highlight - should stay at third "variable" (no wrap-around)
25849 cx.update_editor(|editor, window, cx| {
25850 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25851 });
25852 cx.assert_editor_state(
25853 "let variable = 42;
25854let another = variable + 1;
25855let result = ˇvariable * 2;",
25856 );
25857
25858 // Now test going backwards from third position
25859 cx.update_editor(|editor, window, cx| {
25860 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25861 });
25862 cx.assert_editor_state(
25863 "let variable = 42;
25864let another = ˇvariable + 1;
25865let result = variable * 2;",
25866 );
25867
25868 // Go to previous highlight - should move to first "variable"
25869 cx.update_editor(|editor, window, cx| {
25870 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25871 });
25872 cx.assert_editor_state(
25873 "let ˇvariable = 42;
25874let another = variable + 1;
25875let result = variable * 2;",
25876 );
25877
25878 // Go to previous highlight - should stay on first "variable"
25879 cx.update_editor(|editor, window, cx| {
25880 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25881 });
25882 cx.assert_editor_state(
25883 "let ˇvariable = 42;
25884let another = variable + 1;
25885let result = variable * 2;",
25886 );
25887}
25888
25889#[track_caller]
25890fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
25891 editor
25892 .all_inlays(cx)
25893 .into_iter()
25894 .filter_map(|inlay| inlay.get_color())
25895 .map(Rgba::from)
25896 .collect()
25897}