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, LanguageConfig, LanguageConfigOverride, LanguageMatcher,
26 LanguageName, Override, Point,
27 language_settings::{
28 CompletionSettings, FormatterList, LanguageSettingsContent, LspInsertMode,
29 SelectedFormatter,
30 },
31 tree_sitter_python,
32};
33use language_settings::{Formatter, IndentGuideSettings};
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 = 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 = 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(CompletionSettings {
13345 lsp_insert_mode,
13346 words: WordsCompletionMode::Disabled,
13347 words_min_length: 0,
13348 lsp: true,
13349 lsp_fetch_timeout_ms: 0,
13350 });
13351 });
13352
13353 cx.set_state(&run.initial_state);
13354 cx.update_editor(|editor, window, cx| {
13355 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13356 });
13357
13358 let counter = Arc::new(AtomicUsize::new(0));
13359 handle_completion_request_with_insert_and_replace(
13360 &mut cx,
13361 &run.buffer_marked_text,
13362 vec![(run.completion_label, run.completion_text)],
13363 counter.clone(),
13364 )
13365 .await;
13366 cx.condition(|editor, _| editor.context_menu_visible())
13367 .await;
13368 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13369
13370 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13371 editor
13372 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13373 .unwrap()
13374 });
13375 cx.assert_editor_state(&expected_text);
13376 handle_resolve_completion_request(&mut cx, None).await;
13377 apply_additional_edits.await.unwrap();
13378 }
13379 }
13380}
13381
13382#[gpui::test]
13383async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13384 init_test(cx, |_| {});
13385 let mut cx = EditorLspTestContext::new_rust(
13386 lsp::ServerCapabilities {
13387 completion_provider: Some(lsp::CompletionOptions {
13388 resolve_provider: Some(true),
13389 ..Default::default()
13390 }),
13391 ..Default::default()
13392 },
13393 cx,
13394 )
13395 .await;
13396
13397 let initial_state = "SubˇError";
13398 let buffer_marked_text = "<Sub|Error>";
13399 let completion_text = "SubscriptionError";
13400 let expected_with_insert_mode = "SubscriptionErrorˇError";
13401 let expected_with_replace_mode = "SubscriptionErrorˇ";
13402
13403 update_test_language_settings(&mut cx, |settings| {
13404 settings.defaults.completions = Some(CompletionSettings {
13405 words: WordsCompletionMode::Disabled,
13406 words_min_length: 0,
13407 // set the opposite here to ensure that the action is overriding the default behavior
13408 lsp_insert_mode: LspInsertMode::Insert,
13409 lsp: true,
13410 lsp_fetch_timeout_ms: 0,
13411 });
13412 });
13413
13414 cx.set_state(initial_state);
13415 cx.update_editor(|editor, window, cx| {
13416 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13417 });
13418
13419 let counter = Arc::new(AtomicUsize::new(0));
13420 handle_completion_request_with_insert_and_replace(
13421 &mut cx,
13422 buffer_marked_text,
13423 vec![(completion_text, completion_text)],
13424 counter.clone(),
13425 )
13426 .await;
13427 cx.condition(|editor, _| editor.context_menu_visible())
13428 .await;
13429 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13430
13431 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13432 editor
13433 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13434 .unwrap()
13435 });
13436 cx.assert_editor_state(expected_with_replace_mode);
13437 handle_resolve_completion_request(&mut cx, None).await;
13438 apply_additional_edits.await.unwrap();
13439
13440 update_test_language_settings(&mut cx, |settings| {
13441 settings.defaults.completions = Some(CompletionSettings {
13442 words: WordsCompletionMode::Disabled,
13443 words_min_length: 0,
13444 // set the opposite here to ensure that the action is overriding the default behavior
13445 lsp_insert_mode: LspInsertMode::Replace,
13446 lsp: true,
13447 lsp_fetch_timeout_ms: 0,
13448 });
13449 });
13450
13451 cx.set_state(initial_state);
13452 cx.update_editor(|editor, window, cx| {
13453 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13454 });
13455 handle_completion_request_with_insert_and_replace(
13456 &mut cx,
13457 buffer_marked_text,
13458 vec![(completion_text, completion_text)],
13459 counter.clone(),
13460 )
13461 .await;
13462 cx.condition(|editor, _| editor.context_menu_visible())
13463 .await;
13464 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13465
13466 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13467 editor
13468 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13469 .unwrap()
13470 });
13471 cx.assert_editor_state(expected_with_insert_mode);
13472 handle_resolve_completion_request(&mut cx, None).await;
13473 apply_additional_edits.await.unwrap();
13474}
13475
13476#[gpui::test]
13477async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13478 init_test(cx, |_| {});
13479 let mut cx = EditorLspTestContext::new_rust(
13480 lsp::ServerCapabilities {
13481 completion_provider: Some(lsp::CompletionOptions {
13482 resolve_provider: Some(true),
13483 ..Default::default()
13484 }),
13485 ..Default::default()
13486 },
13487 cx,
13488 )
13489 .await;
13490
13491 // scenario: surrounding text matches completion text
13492 let completion_text = "to_offset";
13493 let initial_state = indoc! {"
13494 1. buf.to_offˇsuffix
13495 2. buf.to_offˇsuf
13496 3. buf.to_offˇfix
13497 4. buf.to_offˇ
13498 5. into_offˇensive
13499 6. ˇsuffix
13500 7. let ˇ //
13501 8. aaˇzz
13502 9. buf.to_off«zzzzzˇ»suffix
13503 10. buf.«ˇzzzzz»suffix
13504 11. to_off«ˇzzzzz»
13505
13506 buf.to_offˇsuffix // newest cursor
13507 "};
13508 let completion_marked_buffer = indoc! {"
13509 1. buf.to_offsuffix
13510 2. buf.to_offsuf
13511 3. buf.to_offfix
13512 4. buf.to_off
13513 5. into_offensive
13514 6. suffix
13515 7. let //
13516 8. aazz
13517 9. buf.to_offzzzzzsuffix
13518 10. buf.zzzzzsuffix
13519 11. to_offzzzzz
13520
13521 buf.<to_off|suffix> // newest cursor
13522 "};
13523 let expected = indoc! {"
13524 1. buf.to_offsetˇ
13525 2. buf.to_offsetˇsuf
13526 3. buf.to_offsetˇfix
13527 4. buf.to_offsetˇ
13528 5. into_offsetˇensive
13529 6. to_offsetˇsuffix
13530 7. let to_offsetˇ //
13531 8. aato_offsetˇzz
13532 9. buf.to_offsetˇ
13533 10. buf.to_offsetˇsuffix
13534 11. to_offsetˇ
13535
13536 buf.to_offsetˇ // newest cursor
13537 "};
13538 cx.set_state(initial_state);
13539 cx.update_editor(|editor, window, cx| {
13540 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13541 });
13542 handle_completion_request_with_insert_and_replace(
13543 &mut cx,
13544 completion_marked_buffer,
13545 vec![(completion_text, completion_text)],
13546 Arc::new(AtomicUsize::new(0)),
13547 )
13548 .await;
13549 cx.condition(|editor, _| editor.context_menu_visible())
13550 .await;
13551 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13552 editor
13553 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13554 .unwrap()
13555 });
13556 cx.assert_editor_state(expected);
13557 handle_resolve_completion_request(&mut cx, None).await;
13558 apply_additional_edits.await.unwrap();
13559
13560 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13561 let completion_text = "foo_and_bar";
13562 let initial_state = indoc! {"
13563 1. ooanbˇ
13564 2. zooanbˇ
13565 3. ooanbˇz
13566 4. zooanbˇz
13567 5. ooanˇ
13568 6. oanbˇ
13569
13570 ooanbˇ
13571 "};
13572 let completion_marked_buffer = indoc! {"
13573 1. ooanb
13574 2. zooanb
13575 3. ooanbz
13576 4. zooanbz
13577 5. ooan
13578 6. oanb
13579
13580 <ooanb|>
13581 "};
13582 let expected = indoc! {"
13583 1. foo_and_barˇ
13584 2. zfoo_and_barˇ
13585 3. foo_and_barˇz
13586 4. zfoo_and_barˇz
13587 5. ooanfoo_and_barˇ
13588 6. oanbfoo_and_barˇ
13589
13590 foo_and_barˇ
13591 "};
13592 cx.set_state(initial_state);
13593 cx.update_editor(|editor, window, cx| {
13594 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13595 });
13596 handle_completion_request_with_insert_and_replace(
13597 &mut cx,
13598 completion_marked_buffer,
13599 vec![(completion_text, completion_text)],
13600 Arc::new(AtomicUsize::new(0)),
13601 )
13602 .await;
13603 cx.condition(|editor, _| editor.context_menu_visible())
13604 .await;
13605 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13606 editor
13607 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13608 .unwrap()
13609 });
13610 cx.assert_editor_state(expected);
13611 handle_resolve_completion_request(&mut cx, None).await;
13612 apply_additional_edits.await.unwrap();
13613
13614 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13615 // (expects the same as if it was inserted at the end)
13616 let completion_text = "foo_and_bar";
13617 let initial_state = indoc! {"
13618 1. ooˇanb
13619 2. zooˇanb
13620 3. ooˇanbz
13621 4. zooˇanbz
13622
13623 ooˇanb
13624 "};
13625 let completion_marked_buffer = indoc! {"
13626 1. ooanb
13627 2. zooanb
13628 3. ooanbz
13629 4. zooanbz
13630
13631 <oo|anb>
13632 "};
13633 let expected = indoc! {"
13634 1. foo_and_barˇ
13635 2. zfoo_and_barˇ
13636 3. foo_and_barˇz
13637 4. zfoo_and_barˇz
13638
13639 foo_and_barˇ
13640 "};
13641 cx.set_state(initial_state);
13642 cx.update_editor(|editor, window, cx| {
13643 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13644 });
13645 handle_completion_request_with_insert_and_replace(
13646 &mut cx,
13647 completion_marked_buffer,
13648 vec![(completion_text, completion_text)],
13649 Arc::new(AtomicUsize::new(0)),
13650 )
13651 .await;
13652 cx.condition(|editor, _| editor.context_menu_visible())
13653 .await;
13654 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13655 editor
13656 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13657 .unwrap()
13658 });
13659 cx.assert_editor_state(expected);
13660 handle_resolve_completion_request(&mut cx, None).await;
13661 apply_additional_edits.await.unwrap();
13662}
13663
13664// This used to crash
13665#[gpui::test]
13666async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13667 init_test(cx, |_| {});
13668
13669 let buffer_text = indoc! {"
13670 fn main() {
13671 10.satu;
13672
13673 //
13674 // separate cursors so they open in different excerpts (manually reproducible)
13675 //
13676
13677 10.satu20;
13678 }
13679 "};
13680 let multibuffer_text_with_selections = indoc! {"
13681 fn main() {
13682 10.satuˇ;
13683
13684 //
13685
13686 //
13687
13688 10.satuˇ20;
13689 }
13690 "};
13691 let expected_multibuffer = indoc! {"
13692 fn main() {
13693 10.saturating_sub()ˇ;
13694
13695 //
13696
13697 //
13698
13699 10.saturating_sub()ˇ;
13700 }
13701 "};
13702
13703 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13704 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13705
13706 let fs = FakeFs::new(cx.executor());
13707 fs.insert_tree(
13708 path!("/a"),
13709 json!({
13710 "main.rs": buffer_text,
13711 }),
13712 )
13713 .await;
13714
13715 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13716 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13717 language_registry.add(rust_lang());
13718 let mut fake_servers = language_registry.register_fake_lsp(
13719 "Rust",
13720 FakeLspAdapter {
13721 capabilities: lsp::ServerCapabilities {
13722 completion_provider: Some(lsp::CompletionOptions {
13723 resolve_provider: None,
13724 ..lsp::CompletionOptions::default()
13725 }),
13726 ..lsp::ServerCapabilities::default()
13727 },
13728 ..FakeLspAdapter::default()
13729 },
13730 );
13731 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13732 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13733 let buffer = project
13734 .update(cx, |project, cx| {
13735 project.open_local_buffer(path!("/a/main.rs"), cx)
13736 })
13737 .await
13738 .unwrap();
13739
13740 let multi_buffer = cx.new(|cx| {
13741 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13742 multi_buffer.push_excerpts(
13743 buffer.clone(),
13744 [ExcerptRange::new(0..first_excerpt_end)],
13745 cx,
13746 );
13747 multi_buffer.push_excerpts(
13748 buffer.clone(),
13749 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13750 cx,
13751 );
13752 multi_buffer
13753 });
13754
13755 let editor = workspace
13756 .update(cx, |_, window, cx| {
13757 cx.new(|cx| {
13758 Editor::new(
13759 EditorMode::Full {
13760 scale_ui_elements_with_buffer_font_size: false,
13761 show_active_line_background: false,
13762 sized_by_content: false,
13763 },
13764 multi_buffer.clone(),
13765 Some(project.clone()),
13766 window,
13767 cx,
13768 )
13769 })
13770 })
13771 .unwrap();
13772
13773 let pane = workspace
13774 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13775 .unwrap();
13776 pane.update_in(cx, |pane, window, cx| {
13777 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13778 });
13779
13780 let fake_server = fake_servers.next().await.unwrap();
13781
13782 editor.update_in(cx, |editor, window, cx| {
13783 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13784 s.select_ranges([
13785 Point::new(1, 11)..Point::new(1, 11),
13786 Point::new(7, 11)..Point::new(7, 11),
13787 ])
13788 });
13789
13790 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13791 });
13792
13793 editor.update_in(cx, |editor, window, cx| {
13794 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13795 });
13796
13797 fake_server
13798 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13799 let completion_item = lsp::CompletionItem {
13800 label: "saturating_sub()".into(),
13801 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13802 lsp::InsertReplaceEdit {
13803 new_text: "saturating_sub()".to_owned(),
13804 insert: lsp::Range::new(
13805 lsp::Position::new(7, 7),
13806 lsp::Position::new(7, 11),
13807 ),
13808 replace: lsp::Range::new(
13809 lsp::Position::new(7, 7),
13810 lsp::Position::new(7, 13),
13811 ),
13812 },
13813 )),
13814 ..lsp::CompletionItem::default()
13815 };
13816
13817 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13818 })
13819 .next()
13820 .await
13821 .unwrap();
13822
13823 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13824 .await;
13825
13826 editor
13827 .update_in(cx, |editor, window, cx| {
13828 editor
13829 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13830 .unwrap()
13831 })
13832 .await
13833 .unwrap();
13834
13835 editor.update(cx, |editor, cx| {
13836 assert_text_with_selections(editor, expected_multibuffer, cx);
13837 })
13838}
13839
13840#[gpui::test]
13841async fn test_completion(cx: &mut TestAppContext) {
13842 init_test(cx, |_| {});
13843
13844 let mut cx = EditorLspTestContext::new_rust(
13845 lsp::ServerCapabilities {
13846 completion_provider: Some(lsp::CompletionOptions {
13847 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13848 resolve_provider: Some(true),
13849 ..Default::default()
13850 }),
13851 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13852 ..Default::default()
13853 },
13854 cx,
13855 )
13856 .await;
13857 let counter = Arc::new(AtomicUsize::new(0));
13858
13859 cx.set_state(indoc! {"
13860 oneˇ
13861 two
13862 three
13863 "});
13864 cx.simulate_keystroke(".");
13865 handle_completion_request(
13866 indoc! {"
13867 one.|<>
13868 two
13869 three
13870 "},
13871 vec!["first_completion", "second_completion"],
13872 true,
13873 counter.clone(),
13874 &mut cx,
13875 )
13876 .await;
13877 cx.condition(|editor, _| editor.context_menu_visible())
13878 .await;
13879 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13880
13881 let _handler = handle_signature_help_request(
13882 &mut cx,
13883 lsp::SignatureHelp {
13884 signatures: vec![lsp::SignatureInformation {
13885 label: "test signature".to_string(),
13886 documentation: None,
13887 parameters: Some(vec![lsp::ParameterInformation {
13888 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13889 documentation: None,
13890 }]),
13891 active_parameter: None,
13892 }],
13893 active_signature: None,
13894 active_parameter: None,
13895 },
13896 );
13897 cx.update_editor(|editor, window, cx| {
13898 assert!(
13899 !editor.signature_help_state.is_shown(),
13900 "No signature help was called for"
13901 );
13902 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13903 });
13904 cx.run_until_parked();
13905 cx.update_editor(|editor, _, _| {
13906 assert!(
13907 !editor.signature_help_state.is_shown(),
13908 "No signature help should be shown when completions menu is open"
13909 );
13910 });
13911
13912 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13913 editor.context_menu_next(&Default::default(), window, cx);
13914 editor
13915 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13916 .unwrap()
13917 });
13918 cx.assert_editor_state(indoc! {"
13919 one.second_completionˇ
13920 two
13921 three
13922 "});
13923
13924 handle_resolve_completion_request(
13925 &mut cx,
13926 Some(vec![
13927 (
13928 //This overlaps with the primary completion edit which is
13929 //misbehavior from the LSP spec, test that we filter it out
13930 indoc! {"
13931 one.second_ˇcompletion
13932 two
13933 threeˇ
13934 "},
13935 "overlapping additional edit",
13936 ),
13937 (
13938 indoc! {"
13939 one.second_completion
13940 two
13941 threeˇ
13942 "},
13943 "\nadditional edit",
13944 ),
13945 ]),
13946 )
13947 .await;
13948 apply_additional_edits.await.unwrap();
13949 cx.assert_editor_state(indoc! {"
13950 one.second_completionˇ
13951 two
13952 three
13953 additional edit
13954 "});
13955
13956 cx.set_state(indoc! {"
13957 one.second_completion
13958 twoˇ
13959 threeˇ
13960 additional edit
13961 "});
13962 cx.simulate_keystroke(" ");
13963 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13964 cx.simulate_keystroke("s");
13965 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
13966
13967 cx.assert_editor_state(indoc! {"
13968 one.second_completion
13969 two sˇ
13970 three sˇ
13971 additional edit
13972 "});
13973 handle_completion_request(
13974 indoc! {"
13975 one.second_completion
13976 two s
13977 three <s|>
13978 additional edit
13979 "},
13980 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
13981 true,
13982 counter.clone(),
13983 &mut cx,
13984 )
13985 .await;
13986 cx.condition(|editor, _| editor.context_menu_visible())
13987 .await;
13988 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13989
13990 cx.simulate_keystroke("i");
13991
13992 handle_completion_request(
13993 indoc! {"
13994 one.second_completion
13995 two si
13996 three <si|>
13997 additional edit
13998 "},
13999 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14000 true,
14001 counter.clone(),
14002 &mut cx,
14003 )
14004 .await;
14005 cx.condition(|editor, _| editor.context_menu_visible())
14006 .await;
14007 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14008
14009 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14010 editor
14011 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14012 .unwrap()
14013 });
14014 cx.assert_editor_state(indoc! {"
14015 one.second_completion
14016 two sixth_completionˇ
14017 three sixth_completionˇ
14018 additional edit
14019 "});
14020
14021 apply_additional_edits.await.unwrap();
14022
14023 update_test_language_settings(&mut cx, |settings| {
14024 settings.defaults.show_completions_on_input = Some(false);
14025 });
14026 cx.set_state("editorˇ");
14027 cx.simulate_keystroke(".");
14028 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14029 cx.simulate_keystrokes("c l o");
14030 cx.assert_editor_state("editor.cloˇ");
14031 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14032 cx.update_editor(|editor, window, cx| {
14033 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14034 });
14035 handle_completion_request(
14036 "editor.<clo|>",
14037 vec!["close", "clobber"],
14038 true,
14039 counter.clone(),
14040 &mut cx,
14041 )
14042 .await;
14043 cx.condition(|editor, _| editor.context_menu_visible())
14044 .await;
14045 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14046
14047 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14048 editor
14049 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14050 .unwrap()
14051 });
14052 cx.assert_editor_state("editor.clobberˇ");
14053 handle_resolve_completion_request(&mut cx, None).await;
14054 apply_additional_edits.await.unwrap();
14055}
14056
14057#[gpui::test]
14058async fn test_completion_reuse(cx: &mut TestAppContext) {
14059 init_test(cx, |_| {});
14060
14061 let mut cx = EditorLspTestContext::new_rust(
14062 lsp::ServerCapabilities {
14063 completion_provider: Some(lsp::CompletionOptions {
14064 trigger_characters: Some(vec![".".to_string()]),
14065 ..Default::default()
14066 }),
14067 ..Default::default()
14068 },
14069 cx,
14070 )
14071 .await;
14072
14073 let counter = Arc::new(AtomicUsize::new(0));
14074 cx.set_state("objˇ");
14075 cx.simulate_keystroke(".");
14076
14077 // Initial completion request returns complete results
14078 let is_incomplete = false;
14079 handle_completion_request(
14080 "obj.|<>",
14081 vec!["a", "ab", "abc"],
14082 is_incomplete,
14083 counter.clone(),
14084 &mut cx,
14085 )
14086 .await;
14087 cx.run_until_parked();
14088 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14089 cx.assert_editor_state("obj.ˇ");
14090 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14091
14092 // Type "a" - filters existing completions
14093 cx.simulate_keystroke("a");
14094 cx.run_until_parked();
14095 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14096 cx.assert_editor_state("obj.aˇ");
14097 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14098
14099 // Type "b" - filters existing completions
14100 cx.simulate_keystroke("b");
14101 cx.run_until_parked();
14102 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14103 cx.assert_editor_state("obj.abˇ");
14104 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14105
14106 // Type "c" - filters existing completions
14107 cx.simulate_keystroke("c");
14108 cx.run_until_parked();
14109 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14110 cx.assert_editor_state("obj.abcˇ");
14111 check_displayed_completions(vec!["abc"], &mut cx);
14112
14113 // Backspace to delete "c" - filters existing completions
14114 cx.update_editor(|editor, window, cx| {
14115 editor.backspace(&Backspace, window, cx);
14116 });
14117 cx.run_until_parked();
14118 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14119 cx.assert_editor_state("obj.abˇ");
14120 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14121
14122 // Moving cursor to the left dismisses menu.
14123 cx.update_editor(|editor, window, cx| {
14124 editor.move_left(&MoveLeft, window, cx);
14125 });
14126 cx.run_until_parked();
14127 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14128 cx.assert_editor_state("obj.aˇb");
14129 cx.update_editor(|editor, _, _| {
14130 assert_eq!(editor.context_menu_visible(), false);
14131 });
14132
14133 // Type "b" - new request
14134 cx.simulate_keystroke("b");
14135 let is_incomplete = false;
14136 handle_completion_request(
14137 "obj.<ab|>a",
14138 vec!["ab", "abc"],
14139 is_incomplete,
14140 counter.clone(),
14141 &mut cx,
14142 )
14143 .await;
14144 cx.run_until_parked();
14145 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14146 cx.assert_editor_state("obj.abˇb");
14147 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14148
14149 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14150 cx.update_editor(|editor, window, cx| {
14151 editor.backspace(&Backspace, window, cx);
14152 });
14153 let is_incomplete = false;
14154 handle_completion_request(
14155 "obj.<a|>b",
14156 vec!["a", "ab", "abc"],
14157 is_incomplete,
14158 counter.clone(),
14159 &mut cx,
14160 )
14161 .await;
14162 cx.run_until_parked();
14163 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14164 cx.assert_editor_state("obj.aˇb");
14165 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14166
14167 // Backspace to delete "a" - dismisses menu.
14168 cx.update_editor(|editor, window, cx| {
14169 editor.backspace(&Backspace, window, cx);
14170 });
14171 cx.run_until_parked();
14172 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14173 cx.assert_editor_state("obj.ˇb");
14174 cx.update_editor(|editor, _, _| {
14175 assert_eq!(editor.context_menu_visible(), false);
14176 });
14177}
14178
14179#[gpui::test]
14180async fn test_word_completion(cx: &mut TestAppContext) {
14181 let lsp_fetch_timeout_ms = 10;
14182 init_test(cx, |language_settings| {
14183 language_settings.defaults.completions = Some(CompletionSettings {
14184 words: WordsCompletionMode::Fallback,
14185 words_min_length: 0,
14186 lsp: true,
14187 lsp_fetch_timeout_ms: 10,
14188 lsp_insert_mode: LspInsertMode::Insert,
14189 });
14190 });
14191
14192 let mut cx = EditorLspTestContext::new_rust(
14193 lsp::ServerCapabilities {
14194 completion_provider: Some(lsp::CompletionOptions {
14195 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14196 ..lsp::CompletionOptions::default()
14197 }),
14198 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14199 ..lsp::ServerCapabilities::default()
14200 },
14201 cx,
14202 )
14203 .await;
14204
14205 let throttle_completions = Arc::new(AtomicBool::new(false));
14206
14207 let lsp_throttle_completions = throttle_completions.clone();
14208 let _completion_requests_handler =
14209 cx.lsp
14210 .server
14211 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14212 let lsp_throttle_completions = lsp_throttle_completions.clone();
14213 let cx = cx.clone();
14214 async move {
14215 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14216 cx.background_executor()
14217 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14218 .await;
14219 }
14220 Ok(Some(lsp::CompletionResponse::Array(vec![
14221 lsp::CompletionItem {
14222 label: "first".into(),
14223 ..lsp::CompletionItem::default()
14224 },
14225 lsp::CompletionItem {
14226 label: "last".into(),
14227 ..lsp::CompletionItem::default()
14228 },
14229 ])))
14230 }
14231 });
14232
14233 cx.set_state(indoc! {"
14234 oneˇ
14235 two
14236 three
14237 "});
14238 cx.simulate_keystroke(".");
14239 cx.executor().run_until_parked();
14240 cx.condition(|editor, _| editor.context_menu_visible())
14241 .await;
14242 cx.update_editor(|editor, window, cx| {
14243 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14244 {
14245 assert_eq!(
14246 completion_menu_entries(menu),
14247 &["first", "last"],
14248 "When LSP server is fast to reply, no fallback word completions are used"
14249 );
14250 } else {
14251 panic!("expected completion menu to be open");
14252 }
14253 editor.cancel(&Cancel, window, cx);
14254 });
14255 cx.executor().run_until_parked();
14256 cx.condition(|editor, _| !editor.context_menu_visible())
14257 .await;
14258
14259 throttle_completions.store(true, atomic::Ordering::Release);
14260 cx.simulate_keystroke(".");
14261 cx.executor()
14262 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14263 cx.executor().run_until_parked();
14264 cx.condition(|editor, _| editor.context_menu_visible())
14265 .await;
14266 cx.update_editor(|editor, _, _| {
14267 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14268 {
14269 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14270 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14271 } else {
14272 panic!("expected completion menu to be open");
14273 }
14274 });
14275}
14276
14277#[gpui::test]
14278async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14279 init_test(cx, |language_settings| {
14280 language_settings.defaults.completions = Some(CompletionSettings {
14281 words: WordsCompletionMode::Enabled,
14282 words_min_length: 0,
14283 lsp: true,
14284 lsp_fetch_timeout_ms: 0,
14285 lsp_insert_mode: LspInsertMode::Insert,
14286 });
14287 });
14288
14289 let mut cx = EditorLspTestContext::new_rust(
14290 lsp::ServerCapabilities {
14291 completion_provider: Some(lsp::CompletionOptions {
14292 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14293 ..lsp::CompletionOptions::default()
14294 }),
14295 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14296 ..lsp::ServerCapabilities::default()
14297 },
14298 cx,
14299 )
14300 .await;
14301
14302 let _completion_requests_handler =
14303 cx.lsp
14304 .server
14305 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14306 Ok(Some(lsp::CompletionResponse::Array(vec![
14307 lsp::CompletionItem {
14308 label: "first".into(),
14309 ..lsp::CompletionItem::default()
14310 },
14311 lsp::CompletionItem {
14312 label: "last".into(),
14313 ..lsp::CompletionItem::default()
14314 },
14315 ])))
14316 });
14317
14318 cx.set_state(indoc! {"ˇ
14319 first
14320 last
14321 second
14322 "});
14323 cx.simulate_keystroke(".");
14324 cx.executor().run_until_parked();
14325 cx.condition(|editor, _| editor.context_menu_visible())
14326 .await;
14327 cx.update_editor(|editor, _, _| {
14328 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14329 {
14330 assert_eq!(
14331 completion_menu_entries(menu),
14332 &["first", "last", "second"],
14333 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14334 );
14335 } else {
14336 panic!("expected completion menu to be open");
14337 }
14338 });
14339}
14340
14341#[gpui::test]
14342async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14343 init_test(cx, |language_settings| {
14344 language_settings.defaults.completions = Some(CompletionSettings {
14345 words: WordsCompletionMode::Disabled,
14346 words_min_length: 0,
14347 lsp: true,
14348 lsp_fetch_timeout_ms: 0,
14349 lsp_insert_mode: LspInsertMode::Insert,
14350 });
14351 });
14352
14353 let mut cx = EditorLspTestContext::new_rust(
14354 lsp::ServerCapabilities {
14355 completion_provider: Some(lsp::CompletionOptions {
14356 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14357 ..lsp::CompletionOptions::default()
14358 }),
14359 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14360 ..lsp::ServerCapabilities::default()
14361 },
14362 cx,
14363 )
14364 .await;
14365
14366 let _completion_requests_handler =
14367 cx.lsp
14368 .server
14369 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14370 panic!("LSP completions should not be queried when dealing with word completions")
14371 });
14372
14373 cx.set_state(indoc! {"ˇ
14374 first
14375 last
14376 second
14377 "});
14378 cx.update_editor(|editor, window, cx| {
14379 editor.show_word_completions(&ShowWordCompletions, window, cx);
14380 });
14381 cx.executor().run_until_parked();
14382 cx.condition(|editor, _| editor.context_menu_visible())
14383 .await;
14384 cx.update_editor(|editor, _, _| {
14385 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14386 {
14387 assert_eq!(
14388 completion_menu_entries(menu),
14389 &["first", "last", "second"],
14390 "`ShowWordCompletions` action should show word completions"
14391 );
14392 } else {
14393 panic!("expected completion menu to be open");
14394 }
14395 });
14396
14397 cx.simulate_keystroke("l");
14398 cx.executor().run_until_parked();
14399 cx.condition(|editor, _| editor.context_menu_visible())
14400 .await;
14401 cx.update_editor(|editor, _, _| {
14402 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14403 {
14404 assert_eq!(
14405 completion_menu_entries(menu),
14406 &["last"],
14407 "After showing word completions, further editing should filter them and not query the LSP"
14408 );
14409 } else {
14410 panic!("expected completion menu to be open");
14411 }
14412 });
14413}
14414
14415#[gpui::test]
14416async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14417 init_test(cx, |language_settings| {
14418 language_settings.defaults.completions = Some(CompletionSettings {
14419 words: WordsCompletionMode::Fallback,
14420 words_min_length: 0,
14421 lsp: false,
14422 lsp_fetch_timeout_ms: 0,
14423 lsp_insert_mode: LspInsertMode::Insert,
14424 });
14425 });
14426
14427 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14428
14429 cx.set_state(indoc! {"ˇ
14430 0_usize
14431 let
14432 33
14433 4.5f32
14434 "});
14435 cx.update_editor(|editor, window, cx| {
14436 editor.show_completions(&ShowCompletions::default(), window, cx);
14437 });
14438 cx.executor().run_until_parked();
14439 cx.condition(|editor, _| editor.context_menu_visible())
14440 .await;
14441 cx.update_editor(|editor, window, cx| {
14442 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14443 {
14444 assert_eq!(
14445 completion_menu_entries(menu),
14446 &["let"],
14447 "With no digits in the completion query, no digits should be in the word completions"
14448 );
14449 } else {
14450 panic!("expected completion menu to be open");
14451 }
14452 editor.cancel(&Cancel, window, cx);
14453 });
14454
14455 cx.set_state(indoc! {"3ˇ
14456 0_usize
14457 let
14458 3
14459 33.35f32
14460 "});
14461 cx.update_editor(|editor, window, cx| {
14462 editor.show_completions(&ShowCompletions::default(), window, cx);
14463 });
14464 cx.executor().run_until_parked();
14465 cx.condition(|editor, _| editor.context_menu_visible())
14466 .await;
14467 cx.update_editor(|editor, _, _| {
14468 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14469 {
14470 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14471 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14472 } else {
14473 panic!("expected completion menu to be open");
14474 }
14475 });
14476}
14477
14478#[gpui::test]
14479async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14480 init_test(cx, |language_settings| {
14481 language_settings.defaults.completions = Some(CompletionSettings {
14482 words: WordsCompletionMode::Enabled,
14483 words_min_length: 3,
14484 lsp: true,
14485 lsp_fetch_timeout_ms: 0,
14486 lsp_insert_mode: LspInsertMode::Insert,
14487 });
14488 });
14489
14490 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14491 cx.set_state(indoc! {"ˇ
14492 wow
14493 wowen
14494 wowser
14495 "});
14496 cx.simulate_keystroke("w");
14497 cx.executor().run_until_parked();
14498 cx.update_editor(|editor, _, _| {
14499 if editor.context_menu.borrow_mut().is_some() {
14500 panic!(
14501 "expected completion menu to be hidden, as words completion threshold is not met"
14502 );
14503 }
14504 });
14505
14506 cx.update_editor(|editor, window, cx| {
14507 editor.show_word_completions(&ShowWordCompletions, window, cx);
14508 });
14509 cx.executor().run_until_parked();
14510 cx.update_editor(|editor, window, cx| {
14511 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14512 {
14513 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");
14514 } else {
14515 panic!("expected completion menu to be open after the word completions are called with an action");
14516 }
14517
14518 editor.cancel(&Cancel, window, cx);
14519 });
14520 cx.update_editor(|editor, _, _| {
14521 if editor.context_menu.borrow_mut().is_some() {
14522 panic!("expected completion menu to be hidden after canceling");
14523 }
14524 });
14525
14526 cx.simulate_keystroke("o");
14527 cx.executor().run_until_parked();
14528 cx.update_editor(|editor, _, _| {
14529 if editor.context_menu.borrow_mut().is_some() {
14530 panic!(
14531 "expected completion menu to be hidden, as words completion threshold is not met still"
14532 );
14533 }
14534 });
14535
14536 cx.simulate_keystroke("w");
14537 cx.executor().run_until_parked();
14538 cx.update_editor(|editor, _, _| {
14539 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14540 {
14541 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14542 } else {
14543 panic!("expected completion menu to be open after the word completions threshold is met");
14544 }
14545 });
14546}
14547
14548#[gpui::test]
14549async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14550 init_test(cx, |language_settings| {
14551 language_settings.defaults.completions = Some(CompletionSettings {
14552 words: WordsCompletionMode::Enabled,
14553 words_min_length: 0,
14554 lsp: true,
14555 lsp_fetch_timeout_ms: 0,
14556 lsp_insert_mode: LspInsertMode::Insert,
14557 });
14558 });
14559
14560 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14561 cx.update_editor(|editor, _, _| {
14562 editor.disable_word_completions();
14563 });
14564 cx.set_state(indoc! {"ˇ
14565 wow
14566 wowen
14567 wowser
14568 "});
14569 cx.simulate_keystroke("w");
14570 cx.executor().run_until_parked();
14571 cx.update_editor(|editor, _, _| {
14572 if editor.context_menu.borrow_mut().is_some() {
14573 panic!(
14574 "expected completion menu to be hidden, as words completion are disabled for this editor"
14575 );
14576 }
14577 });
14578
14579 cx.update_editor(|editor, window, cx| {
14580 editor.show_word_completions(&ShowWordCompletions, window, cx);
14581 });
14582 cx.executor().run_until_parked();
14583 cx.update_editor(|editor, _, _| {
14584 if editor.context_menu.borrow_mut().is_some() {
14585 panic!(
14586 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14587 );
14588 }
14589 });
14590}
14591
14592fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14593 let position = || lsp::Position {
14594 line: params.text_document_position.position.line,
14595 character: params.text_document_position.position.character,
14596 };
14597 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14598 range: lsp::Range {
14599 start: position(),
14600 end: position(),
14601 },
14602 new_text: text.to_string(),
14603 }))
14604}
14605
14606#[gpui::test]
14607async fn test_multiline_completion(cx: &mut TestAppContext) {
14608 init_test(cx, |_| {});
14609
14610 let fs = FakeFs::new(cx.executor());
14611 fs.insert_tree(
14612 path!("/a"),
14613 json!({
14614 "main.ts": "a",
14615 }),
14616 )
14617 .await;
14618
14619 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14620 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14621 let typescript_language = Arc::new(Language::new(
14622 LanguageConfig {
14623 name: "TypeScript".into(),
14624 matcher: LanguageMatcher {
14625 path_suffixes: vec!["ts".to_string()],
14626 ..LanguageMatcher::default()
14627 },
14628 line_comments: vec!["// ".into()],
14629 ..LanguageConfig::default()
14630 },
14631 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14632 ));
14633 language_registry.add(typescript_language.clone());
14634 let mut fake_servers = language_registry.register_fake_lsp(
14635 "TypeScript",
14636 FakeLspAdapter {
14637 capabilities: lsp::ServerCapabilities {
14638 completion_provider: Some(lsp::CompletionOptions {
14639 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14640 ..lsp::CompletionOptions::default()
14641 }),
14642 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14643 ..lsp::ServerCapabilities::default()
14644 },
14645 // Emulate vtsls label generation
14646 label_for_completion: Some(Box::new(|item, _| {
14647 let text = if let Some(description) = item
14648 .label_details
14649 .as_ref()
14650 .and_then(|label_details| label_details.description.as_ref())
14651 {
14652 format!("{} {}", item.label, description)
14653 } else if let Some(detail) = &item.detail {
14654 format!("{} {}", item.label, detail)
14655 } else {
14656 item.label.clone()
14657 };
14658 let len = text.len();
14659 Some(language::CodeLabel {
14660 text,
14661 runs: Vec::new(),
14662 filter_range: 0..len,
14663 })
14664 })),
14665 ..FakeLspAdapter::default()
14666 },
14667 );
14668 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14669 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14670 let worktree_id = workspace
14671 .update(cx, |workspace, _window, cx| {
14672 workspace.project().update(cx, |project, cx| {
14673 project.worktrees(cx).next().unwrap().read(cx).id()
14674 })
14675 })
14676 .unwrap();
14677 let _buffer = project
14678 .update(cx, |project, cx| {
14679 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14680 })
14681 .await
14682 .unwrap();
14683 let editor = workspace
14684 .update(cx, |workspace, window, cx| {
14685 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14686 })
14687 .unwrap()
14688 .await
14689 .unwrap()
14690 .downcast::<Editor>()
14691 .unwrap();
14692 let fake_server = fake_servers.next().await.unwrap();
14693
14694 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14695 let multiline_label_2 = "a\nb\nc\n";
14696 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14697 let multiline_description = "d\ne\nf\n";
14698 let multiline_detail_2 = "g\nh\ni\n";
14699
14700 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14701 move |params, _| async move {
14702 Ok(Some(lsp::CompletionResponse::Array(vec![
14703 lsp::CompletionItem {
14704 label: multiline_label.to_string(),
14705 text_edit: gen_text_edit(¶ms, "new_text_1"),
14706 ..lsp::CompletionItem::default()
14707 },
14708 lsp::CompletionItem {
14709 label: "single line label 1".to_string(),
14710 detail: Some(multiline_detail.to_string()),
14711 text_edit: gen_text_edit(¶ms, "new_text_2"),
14712 ..lsp::CompletionItem::default()
14713 },
14714 lsp::CompletionItem {
14715 label: "single line label 2".to_string(),
14716 label_details: Some(lsp::CompletionItemLabelDetails {
14717 description: Some(multiline_description.to_string()),
14718 detail: None,
14719 }),
14720 text_edit: gen_text_edit(¶ms, "new_text_2"),
14721 ..lsp::CompletionItem::default()
14722 },
14723 lsp::CompletionItem {
14724 label: multiline_label_2.to_string(),
14725 detail: Some(multiline_detail_2.to_string()),
14726 text_edit: gen_text_edit(¶ms, "new_text_3"),
14727 ..lsp::CompletionItem::default()
14728 },
14729 lsp::CompletionItem {
14730 label: "Label with many spaces and \t but without newlines".to_string(),
14731 detail: Some(
14732 "Details with many spaces and \t but without newlines".to_string(),
14733 ),
14734 text_edit: gen_text_edit(¶ms, "new_text_4"),
14735 ..lsp::CompletionItem::default()
14736 },
14737 ])))
14738 },
14739 );
14740
14741 editor.update_in(cx, |editor, window, cx| {
14742 cx.focus_self(window);
14743 editor.move_to_end(&MoveToEnd, window, cx);
14744 editor.handle_input(".", window, cx);
14745 });
14746 cx.run_until_parked();
14747 completion_handle.next().await.unwrap();
14748
14749 editor.update(cx, |editor, _| {
14750 assert!(editor.context_menu_visible());
14751 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14752 {
14753 let completion_labels = menu
14754 .completions
14755 .borrow()
14756 .iter()
14757 .map(|c| c.label.text.clone())
14758 .collect::<Vec<_>>();
14759 assert_eq!(
14760 completion_labels,
14761 &[
14762 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14763 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14764 "single line label 2 d e f ",
14765 "a b c g h i ",
14766 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14767 ],
14768 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14769 );
14770
14771 for completion in menu
14772 .completions
14773 .borrow()
14774 .iter() {
14775 assert_eq!(
14776 completion.label.filter_range,
14777 0..completion.label.text.len(),
14778 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14779 );
14780 }
14781 } else {
14782 panic!("expected completion menu to be open");
14783 }
14784 });
14785}
14786
14787#[gpui::test]
14788async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14789 init_test(cx, |_| {});
14790 let mut cx = EditorLspTestContext::new_rust(
14791 lsp::ServerCapabilities {
14792 completion_provider: Some(lsp::CompletionOptions {
14793 trigger_characters: Some(vec![".".to_string()]),
14794 ..Default::default()
14795 }),
14796 ..Default::default()
14797 },
14798 cx,
14799 )
14800 .await;
14801 cx.lsp
14802 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14803 Ok(Some(lsp::CompletionResponse::Array(vec![
14804 lsp::CompletionItem {
14805 label: "first".into(),
14806 ..Default::default()
14807 },
14808 lsp::CompletionItem {
14809 label: "last".into(),
14810 ..Default::default()
14811 },
14812 ])))
14813 });
14814 cx.set_state("variableˇ");
14815 cx.simulate_keystroke(".");
14816 cx.executor().run_until_parked();
14817
14818 cx.update_editor(|editor, _, _| {
14819 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14820 {
14821 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14822 } else {
14823 panic!("expected completion menu to be open");
14824 }
14825 });
14826
14827 cx.update_editor(|editor, window, cx| {
14828 editor.move_page_down(&MovePageDown::default(), window, cx);
14829 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14830 {
14831 assert!(
14832 menu.selected_item == 1,
14833 "expected PageDown to select the last item from the context menu"
14834 );
14835 } else {
14836 panic!("expected completion menu to stay open after PageDown");
14837 }
14838 });
14839
14840 cx.update_editor(|editor, window, cx| {
14841 editor.move_page_up(&MovePageUp::default(), window, cx);
14842 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14843 {
14844 assert!(
14845 menu.selected_item == 0,
14846 "expected PageUp to select the first item from the context menu"
14847 );
14848 } else {
14849 panic!("expected completion menu to stay open after PageUp");
14850 }
14851 });
14852}
14853
14854#[gpui::test]
14855async fn test_as_is_completions(cx: &mut TestAppContext) {
14856 init_test(cx, |_| {});
14857 let mut cx = EditorLspTestContext::new_rust(
14858 lsp::ServerCapabilities {
14859 completion_provider: Some(lsp::CompletionOptions {
14860 ..Default::default()
14861 }),
14862 ..Default::default()
14863 },
14864 cx,
14865 )
14866 .await;
14867 cx.lsp
14868 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14869 Ok(Some(lsp::CompletionResponse::Array(vec![
14870 lsp::CompletionItem {
14871 label: "unsafe".into(),
14872 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14873 range: lsp::Range {
14874 start: lsp::Position {
14875 line: 1,
14876 character: 2,
14877 },
14878 end: lsp::Position {
14879 line: 1,
14880 character: 3,
14881 },
14882 },
14883 new_text: "unsafe".to_string(),
14884 })),
14885 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14886 ..Default::default()
14887 },
14888 ])))
14889 });
14890 cx.set_state("fn a() {}\n nˇ");
14891 cx.executor().run_until_parked();
14892 cx.update_editor(|editor, window, cx| {
14893 editor.show_completions(
14894 &ShowCompletions {
14895 trigger: Some("\n".into()),
14896 },
14897 window,
14898 cx,
14899 );
14900 });
14901 cx.executor().run_until_parked();
14902
14903 cx.update_editor(|editor, window, cx| {
14904 editor.confirm_completion(&Default::default(), window, cx)
14905 });
14906 cx.executor().run_until_parked();
14907 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14908}
14909
14910#[gpui::test]
14911async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14912 init_test(cx, |_| {});
14913 let language =
14914 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14915 let mut cx = EditorLspTestContext::new(
14916 language,
14917 lsp::ServerCapabilities {
14918 completion_provider: Some(lsp::CompletionOptions {
14919 ..lsp::CompletionOptions::default()
14920 }),
14921 ..lsp::ServerCapabilities::default()
14922 },
14923 cx,
14924 )
14925 .await;
14926
14927 cx.set_state(
14928 "#ifndef BAR_H
14929#define BAR_H
14930
14931#include <stdbool.h>
14932
14933int fn_branch(bool do_branch1, bool do_branch2);
14934
14935#endif // BAR_H
14936ˇ",
14937 );
14938 cx.executor().run_until_parked();
14939 cx.update_editor(|editor, window, cx| {
14940 editor.handle_input("#", window, cx);
14941 });
14942 cx.executor().run_until_parked();
14943 cx.update_editor(|editor, window, cx| {
14944 editor.handle_input("i", window, cx);
14945 });
14946 cx.executor().run_until_parked();
14947 cx.update_editor(|editor, window, cx| {
14948 editor.handle_input("n", window, cx);
14949 });
14950 cx.executor().run_until_parked();
14951 cx.assert_editor_state(
14952 "#ifndef BAR_H
14953#define BAR_H
14954
14955#include <stdbool.h>
14956
14957int fn_branch(bool do_branch1, bool do_branch2);
14958
14959#endif // BAR_H
14960#inˇ",
14961 );
14962
14963 cx.lsp
14964 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14965 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14966 is_incomplete: false,
14967 item_defaults: None,
14968 items: vec![lsp::CompletionItem {
14969 kind: Some(lsp::CompletionItemKind::SNIPPET),
14970 label_details: Some(lsp::CompletionItemLabelDetails {
14971 detail: Some("header".to_string()),
14972 description: None,
14973 }),
14974 label: " include".to_string(),
14975 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14976 range: lsp::Range {
14977 start: lsp::Position {
14978 line: 8,
14979 character: 1,
14980 },
14981 end: lsp::Position {
14982 line: 8,
14983 character: 1,
14984 },
14985 },
14986 new_text: "include \"$0\"".to_string(),
14987 })),
14988 sort_text: Some("40b67681include".to_string()),
14989 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14990 filter_text: Some("include".to_string()),
14991 insert_text: Some("include \"$0\"".to_string()),
14992 ..lsp::CompletionItem::default()
14993 }],
14994 })))
14995 });
14996 cx.update_editor(|editor, window, cx| {
14997 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14998 });
14999 cx.executor().run_until_parked();
15000 cx.update_editor(|editor, window, cx| {
15001 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15002 });
15003 cx.executor().run_until_parked();
15004 cx.assert_editor_state(
15005 "#ifndef BAR_H
15006#define BAR_H
15007
15008#include <stdbool.h>
15009
15010int fn_branch(bool do_branch1, bool do_branch2);
15011
15012#endif // BAR_H
15013#include \"ˇ\"",
15014 );
15015
15016 cx.lsp
15017 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15018 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15019 is_incomplete: true,
15020 item_defaults: None,
15021 items: vec![lsp::CompletionItem {
15022 kind: Some(lsp::CompletionItemKind::FILE),
15023 label: "AGL/".to_string(),
15024 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15025 range: lsp::Range {
15026 start: lsp::Position {
15027 line: 8,
15028 character: 10,
15029 },
15030 end: lsp::Position {
15031 line: 8,
15032 character: 11,
15033 },
15034 },
15035 new_text: "AGL/".to_string(),
15036 })),
15037 sort_text: Some("40b67681AGL/".to_string()),
15038 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15039 filter_text: Some("AGL/".to_string()),
15040 insert_text: Some("AGL/".to_string()),
15041 ..lsp::CompletionItem::default()
15042 }],
15043 })))
15044 });
15045 cx.update_editor(|editor, window, cx| {
15046 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15047 });
15048 cx.executor().run_until_parked();
15049 cx.update_editor(|editor, window, cx| {
15050 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15051 });
15052 cx.executor().run_until_parked();
15053 cx.assert_editor_state(
15054 r##"#ifndef BAR_H
15055#define BAR_H
15056
15057#include <stdbool.h>
15058
15059int fn_branch(bool do_branch1, bool do_branch2);
15060
15061#endif // BAR_H
15062#include "AGL/ˇ"##,
15063 );
15064
15065 cx.update_editor(|editor, window, cx| {
15066 editor.handle_input("\"", window, cx);
15067 });
15068 cx.executor().run_until_parked();
15069 cx.assert_editor_state(
15070 r##"#ifndef BAR_H
15071#define BAR_H
15072
15073#include <stdbool.h>
15074
15075int fn_branch(bool do_branch1, bool do_branch2);
15076
15077#endif // BAR_H
15078#include "AGL/"ˇ"##,
15079 );
15080}
15081
15082#[gpui::test]
15083async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15084 init_test(cx, |_| {});
15085
15086 let mut cx = EditorLspTestContext::new_rust(
15087 lsp::ServerCapabilities {
15088 completion_provider: Some(lsp::CompletionOptions {
15089 trigger_characters: Some(vec![".".to_string()]),
15090 resolve_provider: Some(true),
15091 ..Default::default()
15092 }),
15093 ..Default::default()
15094 },
15095 cx,
15096 )
15097 .await;
15098
15099 cx.set_state("fn main() { let a = 2ˇ; }");
15100 cx.simulate_keystroke(".");
15101 let completion_item = lsp::CompletionItem {
15102 label: "Some".into(),
15103 kind: Some(lsp::CompletionItemKind::SNIPPET),
15104 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15105 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15106 kind: lsp::MarkupKind::Markdown,
15107 value: "```rust\nSome(2)\n```".to_string(),
15108 })),
15109 deprecated: Some(false),
15110 sort_text: Some("Some".to_string()),
15111 filter_text: Some("Some".to_string()),
15112 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15113 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15114 range: lsp::Range {
15115 start: lsp::Position {
15116 line: 0,
15117 character: 22,
15118 },
15119 end: lsp::Position {
15120 line: 0,
15121 character: 22,
15122 },
15123 },
15124 new_text: "Some(2)".to_string(),
15125 })),
15126 additional_text_edits: Some(vec![lsp::TextEdit {
15127 range: lsp::Range {
15128 start: lsp::Position {
15129 line: 0,
15130 character: 20,
15131 },
15132 end: lsp::Position {
15133 line: 0,
15134 character: 22,
15135 },
15136 },
15137 new_text: "".to_string(),
15138 }]),
15139 ..Default::default()
15140 };
15141
15142 let closure_completion_item = completion_item.clone();
15143 let counter = Arc::new(AtomicUsize::new(0));
15144 let counter_clone = counter.clone();
15145 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15146 let task_completion_item = closure_completion_item.clone();
15147 counter_clone.fetch_add(1, atomic::Ordering::Release);
15148 async move {
15149 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15150 is_incomplete: true,
15151 item_defaults: None,
15152 items: vec![task_completion_item],
15153 })))
15154 }
15155 });
15156
15157 cx.condition(|editor, _| editor.context_menu_visible())
15158 .await;
15159 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15160 assert!(request.next().await.is_some());
15161 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15162
15163 cx.simulate_keystrokes("S o m");
15164 cx.condition(|editor, _| editor.context_menu_visible())
15165 .await;
15166 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15167 assert!(request.next().await.is_some());
15168 assert!(request.next().await.is_some());
15169 assert!(request.next().await.is_some());
15170 request.close();
15171 assert!(request.next().await.is_none());
15172 assert_eq!(
15173 counter.load(atomic::Ordering::Acquire),
15174 4,
15175 "With the completions menu open, only one LSP request should happen per input"
15176 );
15177}
15178
15179#[gpui::test]
15180async fn test_toggle_comment(cx: &mut TestAppContext) {
15181 init_test(cx, |_| {});
15182 let mut cx = EditorTestContext::new(cx).await;
15183 let language = Arc::new(Language::new(
15184 LanguageConfig {
15185 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15186 ..Default::default()
15187 },
15188 Some(tree_sitter_rust::LANGUAGE.into()),
15189 ));
15190 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15191
15192 // If multiple selections intersect a line, the line is only toggled once.
15193 cx.set_state(indoc! {"
15194 fn a() {
15195 «//b();
15196 ˇ»// «c();
15197 //ˇ» d();
15198 }
15199 "});
15200
15201 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15202
15203 cx.assert_editor_state(indoc! {"
15204 fn a() {
15205 «b();
15206 c();
15207 ˇ» d();
15208 }
15209 "});
15210
15211 // The comment prefix is inserted at the same column for every line in a
15212 // selection.
15213 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15214
15215 cx.assert_editor_state(indoc! {"
15216 fn a() {
15217 // «b();
15218 // c();
15219 ˇ»// d();
15220 }
15221 "});
15222
15223 // If a selection ends at the beginning of a line, that line is not toggled.
15224 cx.set_selections_state(indoc! {"
15225 fn a() {
15226 // b();
15227 «// c();
15228 ˇ» // d();
15229 }
15230 "});
15231
15232 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15233
15234 cx.assert_editor_state(indoc! {"
15235 fn a() {
15236 // b();
15237 «c();
15238 ˇ» // d();
15239 }
15240 "});
15241
15242 // If a selection span a single line and is empty, the line is toggled.
15243 cx.set_state(indoc! {"
15244 fn a() {
15245 a();
15246 b();
15247 ˇ
15248 }
15249 "});
15250
15251 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15252
15253 cx.assert_editor_state(indoc! {"
15254 fn a() {
15255 a();
15256 b();
15257 //•ˇ
15258 }
15259 "});
15260
15261 // If a selection span multiple lines, empty lines are not toggled.
15262 cx.set_state(indoc! {"
15263 fn a() {
15264 «a();
15265
15266 c();ˇ»
15267 }
15268 "});
15269
15270 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15271
15272 cx.assert_editor_state(indoc! {"
15273 fn a() {
15274 // «a();
15275
15276 // c();ˇ»
15277 }
15278 "});
15279
15280 // If a selection includes multiple comment prefixes, all lines are uncommented.
15281 cx.set_state(indoc! {"
15282 fn a() {
15283 «// a();
15284 /// b();
15285 //! c();ˇ»
15286 }
15287 "});
15288
15289 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15290
15291 cx.assert_editor_state(indoc! {"
15292 fn a() {
15293 «a();
15294 b();
15295 c();ˇ»
15296 }
15297 "});
15298}
15299
15300#[gpui::test]
15301async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15302 init_test(cx, |_| {});
15303 let mut cx = EditorTestContext::new(cx).await;
15304 let language = Arc::new(Language::new(
15305 LanguageConfig {
15306 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15307 ..Default::default()
15308 },
15309 Some(tree_sitter_rust::LANGUAGE.into()),
15310 ));
15311 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15312
15313 let toggle_comments = &ToggleComments {
15314 advance_downwards: false,
15315 ignore_indent: true,
15316 };
15317
15318 // If multiple selections intersect a line, the line is only toggled once.
15319 cx.set_state(indoc! {"
15320 fn a() {
15321 // «b();
15322 // c();
15323 // ˇ» d();
15324 }
15325 "});
15326
15327 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15328
15329 cx.assert_editor_state(indoc! {"
15330 fn a() {
15331 «b();
15332 c();
15333 ˇ» d();
15334 }
15335 "});
15336
15337 // The comment prefix is inserted at the beginning of each line
15338 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15339
15340 cx.assert_editor_state(indoc! {"
15341 fn a() {
15342 // «b();
15343 // c();
15344 // ˇ» d();
15345 }
15346 "});
15347
15348 // If a selection ends at the beginning of a line, that line is not toggled.
15349 cx.set_selections_state(indoc! {"
15350 fn a() {
15351 // b();
15352 // «c();
15353 ˇ»// d();
15354 }
15355 "});
15356
15357 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15358
15359 cx.assert_editor_state(indoc! {"
15360 fn a() {
15361 // b();
15362 «c();
15363 ˇ»// d();
15364 }
15365 "});
15366
15367 // If a selection span a single line and is empty, the line is toggled.
15368 cx.set_state(indoc! {"
15369 fn a() {
15370 a();
15371 b();
15372 ˇ
15373 }
15374 "});
15375
15376 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15377
15378 cx.assert_editor_state(indoc! {"
15379 fn a() {
15380 a();
15381 b();
15382 //ˇ
15383 }
15384 "});
15385
15386 // If a selection span multiple lines, empty lines are not toggled.
15387 cx.set_state(indoc! {"
15388 fn a() {
15389 «a();
15390
15391 c();ˇ»
15392 }
15393 "});
15394
15395 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15396
15397 cx.assert_editor_state(indoc! {"
15398 fn a() {
15399 // «a();
15400
15401 // c();ˇ»
15402 }
15403 "});
15404
15405 // If a selection includes multiple comment prefixes, all lines are uncommented.
15406 cx.set_state(indoc! {"
15407 fn a() {
15408 // «a();
15409 /// b();
15410 //! c();ˇ»
15411 }
15412 "});
15413
15414 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15415
15416 cx.assert_editor_state(indoc! {"
15417 fn a() {
15418 «a();
15419 b();
15420 c();ˇ»
15421 }
15422 "});
15423}
15424
15425#[gpui::test]
15426async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15427 init_test(cx, |_| {});
15428
15429 let language = Arc::new(Language::new(
15430 LanguageConfig {
15431 line_comments: vec!["// ".into()],
15432 ..Default::default()
15433 },
15434 Some(tree_sitter_rust::LANGUAGE.into()),
15435 ));
15436
15437 let mut cx = EditorTestContext::new(cx).await;
15438
15439 cx.language_registry().add(language.clone());
15440 cx.update_buffer(|buffer, cx| {
15441 buffer.set_language(Some(language), cx);
15442 });
15443
15444 let toggle_comments = &ToggleComments {
15445 advance_downwards: true,
15446 ignore_indent: false,
15447 };
15448
15449 // Single cursor on one line -> advance
15450 // Cursor moves horizontally 3 characters as well on non-blank line
15451 cx.set_state(indoc!(
15452 "fn a() {
15453 ˇdog();
15454 cat();
15455 }"
15456 ));
15457 cx.update_editor(|editor, window, cx| {
15458 editor.toggle_comments(toggle_comments, window, cx);
15459 });
15460 cx.assert_editor_state(indoc!(
15461 "fn a() {
15462 // dog();
15463 catˇ();
15464 }"
15465 ));
15466
15467 // Single selection on one line -> don't advance
15468 cx.set_state(indoc!(
15469 "fn a() {
15470 «dog()ˇ»;
15471 cat();
15472 }"
15473 ));
15474 cx.update_editor(|editor, window, cx| {
15475 editor.toggle_comments(toggle_comments, window, cx);
15476 });
15477 cx.assert_editor_state(indoc!(
15478 "fn a() {
15479 // «dog()ˇ»;
15480 cat();
15481 }"
15482 ));
15483
15484 // Multiple cursors on one line -> advance
15485 cx.set_state(indoc!(
15486 "fn a() {
15487 ˇdˇog();
15488 cat();
15489 }"
15490 ));
15491 cx.update_editor(|editor, window, cx| {
15492 editor.toggle_comments(toggle_comments, window, cx);
15493 });
15494 cx.assert_editor_state(indoc!(
15495 "fn a() {
15496 // dog();
15497 catˇ(ˇ);
15498 }"
15499 ));
15500
15501 // Multiple cursors on one line, with selection -> don't advance
15502 cx.set_state(indoc!(
15503 "fn a() {
15504 ˇdˇog«()ˇ»;
15505 cat();
15506 }"
15507 ));
15508 cx.update_editor(|editor, window, cx| {
15509 editor.toggle_comments(toggle_comments, window, cx);
15510 });
15511 cx.assert_editor_state(indoc!(
15512 "fn a() {
15513 // ˇdˇog«()ˇ»;
15514 cat();
15515 }"
15516 ));
15517
15518 // Single cursor on one line -> advance
15519 // Cursor moves to column 0 on blank line
15520 cx.set_state(indoc!(
15521 "fn a() {
15522 ˇdog();
15523
15524 cat();
15525 }"
15526 ));
15527 cx.update_editor(|editor, window, cx| {
15528 editor.toggle_comments(toggle_comments, window, cx);
15529 });
15530 cx.assert_editor_state(indoc!(
15531 "fn a() {
15532 // dog();
15533 ˇ
15534 cat();
15535 }"
15536 ));
15537
15538 // Single cursor on one line -> advance
15539 // Cursor starts and ends at column 0
15540 cx.set_state(indoc!(
15541 "fn a() {
15542 ˇ dog();
15543 cat();
15544 }"
15545 ));
15546 cx.update_editor(|editor, window, cx| {
15547 editor.toggle_comments(toggle_comments, window, cx);
15548 });
15549 cx.assert_editor_state(indoc!(
15550 "fn a() {
15551 // dog();
15552 ˇ cat();
15553 }"
15554 ));
15555}
15556
15557#[gpui::test]
15558async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15559 init_test(cx, |_| {});
15560
15561 let mut cx = EditorTestContext::new(cx).await;
15562
15563 let html_language = Arc::new(
15564 Language::new(
15565 LanguageConfig {
15566 name: "HTML".into(),
15567 block_comment: Some(BlockCommentConfig {
15568 start: "<!-- ".into(),
15569 prefix: "".into(),
15570 end: " -->".into(),
15571 tab_size: 0,
15572 }),
15573 ..Default::default()
15574 },
15575 Some(tree_sitter_html::LANGUAGE.into()),
15576 )
15577 .with_injection_query(
15578 r#"
15579 (script_element
15580 (raw_text) @injection.content
15581 (#set! injection.language "javascript"))
15582 "#,
15583 )
15584 .unwrap(),
15585 );
15586
15587 let javascript_language = Arc::new(Language::new(
15588 LanguageConfig {
15589 name: "JavaScript".into(),
15590 line_comments: vec!["// ".into()],
15591 ..Default::default()
15592 },
15593 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15594 ));
15595
15596 cx.language_registry().add(html_language.clone());
15597 cx.language_registry().add(javascript_language);
15598 cx.update_buffer(|buffer, cx| {
15599 buffer.set_language(Some(html_language), cx);
15600 });
15601
15602 // Toggle comments for empty selections
15603 cx.set_state(
15604 &r#"
15605 <p>A</p>ˇ
15606 <p>B</p>ˇ
15607 <p>C</p>ˇ
15608 "#
15609 .unindent(),
15610 );
15611 cx.update_editor(|editor, window, cx| {
15612 editor.toggle_comments(&ToggleComments::default(), window, cx)
15613 });
15614 cx.assert_editor_state(
15615 &r#"
15616 <!-- <p>A</p>ˇ -->
15617 <!-- <p>B</p>ˇ -->
15618 <!-- <p>C</p>ˇ -->
15619 "#
15620 .unindent(),
15621 );
15622 cx.update_editor(|editor, window, cx| {
15623 editor.toggle_comments(&ToggleComments::default(), window, cx)
15624 });
15625 cx.assert_editor_state(
15626 &r#"
15627 <p>A</p>ˇ
15628 <p>B</p>ˇ
15629 <p>C</p>ˇ
15630 "#
15631 .unindent(),
15632 );
15633
15634 // Toggle comments for mixture of empty and non-empty selections, where
15635 // multiple selections occupy a given line.
15636 cx.set_state(
15637 &r#"
15638 <p>A«</p>
15639 <p>ˇ»B</p>ˇ
15640 <p>C«</p>
15641 <p>ˇ»D</p>ˇ
15642 "#
15643 .unindent(),
15644 );
15645
15646 cx.update_editor(|editor, window, cx| {
15647 editor.toggle_comments(&ToggleComments::default(), window, cx)
15648 });
15649 cx.assert_editor_state(
15650 &r#"
15651 <!-- <p>A«</p>
15652 <p>ˇ»B</p>ˇ -->
15653 <!-- <p>C«</p>
15654 <p>ˇ»D</p>ˇ -->
15655 "#
15656 .unindent(),
15657 );
15658 cx.update_editor(|editor, window, cx| {
15659 editor.toggle_comments(&ToggleComments::default(), window, cx)
15660 });
15661 cx.assert_editor_state(
15662 &r#"
15663 <p>A«</p>
15664 <p>ˇ»B</p>ˇ
15665 <p>C«</p>
15666 <p>ˇ»D</p>ˇ
15667 "#
15668 .unindent(),
15669 );
15670
15671 // Toggle comments when different languages are active for different
15672 // selections.
15673 cx.set_state(
15674 &r#"
15675 ˇ<script>
15676 ˇvar x = new Y();
15677 ˇ</script>
15678 "#
15679 .unindent(),
15680 );
15681 cx.executor().run_until_parked();
15682 cx.update_editor(|editor, window, cx| {
15683 editor.toggle_comments(&ToggleComments::default(), window, cx)
15684 });
15685 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15686 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15687 cx.assert_editor_state(
15688 &r#"
15689 <!-- ˇ<script> -->
15690 // ˇvar x = new Y();
15691 <!-- ˇ</script> -->
15692 "#
15693 .unindent(),
15694 );
15695}
15696
15697#[gpui::test]
15698fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15699 init_test(cx, |_| {});
15700
15701 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15702 let multibuffer = cx.new(|cx| {
15703 let mut multibuffer = MultiBuffer::new(ReadWrite);
15704 multibuffer.push_excerpts(
15705 buffer.clone(),
15706 [
15707 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15708 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15709 ],
15710 cx,
15711 );
15712 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15713 multibuffer
15714 });
15715
15716 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15717 editor.update_in(cx, |editor, window, cx| {
15718 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15719 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15720 s.select_ranges([
15721 Point::new(0, 0)..Point::new(0, 0),
15722 Point::new(1, 0)..Point::new(1, 0),
15723 ])
15724 });
15725
15726 editor.handle_input("X", window, cx);
15727 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15728 assert_eq!(
15729 editor.selections.ranges(cx),
15730 [
15731 Point::new(0, 1)..Point::new(0, 1),
15732 Point::new(1, 1)..Point::new(1, 1),
15733 ]
15734 );
15735
15736 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15737 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15738 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15739 });
15740 editor.backspace(&Default::default(), window, cx);
15741 assert_eq!(editor.text(cx), "Xa\nbbb");
15742 assert_eq!(
15743 editor.selections.ranges(cx),
15744 [Point::new(1, 0)..Point::new(1, 0)]
15745 );
15746
15747 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15748 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15749 });
15750 editor.backspace(&Default::default(), window, cx);
15751 assert_eq!(editor.text(cx), "X\nbb");
15752 assert_eq!(
15753 editor.selections.ranges(cx),
15754 [Point::new(0, 1)..Point::new(0, 1)]
15755 );
15756 });
15757}
15758
15759#[gpui::test]
15760fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15761 init_test(cx, |_| {});
15762
15763 let markers = vec![('[', ']').into(), ('(', ')').into()];
15764 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15765 indoc! {"
15766 [aaaa
15767 (bbbb]
15768 cccc)",
15769 },
15770 markers.clone(),
15771 );
15772 let excerpt_ranges = markers.into_iter().map(|marker| {
15773 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15774 ExcerptRange::new(context)
15775 });
15776 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15777 let multibuffer = cx.new(|cx| {
15778 let mut multibuffer = MultiBuffer::new(ReadWrite);
15779 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15780 multibuffer
15781 });
15782
15783 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15784 editor.update_in(cx, |editor, window, cx| {
15785 let (expected_text, selection_ranges) = marked_text_ranges(
15786 indoc! {"
15787 aaaa
15788 bˇbbb
15789 bˇbbˇb
15790 cccc"
15791 },
15792 true,
15793 );
15794 assert_eq!(editor.text(cx), expected_text);
15795 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15796 s.select_ranges(selection_ranges)
15797 });
15798
15799 editor.handle_input("X", window, cx);
15800
15801 let (expected_text, expected_selections) = marked_text_ranges(
15802 indoc! {"
15803 aaaa
15804 bXˇbbXb
15805 bXˇbbXˇb
15806 cccc"
15807 },
15808 false,
15809 );
15810 assert_eq!(editor.text(cx), expected_text);
15811 assert_eq!(editor.selections.ranges(cx), expected_selections);
15812
15813 editor.newline(&Newline, window, cx);
15814 let (expected_text, expected_selections) = marked_text_ranges(
15815 indoc! {"
15816 aaaa
15817 bX
15818 ˇbbX
15819 b
15820 bX
15821 ˇbbX
15822 ˇb
15823 cccc"
15824 },
15825 false,
15826 );
15827 assert_eq!(editor.text(cx), expected_text);
15828 assert_eq!(editor.selections.ranges(cx), expected_selections);
15829 });
15830}
15831
15832#[gpui::test]
15833fn test_refresh_selections(cx: &mut TestAppContext) {
15834 init_test(cx, |_| {});
15835
15836 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15837 let mut excerpt1_id = None;
15838 let multibuffer = cx.new(|cx| {
15839 let mut multibuffer = MultiBuffer::new(ReadWrite);
15840 excerpt1_id = multibuffer
15841 .push_excerpts(
15842 buffer.clone(),
15843 [
15844 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15845 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15846 ],
15847 cx,
15848 )
15849 .into_iter()
15850 .next();
15851 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15852 multibuffer
15853 });
15854
15855 let editor = cx.add_window(|window, cx| {
15856 let mut editor = build_editor(multibuffer.clone(), window, cx);
15857 let snapshot = editor.snapshot(window, cx);
15858 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15859 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15860 });
15861 editor.begin_selection(
15862 Point::new(2, 1).to_display_point(&snapshot),
15863 true,
15864 1,
15865 window,
15866 cx,
15867 );
15868 assert_eq!(
15869 editor.selections.ranges(cx),
15870 [
15871 Point::new(1, 3)..Point::new(1, 3),
15872 Point::new(2, 1)..Point::new(2, 1),
15873 ]
15874 );
15875 editor
15876 });
15877
15878 // Refreshing selections is a no-op when excerpts haven't changed.
15879 _ = editor.update(cx, |editor, window, cx| {
15880 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15881 assert_eq!(
15882 editor.selections.ranges(cx),
15883 [
15884 Point::new(1, 3)..Point::new(1, 3),
15885 Point::new(2, 1)..Point::new(2, 1),
15886 ]
15887 );
15888 });
15889
15890 multibuffer.update(cx, |multibuffer, cx| {
15891 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15892 });
15893 _ = editor.update(cx, |editor, window, cx| {
15894 // Removing an excerpt causes the first selection to become degenerate.
15895 assert_eq!(
15896 editor.selections.ranges(cx),
15897 [
15898 Point::new(0, 0)..Point::new(0, 0),
15899 Point::new(0, 1)..Point::new(0, 1)
15900 ]
15901 );
15902
15903 // Refreshing selections will relocate the first selection to the original buffer
15904 // location.
15905 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15906 assert_eq!(
15907 editor.selections.ranges(cx),
15908 [
15909 Point::new(0, 1)..Point::new(0, 1),
15910 Point::new(0, 3)..Point::new(0, 3)
15911 ]
15912 );
15913 assert!(editor.selections.pending_anchor().is_some());
15914 });
15915}
15916
15917#[gpui::test]
15918fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15919 init_test(cx, |_| {});
15920
15921 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15922 let mut excerpt1_id = None;
15923 let multibuffer = cx.new(|cx| {
15924 let mut multibuffer = MultiBuffer::new(ReadWrite);
15925 excerpt1_id = multibuffer
15926 .push_excerpts(
15927 buffer.clone(),
15928 [
15929 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15930 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15931 ],
15932 cx,
15933 )
15934 .into_iter()
15935 .next();
15936 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15937 multibuffer
15938 });
15939
15940 let editor = cx.add_window(|window, cx| {
15941 let mut editor = build_editor(multibuffer.clone(), window, cx);
15942 let snapshot = editor.snapshot(window, cx);
15943 editor.begin_selection(
15944 Point::new(1, 3).to_display_point(&snapshot),
15945 false,
15946 1,
15947 window,
15948 cx,
15949 );
15950 assert_eq!(
15951 editor.selections.ranges(cx),
15952 [Point::new(1, 3)..Point::new(1, 3)]
15953 );
15954 editor
15955 });
15956
15957 multibuffer.update(cx, |multibuffer, cx| {
15958 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15959 });
15960 _ = editor.update(cx, |editor, window, cx| {
15961 assert_eq!(
15962 editor.selections.ranges(cx),
15963 [Point::new(0, 0)..Point::new(0, 0)]
15964 );
15965
15966 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
15967 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15968 assert_eq!(
15969 editor.selections.ranges(cx),
15970 [Point::new(0, 3)..Point::new(0, 3)]
15971 );
15972 assert!(editor.selections.pending_anchor().is_some());
15973 });
15974}
15975
15976#[gpui::test]
15977async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
15978 init_test(cx, |_| {});
15979
15980 let language = Arc::new(
15981 Language::new(
15982 LanguageConfig {
15983 brackets: BracketPairConfig {
15984 pairs: vec![
15985 BracketPair {
15986 start: "{".to_string(),
15987 end: "}".to_string(),
15988 close: true,
15989 surround: true,
15990 newline: true,
15991 },
15992 BracketPair {
15993 start: "/* ".to_string(),
15994 end: " */".to_string(),
15995 close: true,
15996 surround: true,
15997 newline: true,
15998 },
15999 ],
16000 ..Default::default()
16001 },
16002 ..Default::default()
16003 },
16004 Some(tree_sitter_rust::LANGUAGE.into()),
16005 )
16006 .with_indents_query("")
16007 .unwrap(),
16008 );
16009
16010 let text = concat!(
16011 "{ }\n", //
16012 " x\n", //
16013 " /* */\n", //
16014 "x\n", //
16015 "{{} }\n", //
16016 );
16017
16018 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16019 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16020 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16021 editor
16022 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16023 .await;
16024
16025 editor.update_in(cx, |editor, window, cx| {
16026 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16027 s.select_display_ranges([
16028 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16029 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16030 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16031 ])
16032 });
16033 editor.newline(&Newline, window, cx);
16034
16035 assert_eq!(
16036 editor.buffer().read(cx).read(cx).text(),
16037 concat!(
16038 "{ \n", // Suppress rustfmt
16039 "\n", //
16040 "}\n", //
16041 " x\n", //
16042 " /* \n", //
16043 " \n", //
16044 " */\n", //
16045 "x\n", //
16046 "{{} \n", //
16047 "}\n", //
16048 )
16049 );
16050 });
16051}
16052
16053#[gpui::test]
16054fn test_highlighted_ranges(cx: &mut TestAppContext) {
16055 init_test(cx, |_| {});
16056
16057 let editor = cx.add_window(|window, cx| {
16058 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16059 build_editor(buffer, window, cx)
16060 });
16061
16062 _ = editor.update(cx, |editor, window, cx| {
16063 struct Type1;
16064 struct Type2;
16065
16066 let buffer = editor.buffer.read(cx).snapshot(cx);
16067
16068 let anchor_range =
16069 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16070
16071 editor.highlight_background::<Type1>(
16072 &[
16073 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16074 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16075 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16076 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16077 ],
16078 |_| Hsla::red(),
16079 cx,
16080 );
16081 editor.highlight_background::<Type2>(
16082 &[
16083 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16084 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16085 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16086 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16087 ],
16088 |_| Hsla::green(),
16089 cx,
16090 );
16091
16092 let snapshot = editor.snapshot(window, cx);
16093 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16094 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16095 &snapshot,
16096 cx.theme(),
16097 );
16098 assert_eq!(
16099 highlighted_ranges,
16100 &[
16101 (
16102 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16103 Hsla::green(),
16104 ),
16105 (
16106 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16107 Hsla::red(),
16108 ),
16109 (
16110 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16111 Hsla::green(),
16112 ),
16113 (
16114 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16115 Hsla::red(),
16116 ),
16117 ]
16118 );
16119 assert_eq!(
16120 editor.sorted_background_highlights_in_range(
16121 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16122 &snapshot,
16123 cx.theme(),
16124 ),
16125 &[(
16126 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16127 Hsla::red(),
16128 )]
16129 );
16130 });
16131}
16132
16133#[gpui::test]
16134async fn test_following(cx: &mut TestAppContext) {
16135 init_test(cx, |_| {});
16136
16137 let fs = FakeFs::new(cx.executor());
16138 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16139
16140 let buffer = project.update(cx, |project, cx| {
16141 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16142 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16143 });
16144 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16145 let follower = cx.update(|cx| {
16146 cx.open_window(
16147 WindowOptions {
16148 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16149 gpui::Point::new(px(0.), px(0.)),
16150 gpui::Point::new(px(10.), px(80.)),
16151 ))),
16152 ..Default::default()
16153 },
16154 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16155 )
16156 .unwrap()
16157 });
16158
16159 let is_still_following = Rc::new(RefCell::new(true));
16160 let follower_edit_event_count = Rc::new(RefCell::new(0));
16161 let pending_update = Rc::new(RefCell::new(None));
16162 let leader_entity = leader.root(cx).unwrap();
16163 let follower_entity = follower.root(cx).unwrap();
16164 _ = follower.update(cx, {
16165 let update = pending_update.clone();
16166 let is_still_following = is_still_following.clone();
16167 let follower_edit_event_count = follower_edit_event_count.clone();
16168 |_, window, cx| {
16169 cx.subscribe_in(
16170 &leader_entity,
16171 window,
16172 move |_, leader, event, window, cx| {
16173 leader.read(cx).add_event_to_update_proto(
16174 event,
16175 &mut update.borrow_mut(),
16176 window,
16177 cx,
16178 );
16179 },
16180 )
16181 .detach();
16182
16183 cx.subscribe_in(
16184 &follower_entity,
16185 window,
16186 move |_, _, event: &EditorEvent, _window, _cx| {
16187 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16188 *is_still_following.borrow_mut() = false;
16189 }
16190
16191 if let EditorEvent::BufferEdited = event {
16192 *follower_edit_event_count.borrow_mut() += 1;
16193 }
16194 },
16195 )
16196 .detach();
16197 }
16198 });
16199
16200 // Update the selections only
16201 _ = leader.update(cx, |leader, window, cx| {
16202 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16203 s.select_ranges([1..1])
16204 });
16205 });
16206 follower
16207 .update(cx, |follower, window, cx| {
16208 follower.apply_update_proto(
16209 &project,
16210 pending_update.borrow_mut().take().unwrap(),
16211 window,
16212 cx,
16213 )
16214 })
16215 .unwrap()
16216 .await
16217 .unwrap();
16218 _ = follower.update(cx, |follower, _, cx| {
16219 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16220 });
16221 assert!(*is_still_following.borrow());
16222 assert_eq!(*follower_edit_event_count.borrow(), 0);
16223
16224 // Update the scroll position only
16225 _ = leader.update(cx, |leader, window, cx| {
16226 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16227 });
16228 follower
16229 .update(cx, |follower, window, cx| {
16230 follower.apply_update_proto(
16231 &project,
16232 pending_update.borrow_mut().take().unwrap(),
16233 window,
16234 cx,
16235 )
16236 })
16237 .unwrap()
16238 .await
16239 .unwrap();
16240 assert_eq!(
16241 follower
16242 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16243 .unwrap(),
16244 gpui::Point::new(1.5, 3.5)
16245 );
16246 assert!(*is_still_following.borrow());
16247 assert_eq!(*follower_edit_event_count.borrow(), 0);
16248
16249 // Update the selections and scroll position. The follower's scroll position is updated
16250 // via autoscroll, not via the leader's exact scroll position.
16251 _ = leader.update(cx, |leader, window, cx| {
16252 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16253 s.select_ranges([0..0])
16254 });
16255 leader.request_autoscroll(Autoscroll::newest(), cx);
16256 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16257 });
16258 follower
16259 .update(cx, |follower, window, cx| {
16260 follower.apply_update_proto(
16261 &project,
16262 pending_update.borrow_mut().take().unwrap(),
16263 window,
16264 cx,
16265 )
16266 })
16267 .unwrap()
16268 .await
16269 .unwrap();
16270 _ = follower.update(cx, |follower, _, cx| {
16271 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16272 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16273 });
16274 assert!(*is_still_following.borrow());
16275
16276 // Creating a pending selection that precedes another selection
16277 _ = leader.update(cx, |leader, window, cx| {
16278 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16279 s.select_ranges([1..1])
16280 });
16281 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16282 });
16283 follower
16284 .update(cx, |follower, window, cx| {
16285 follower.apply_update_proto(
16286 &project,
16287 pending_update.borrow_mut().take().unwrap(),
16288 window,
16289 cx,
16290 )
16291 })
16292 .unwrap()
16293 .await
16294 .unwrap();
16295 _ = follower.update(cx, |follower, _, cx| {
16296 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16297 });
16298 assert!(*is_still_following.borrow());
16299
16300 // Extend the pending selection so that it surrounds another selection
16301 _ = leader.update(cx, |leader, window, cx| {
16302 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16303 });
16304 follower
16305 .update(cx, |follower, window, cx| {
16306 follower.apply_update_proto(
16307 &project,
16308 pending_update.borrow_mut().take().unwrap(),
16309 window,
16310 cx,
16311 )
16312 })
16313 .unwrap()
16314 .await
16315 .unwrap();
16316 _ = follower.update(cx, |follower, _, cx| {
16317 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16318 });
16319
16320 // Scrolling locally breaks the follow
16321 _ = follower.update(cx, |follower, window, cx| {
16322 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16323 follower.set_scroll_anchor(
16324 ScrollAnchor {
16325 anchor: top_anchor,
16326 offset: gpui::Point::new(0.0, 0.5),
16327 },
16328 window,
16329 cx,
16330 );
16331 });
16332 assert!(!(*is_still_following.borrow()));
16333}
16334
16335#[gpui::test]
16336async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16337 init_test(cx, |_| {});
16338
16339 let fs = FakeFs::new(cx.executor());
16340 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16341 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16342 let pane = workspace
16343 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16344 .unwrap();
16345
16346 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16347
16348 let leader = pane.update_in(cx, |_, window, cx| {
16349 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16350 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16351 });
16352
16353 // Start following the editor when it has no excerpts.
16354 let mut state_message =
16355 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16356 let workspace_entity = workspace.root(cx).unwrap();
16357 let follower_1 = cx
16358 .update_window(*workspace.deref(), |_, window, cx| {
16359 Editor::from_state_proto(
16360 workspace_entity,
16361 ViewId {
16362 creator: CollaboratorId::PeerId(PeerId::default()),
16363 id: 0,
16364 },
16365 &mut state_message,
16366 window,
16367 cx,
16368 )
16369 })
16370 .unwrap()
16371 .unwrap()
16372 .await
16373 .unwrap();
16374
16375 let update_message = Rc::new(RefCell::new(None));
16376 follower_1.update_in(cx, {
16377 let update = update_message.clone();
16378 |_, window, cx| {
16379 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16380 leader.read(cx).add_event_to_update_proto(
16381 event,
16382 &mut update.borrow_mut(),
16383 window,
16384 cx,
16385 );
16386 })
16387 .detach();
16388 }
16389 });
16390
16391 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16392 (
16393 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16394 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16395 )
16396 });
16397
16398 // Insert some excerpts.
16399 leader.update(cx, |leader, cx| {
16400 leader.buffer.update(cx, |multibuffer, cx| {
16401 multibuffer.set_excerpts_for_path(
16402 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
16403 buffer_1.clone(),
16404 vec![
16405 Point::row_range(0..3),
16406 Point::row_range(1..6),
16407 Point::row_range(12..15),
16408 ],
16409 0,
16410 cx,
16411 );
16412 multibuffer.set_excerpts_for_path(
16413 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
16414 buffer_2.clone(),
16415 vec![Point::row_range(0..6), Point::row_range(8..12)],
16416 0,
16417 cx,
16418 );
16419 });
16420 });
16421
16422 // Apply the update of adding the excerpts.
16423 follower_1
16424 .update_in(cx, |follower, window, cx| {
16425 follower.apply_update_proto(
16426 &project,
16427 update_message.borrow().clone().unwrap(),
16428 window,
16429 cx,
16430 )
16431 })
16432 .await
16433 .unwrap();
16434 assert_eq!(
16435 follower_1.update(cx, |editor, cx| editor.text(cx)),
16436 leader.update(cx, |editor, cx| editor.text(cx))
16437 );
16438 update_message.borrow_mut().take();
16439
16440 // Start following separately after it already has excerpts.
16441 let mut state_message =
16442 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16443 let workspace_entity = workspace.root(cx).unwrap();
16444 let follower_2 = cx
16445 .update_window(*workspace.deref(), |_, window, cx| {
16446 Editor::from_state_proto(
16447 workspace_entity,
16448 ViewId {
16449 creator: CollaboratorId::PeerId(PeerId::default()),
16450 id: 0,
16451 },
16452 &mut state_message,
16453 window,
16454 cx,
16455 )
16456 })
16457 .unwrap()
16458 .unwrap()
16459 .await
16460 .unwrap();
16461 assert_eq!(
16462 follower_2.update(cx, |editor, cx| editor.text(cx)),
16463 leader.update(cx, |editor, cx| editor.text(cx))
16464 );
16465
16466 // Remove some excerpts.
16467 leader.update(cx, |leader, cx| {
16468 leader.buffer.update(cx, |multibuffer, cx| {
16469 let excerpt_ids = multibuffer.excerpt_ids();
16470 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16471 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16472 });
16473 });
16474
16475 // Apply the update of removing the excerpts.
16476 follower_1
16477 .update_in(cx, |follower, window, cx| {
16478 follower.apply_update_proto(
16479 &project,
16480 update_message.borrow().clone().unwrap(),
16481 window,
16482 cx,
16483 )
16484 })
16485 .await
16486 .unwrap();
16487 follower_2
16488 .update_in(cx, |follower, window, cx| {
16489 follower.apply_update_proto(
16490 &project,
16491 update_message.borrow().clone().unwrap(),
16492 window,
16493 cx,
16494 )
16495 })
16496 .await
16497 .unwrap();
16498 update_message.borrow_mut().take();
16499 assert_eq!(
16500 follower_1.update(cx, |editor, cx| editor.text(cx)),
16501 leader.update(cx, |editor, cx| editor.text(cx))
16502 );
16503}
16504
16505#[gpui::test]
16506async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16507 init_test(cx, |_| {});
16508
16509 let mut cx = EditorTestContext::new(cx).await;
16510 let lsp_store =
16511 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16512
16513 cx.set_state(indoc! {"
16514 ˇfn func(abc def: i32) -> u32 {
16515 }
16516 "});
16517
16518 cx.update(|_, cx| {
16519 lsp_store.update(cx, |lsp_store, cx| {
16520 lsp_store
16521 .update_diagnostics(
16522 LanguageServerId(0),
16523 lsp::PublishDiagnosticsParams {
16524 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16525 version: None,
16526 diagnostics: vec![
16527 lsp::Diagnostic {
16528 range: lsp::Range::new(
16529 lsp::Position::new(0, 11),
16530 lsp::Position::new(0, 12),
16531 ),
16532 severity: Some(lsp::DiagnosticSeverity::ERROR),
16533 ..Default::default()
16534 },
16535 lsp::Diagnostic {
16536 range: lsp::Range::new(
16537 lsp::Position::new(0, 12),
16538 lsp::Position::new(0, 15),
16539 ),
16540 severity: Some(lsp::DiagnosticSeverity::ERROR),
16541 ..Default::default()
16542 },
16543 lsp::Diagnostic {
16544 range: lsp::Range::new(
16545 lsp::Position::new(0, 25),
16546 lsp::Position::new(0, 28),
16547 ),
16548 severity: Some(lsp::DiagnosticSeverity::ERROR),
16549 ..Default::default()
16550 },
16551 ],
16552 },
16553 None,
16554 DiagnosticSourceKind::Pushed,
16555 &[],
16556 cx,
16557 )
16558 .unwrap()
16559 });
16560 });
16561
16562 executor.run_until_parked();
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 cx.update_editor(|editor, window, cx| {
16592 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16593 });
16594
16595 cx.assert_editor_state(indoc! {"
16596 fn func(abc def: i32) -> ˇu32 {
16597 }
16598 "});
16599}
16600
16601#[gpui::test]
16602async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16603 init_test(cx, |_| {});
16604
16605 let mut cx = EditorTestContext::new(cx).await;
16606
16607 let diff_base = r#"
16608 use some::mod;
16609
16610 const A: u32 = 42;
16611
16612 fn main() {
16613 println!("hello");
16614
16615 println!("world");
16616 }
16617 "#
16618 .unindent();
16619
16620 // Edits are modified, removed, modified, added
16621 cx.set_state(
16622 &r#"
16623 use some::modified;
16624
16625 ˇ
16626 fn main() {
16627 println!("hello there");
16628
16629 println!("around the");
16630 println!("world");
16631 }
16632 "#
16633 .unindent(),
16634 );
16635
16636 cx.set_head_text(&diff_base);
16637 executor.run_until_parked();
16638
16639 cx.update_editor(|editor, window, cx| {
16640 //Wrap around the bottom of the buffer
16641 for _ in 0..3 {
16642 editor.go_to_next_hunk(&GoToHunk, window, cx);
16643 }
16644 });
16645
16646 cx.assert_editor_state(
16647 &r#"
16648 ˇuse some::modified;
16649
16650
16651 fn main() {
16652 println!("hello there");
16653
16654 println!("around the");
16655 println!("world");
16656 }
16657 "#
16658 .unindent(),
16659 );
16660
16661 cx.update_editor(|editor, window, cx| {
16662 //Wrap around the top of the buffer
16663 for _ in 0..2 {
16664 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16665 }
16666 });
16667
16668 cx.assert_editor_state(
16669 &r#"
16670 use some::modified;
16671
16672
16673 fn main() {
16674 ˇ println!("hello there");
16675
16676 println!("around the");
16677 println!("world");
16678 }
16679 "#
16680 .unindent(),
16681 );
16682
16683 cx.update_editor(|editor, window, cx| {
16684 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16685 });
16686
16687 cx.assert_editor_state(
16688 &r#"
16689 use some::modified;
16690
16691 ˇ
16692 fn main() {
16693 println!("hello there");
16694
16695 println!("around the");
16696 println!("world");
16697 }
16698 "#
16699 .unindent(),
16700 );
16701
16702 cx.update_editor(|editor, window, cx| {
16703 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16704 });
16705
16706 cx.assert_editor_state(
16707 &r#"
16708 ˇuse some::modified;
16709
16710
16711 fn main() {
16712 println!("hello there");
16713
16714 println!("around the");
16715 println!("world");
16716 }
16717 "#
16718 .unindent(),
16719 );
16720
16721 cx.update_editor(|editor, window, cx| {
16722 for _ in 0..2 {
16723 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16724 }
16725 });
16726
16727 cx.assert_editor_state(
16728 &r#"
16729 use some::modified;
16730
16731
16732 fn main() {
16733 ˇ println!("hello there");
16734
16735 println!("around the");
16736 println!("world");
16737 }
16738 "#
16739 .unindent(),
16740 );
16741
16742 cx.update_editor(|editor, window, cx| {
16743 editor.fold(&Fold, window, cx);
16744 });
16745
16746 cx.update_editor(|editor, window, cx| {
16747 editor.go_to_next_hunk(&GoToHunk, window, cx);
16748 });
16749
16750 cx.assert_editor_state(
16751 &r#"
16752 ˇuse some::modified;
16753
16754
16755 fn main() {
16756 println!("hello there");
16757
16758 println!("around the");
16759 println!("world");
16760 }
16761 "#
16762 .unindent(),
16763 );
16764}
16765
16766#[test]
16767fn test_split_words() {
16768 fn split(text: &str) -> Vec<&str> {
16769 split_words(text).collect()
16770 }
16771
16772 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16773 assert_eq!(split("hello_world"), &["hello_", "world"]);
16774 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16775 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16776 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16777 assert_eq!(split("helloworld"), &["helloworld"]);
16778
16779 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16780}
16781
16782#[gpui::test]
16783async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16784 init_test(cx, |_| {});
16785
16786 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16787 let mut assert = |before, after| {
16788 let _state_context = cx.set_state(before);
16789 cx.run_until_parked();
16790 cx.update_editor(|editor, window, cx| {
16791 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16792 });
16793 cx.run_until_parked();
16794 cx.assert_editor_state(after);
16795 };
16796
16797 // Outside bracket jumps to outside of matching bracket
16798 assert("console.logˇ(var);", "console.log(var)ˇ;");
16799 assert("console.log(var)ˇ;", "console.logˇ(var);");
16800
16801 // Inside bracket jumps to inside of matching bracket
16802 assert("console.log(ˇvar);", "console.log(varˇ);");
16803 assert("console.log(varˇ);", "console.log(ˇvar);");
16804
16805 // When outside a bracket and inside, favor jumping to the inside bracket
16806 assert(
16807 "console.log('foo', [1, 2, 3]ˇ);",
16808 "console.log(ˇ'foo', [1, 2, 3]);",
16809 );
16810 assert(
16811 "console.log(ˇ'foo', [1, 2, 3]);",
16812 "console.log('foo', [1, 2, 3]ˇ);",
16813 );
16814
16815 // Bias forward if two options are equally likely
16816 assert(
16817 "let result = curried_fun()ˇ();",
16818 "let result = curried_fun()()ˇ;",
16819 );
16820
16821 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16822 assert(
16823 indoc! {"
16824 function test() {
16825 console.log('test')ˇ
16826 }"},
16827 indoc! {"
16828 function test() {
16829 console.logˇ('test')
16830 }"},
16831 );
16832}
16833
16834#[gpui::test]
16835async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16836 init_test(cx, |_| {});
16837
16838 let fs = FakeFs::new(cx.executor());
16839 fs.insert_tree(
16840 path!("/a"),
16841 json!({
16842 "main.rs": "fn main() { let a = 5; }",
16843 "other.rs": "// Test file",
16844 }),
16845 )
16846 .await;
16847 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16848
16849 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16850 language_registry.add(Arc::new(Language::new(
16851 LanguageConfig {
16852 name: "Rust".into(),
16853 matcher: LanguageMatcher {
16854 path_suffixes: vec!["rs".to_string()],
16855 ..Default::default()
16856 },
16857 brackets: BracketPairConfig {
16858 pairs: vec![BracketPair {
16859 start: "{".to_string(),
16860 end: "}".to_string(),
16861 close: true,
16862 surround: true,
16863 newline: true,
16864 }],
16865 disabled_scopes_by_bracket_ix: Vec::new(),
16866 },
16867 ..Default::default()
16868 },
16869 Some(tree_sitter_rust::LANGUAGE.into()),
16870 )));
16871 let mut fake_servers = language_registry.register_fake_lsp(
16872 "Rust",
16873 FakeLspAdapter {
16874 capabilities: lsp::ServerCapabilities {
16875 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16876 first_trigger_character: "{".to_string(),
16877 more_trigger_character: None,
16878 }),
16879 ..Default::default()
16880 },
16881 ..Default::default()
16882 },
16883 );
16884
16885 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16886
16887 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16888
16889 let worktree_id = workspace
16890 .update(cx, |workspace, _, cx| {
16891 workspace.project().update(cx, |project, cx| {
16892 project.worktrees(cx).next().unwrap().read(cx).id()
16893 })
16894 })
16895 .unwrap();
16896
16897 let buffer = project
16898 .update(cx, |project, cx| {
16899 project.open_local_buffer(path!("/a/main.rs"), cx)
16900 })
16901 .await
16902 .unwrap();
16903 let editor_handle = workspace
16904 .update(cx, |workspace, window, cx| {
16905 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16906 })
16907 .unwrap()
16908 .await
16909 .unwrap()
16910 .downcast::<Editor>()
16911 .unwrap();
16912
16913 cx.executor().start_waiting();
16914 let fake_server = fake_servers.next().await.unwrap();
16915
16916 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16917 |params, _| async move {
16918 assert_eq!(
16919 params.text_document_position.text_document.uri,
16920 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16921 );
16922 assert_eq!(
16923 params.text_document_position.position,
16924 lsp::Position::new(0, 21),
16925 );
16926
16927 Ok(Some(vec![lsp::TextEdit {
16928 new_text: "]".to_string(),
16929 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16930 }]))
16931 },
16932 );
16933
16934 editor_handle.update_in(cx, |editor, window, cx| {
16935 window.focus(&editor.focus_handle(cx));
16936 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16937 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16938 });
16939 editor.handle_input("{", window, cx);
16940 });
16941
16942 cx.executor().run_until_parked();
16943
16944 buffer.update(cx, |buffer, _| {
16945 assert_eq!(
16946 buffer.text(),
16947 "fn main() { let a = {5}; }",
16948 "No extra braces from on type formatting should appear in the buffer"
16949 )
16950 });
16951}
16952
16953#[gpui::test(iterations = 20, seeds(31))]
16954async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
16955 init_test(cx, |_| {});
16956
16957 let mut cx = EditorLspTestContext::new_rust(
16958 lsp::ServerCapabilities {
16959 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16960 first_trigger_character: ".".to_string(),
16961 more_trigger_character: None,
16962 }),
16963 ..Default::default()
16964 },
16965 cx,
16966 )
16967 .await;
16968
16969 cx.update_buffer(|buffer, _| {
16970 // This causes autoindent to be async.
16971 buffer.set_sync_parse_timeout(Duration::ZERO)
16972 });
16973
16974 cx.set_state("fn c() {\n d()ˇ\n}\n");
16975 cx.simulate_keystroke("\n");
16976 cx.run_until_parked();
16977
16978 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
16979 let mut request =
16980 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
16981 let buffer_cloned = buffer_cloned.clone();
16982 async move {
16983 buffer_cloned.update(&mut cx, |buffer, _| {
16984 assert_eq!(
16985 buffer.text(),
16986 "fn c() {\n d()\n .\n}\n",
16987 "OnTypeFormatting should triggered after autoindent applied"
16988 )
16989 })?;
16990
16991 Ok(Some(vec![]))
16992 }
16993 });
16994
16995 cx.simulate_keystroke(".");
16996 cx.run_until_parked();
16997
16998 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
16999 assert!(request.next().await.is_some());
17000 request.close();
17001 assert!(request.next().await.is_none());
17002}
17003
17004#[gpui::test]
17005async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17006 init_test(cx, |_| {});
17007
17008 let fs = FakeFs::new(cx.executor());
17009 fs.insert_tree(
17010 path!("/a"),
17011 json!({
17012 "main.rs": "fn main() { let a = 5; }",
17013 "other.rs": "// Test file",
17014 }),
17015 )
17016 .await;
17017
17018 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17019
17020 let server_restarts = Arc::new(AtomicUsize::new(0));
17021 let closure_restarts = Arc::clone(&server_restarts);
17022 let language_server_name = "test language server";
17023 let language_name: LanguageName = "Rust".into();
17024
17025 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17026 language_registry.add(Arc::new(Language::new(
17027 LanguageConfig {
17028 name: language_name.clone(),
17029 matcher: LanguageMatcher {
17030 path_suffixes: vec!["rs".to_string()],
17031 ..Default::default()
17032 },
17033 ..Default::default()
17034 },
17035 Some(tree_sitter_rust::LANGUAGE.into()),
17036 )));
17037 let mut fake_servers = language_registry.register_fake_lsp(
17038 "Rust",
17039 FakeLspAdapter {
17040 name: language_server_name,
17041 initialization_options: Some(json!({
17042 "testOptionValue": true
17043 })),
17044 initializer: Some(Box::new(move |fake_server| {
17045 let task_restarts = Arc::clone(&closure_restarts);
17046 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17047 task_restarts.fetch_add(1, atomic::Ordering::Release);
17048 futures::future::ready(Ok(()))
17049 });
17050 })),
17051 ..Default::default()
17052 },
17053 );
17054
17055 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17056 let _buffer = project
17057 .update(cx, |project, cx| {
17058 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17059 })
17060 .await
17061 .unwrap();
17062 let _fake_server = fake_servers.next().await.unwrap();
17063 update_test_language_settings(cx, |language_settings| {
17064 language_settings.languages.0.insert(
17065 language_name.clone().0,
17066 LanguageSettingsContent {
17067 tab_size: NonZeroU32::new(8),
17068 ..Default::default()
17069 },
17070 );
17071 });
17072 cx.executor().run_until_parked();
17073 assert_eq!(
17074 server_restarts.load(atomic::Ordering::Acquire),
17075 0,
17076 "Should not restart LSP server on an unrelated change"
17077 );
17078
17079 update_test_project_settings(cx, |project_settings| {
17080 project_settings.lsp.insert(
17081 "Some other server name".into(),
17082 LspSettings {
17083 binary: None,
17084 settings: None,
17085 initialization_options: Some(json!({
17086 "some other init value": false
17087 })),
17088 enable_lsp_tasks: false,
17089 fetch: None,
17090 },
17091 );
17092 });
17093 cx.executor().run_until_parked();
17094 assert_eq!(
17095 server_restarts.load(atomic::Ordering::Acquire),
17096 0,
17097 "Should not restart LSP server on an unrelated LSP settings change"
17098 );
17099
17100 update_test_project_settings(cx, |project_settings| {
17101 project_settings.lsp.insert(
17102 language_server_name.into(),
17103 LspSettings {
17104 binary: None,
17105 settings: None,
17106 initialization_options: Some(json!({
17107 "anotherInitValue": false
17108 })),
17109 enable_lsp_tasks: false,
17110 fetch: None,
17111 },
17112 );
17113 });
17114 cx.executor().run_until_parked();
17115 assert_eq!(
17116 server_restarts.load(atomic::Ordering::Acquire),
17117 1,
17118 "Should restart LSP server on a related LSP settings change"
17119 );
17120
17121 update_test_project_settings(cx, |project_settings| {
17122 project_settings.lsp.insert(
17123 language_server_name.into(),
17124 LspSettings {
17125 binary: None,
17126 settings: None,
17127 initialization_options: Some(json!({
17128 "anotherInitValue": false
17129 })),
17130 enable_lsp_tasks: false,
17131 fetch: None,
17132 },
17133 );
17134 });
17135 cx.executor().run_until_parked();
17136 assert_eq!(
17137 server_restarts.load(atomic::Ordering::Acquire),
17138 1,
17139 "Should not restart LSP server on a related LSP settings change that is the same"
17140 );
17141
17142 update_test_project_settings(cx, |project_settings| {
17143 project_settings.lsp.insert(
17144 language_server_name.into(),
17145 LspSettings {
17146 binary: None,
17147 settings: None,
17148 initialization_options: None,
17149 enable_lsp_tasks: false,
17150 fetch: None,
17151 },
17152 );
17153 });
17154 cx.executor().run_until_parked();
17155 assert_eq!(
17156 server_restarts.load(atomic::Ordering::Acquire),
17157 2,
17158 "Should restart LSP server on another related LSP settings change"
17159 );
17160}
17161
17162#[gpui::test]
17163async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17164 init_test(cx, |_| {});
17165
17166 let mut cx = EditorLspTestContext::new_rust(
17167 lsp::ServerCapabilities {
17168 completion_provider: Some(lsp::CompletionOptions {
17169 trigger_characters: Some(vec![".".to_string()]),
17170 resolve_provider: Some(true),
17171 ..Default::default()
17172 }),
17173 ..Default::default()
17174 },
17175 cx,
17176 )
17177 .await;
17178
17179 cx.set_state("fn main() { let a = 2ˇ; }");
17180 cx.simulate_keystroke(".");
17181 let completion_item = lsp::CompletionItem {
17182 label: "some".into(),
17183 kind: Some(lsp::CompletionItemKind::SNIPPET),
17184 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17185 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17186 kind: lsp::MarkupKind::Markdown,
17187 value: "```rust\nSome(2)\n```".to_string(),
17188 })),
17189 deprecated: Some(false),
17190 sort_text: Some("fffffff2".to_string()),
17191 filter_text: Some("some".to_string()),
17192 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17193 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17194 range: lsp::Range {
17195 start: lsp::Position {
17196 line: 0,
17197 character: 22,
17198 },
17199 end: lsp::Position {
17200 line: 0,
17201 character: 22,
17202 },
17203 },
17204 new_text: "Some(2)".to_string(),
17205 })),
17206 additional_text_edits: Some(vec![lsp::TextEdit {
17207 range: lsp::Range {
17208 start: lsp::Position {
17209 line: 0,
17210 character: 20,
17211 },
17212 end: lsp::Position {
17213 line: 0,
17214 character: 22,
17215 },
17216 },
17217 new_text: "".to_string(),
17218 }]),
17219 ..Default::default()
17220 };
17221
17222 let closure_completion_item = completion_item.clone();
17223 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17224 let task_completion_item = closure_completion_item.clone();
17225 async move {
17226 Ok(Some(lsp::CompletionResponse::Array(vec![
17227 task_completion_item,
17228 ])))
17229 }
17230 });
17231
17232 request.next().await;
17233
17234 cx.condition(|editor, _| editor.context_menu_visible())
17235 .await;
17236 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17237 editor
17238 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17239 .unwrap()
17240 });
17241 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17242
17243 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17244 let task_completion_item = completion_item.clone();
17245 async move { Ok(task_completion_item) }
17246 })
17247 .next()
17248 .await
17249 .unwrap();
17250 apply_additional_edits.await.unwrap();
17251 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17252}
17253
17254#[gpui::test]
17255async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17256 init_test(cx, |_| {});
17257
17258 let mut cx = EditorLspTestContext::new_rust(
17259 lsp::ServerCapabilities {
17260 completion_provider: Some(lsp::CompletionOptions {
17261 trigger_characters: Some(vec![".".to_string()]),
17262 resolve_provider: Some(true),
17263 ..Default::default()
17264 }),
17265 ..Default::default()
17266 },
17267 cx,
17268 )
17269 .await;
17270
17271 cx.set_state("fn main() { let a = 2ˇ; }");
17272 cx.simulate_keystroke(".");
17273
17274 let item1 = lsp::CompletionItem {
17275 label: "method id()".to_string(),
17276 filter_text: Some("id".to_string()),
17277 detail: None,
17278 documentation: None,
17279 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17280 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17281 new_text: ".id".to_string(),
17282 })),
17283 ..lsp::CompletionItem::default()
17284 };
17285
17286 let item2 = lsp::CompletionItem {
17287 label: "other".to_string(),
17288 filter_text: Some("other".to_string()),
17289 detail: None,
17290 documentation: None,
17291 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17292 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17293 new_text: ".other".to_string(),
17294 })),
17295 ..lsp::CompletionItem::default()
17296 };
17297
17298 let item1 = item1.clone();
17299 cx.set_request_handler::<lsp::request::Completion, _, _>({
17300 let item1 = item1.clone();
17301 move |_, _, _| {
17302 let item1 = item1.clone();
17303 let item2 = item2.clone();
17304 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17305 }
17306 })
17307 .next()
17308 .await;
17309
17310 cx.condition(|editor, _| editor.context_menu_visible())
17311 .await;
17312 cx.update_editor(|editor, _, _| {
17313 let context_menu = editor.context_menu.borrow_mut();
17314 let context_menu = context_menu
17315 .as_ref()
17316 .expect("Should have the context menu deployed");
17317 match context_menu {
17318 CodeContextMenu::Completions(completions_menu) => {
17319 let completions = completions_menu.completions.borrow_mut();
17320 assert_eq!(
17321 completions
17322 .iter()
17323 .map(|completion| &completion.label.text)
17324 .collect::<Vec<_>>(),
17325 vec!["method id()", "other"]
17326 )
17327 }
17328 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17329 }
17330 });
17331
17332 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17333 let item1 = item1.clone();
17334 move |_, item_to_resolve, _| {
17335 let item1 = item1.clone();
17336 async move {
17337 if item1 == item_to_resolve {
17338 Ok(lsp::CompletionItem {
17339 label: "method id()".to_string(),
17340 filter_text: Some("id".to_string()),
17341 detail: Some("Now resolved!".to_string()),
17342 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17343 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17344 range: lsp::Range::new(
17345 lsp::Position::new(0, 22),
17346 lsp::Position::new(0, 22),
17347 ),
17348 new_text: ".id".to_string(),
17349 })),
17350 ..lsp::CompletionItem::default()
17351 })
17352 } else {
17353 Ok(item_to_resolve)
17354 }
17355 }
17356 }
17357 })
17358 .next()
17359 .await
17360 .unwrap();
17361 cx.run_until_parked();
17362
17363 cx.update_editor(|editor, window, cx| {
17364 editor.context_menu_next(&Default::default(), window, cx);
17365 });
17366
17367 cx.update_editor(|editor, _, _| {
17368 let context_menu = editor.context_menu.borrow_mut();
17369 let context_menu = context_menu
17370 .as_ref()
17371 .expect("Should have the context menu deployed");
17372 match context_menu {
17373 CodeContextMenu::Completions(completions_menu) => {
17374 let completions = completions_menu.completions.borrow_mut();
17375 assert_eq!(
17376 completions
17377 .iter()
17378 .map(|completion| &completion.label.text)
17379 .collect::<Vec<_>>(),
17380 vec!["method id() Now resolved!", "other"],
17381 "Should update first completion label, but not second as the filter text did not match."
17382 );
17383 }
17384 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17385 }
17386 });
17387}
17388
17389#[gpui::test]
17390async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17391 init_test(cx, |_| {});
17392 let mut cx = EditorLspTestContext::new_rust(
17393 lsp::ServerCapabilities {
17394 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17395 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17396 completion_provider: Some(lsp::CompletionOptions {
17397 resolve_provider: Some(true),
17398 ..Default::default()
17399 }),
17400 ..Default::default()
17401 },
17402 cx,
17403 )
17404 .await;
17405 cx.set_state(indoc! {"
17406 struct TestStruct {
17407 field: i32
17408 }
17409
17410 fn mainˇ() {
17411 let unused_var = 42;
17412 let test_struct = TestStruct { field: 42 };
17413 }
17414 "});
17415 let symbol_range = cx.lsp_range(indoc! {"
17416 struct TestStruct {
17417 field: i32
17418 }
17419
17420 «fn main»() {
17421 let unused_var = 42;
17422 let test_struct = TestStruct { field: 42 };
17423 }
17424 "});
17425 let mut hover_requests =
17426 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17427 Ok(Some(lsp::Hover {
17428 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17429 kind: lsp::MarkupKind::Markdown,
17430 value: "Function documentation".to_string(),
17431 }),
17432 range: Some(symbol_range),
17433 }))
17434 });
17435
17436 // Case 1: Test that code action menu hide hover popover
17437 cx.dispatch_action(Hover);
17438 hover_requests.next().await;
17439 cx.condition(|editor, _| editor.hover_state.visible()).await;
17440 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17441 move |_, _, _| async move {
17442 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17443 lsp::CodeAction {
17444 title: "Remove unused variable".to_string(),
17445 kind: Some(CodeActionKind::QUICKFIX),
17446 edit: Some(lsp::WorkspaceEdit {
17447 changes: Some(
17448 [(
17449 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17450 vec![lsp::TextEdit {
17451 range: lsp::Range::new(
17452 lsp::Position::new(5, 4),
17453 lsp::Position::new(5, 27),
17454 ),
17455 new_text: "".to_string(),
17456 }],
17457 )]
17458 .into_iter()
17459 .collect(),
17460 ),
17461 ..Default::default()
17462 }),
17463 ..Default::default()
17464 },
17465 )]))
17466 },
17467 );
17468 cx.update_editor(|editor, window, cx| {
17469 editor.toggle_code_actions(
17470 &ToggleCodeActions {
17471 deployed_from: None,
17472 quick_launch: false,
17473 },
17474 window,
17475 cx,
17476 );
17477 });
17478 code_action_requests.next().await;
17479 cx.run_until_parked();
17480 cx.condition(|editor, _| editor.context_menu_visible())
17481 .await;
17482 cx.update_editor(|editor, _, _| {
17483 assert!(
17484 !editor.hover_state.visible(),
17485 "Hover popover should be hidden when code action menu is shown"
17486 );
17487 // Hide code actions
17488 editor.context_menu.take();
17489 });
17490
17491 // Case 2: Test that code completions hide hover popover
17492 cx.dispatch_action(Hover);
17493 hover_requests.next().await;
17494 cx.condition(|editor, _| editor.hover_state.visible()).await;
17495 let counter = Arc::new(AtomicUsize::new(0));
17496 let mut completion_requests =
17497 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17498 let counter = counter.clone();
17499 async move {
17500 counter.fetch_add(1, atomic::Ordering::Release);
17501 Ok(Some(lsp::CompletionResponse::Array(vec![
17502 lsp::CompletionItem {
17503 label: "main".into(),
17504 kind: Some(lsp::CompletionItemKind::FUNCTION),
17505 detail: Some("() -> ()".to_string()),
17506 ..Default::default()
17507 },
17508 lsp::CompletionItem {
17509 label: "TestStruct".into(),
17510 kind: Some(lsp::CompletionItemKind::STRUCT),
17511 detail: Some("struct TestStruct".to_string()),
17512 ..Default::default()
17513 },
17514 ])))
17515 }
17516 });
17517 cx.update_editor(|editor, window, cx| {
17518 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17519 });
17520 completion_requests.next().await;
17521 cx.condition(|editor, _| editor.context_menu_visible())
17522 .await;
17523 cx.update_editor(|editor, _, _| {
17524 assert!(
17525 !editor.hover_state.visible(),
17526 "Hover popover should be hidden when completion menu is shown"
17527 );
17528 });
17529}
17530
17531#[gpui::test]
17532async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17533 init_test(cx, |_| {});
17534
17535 let mut cx = EditorLspTestContext::new_rust(
17536 lsp::ServerCapabilities {
17537 completion_provider: Some(lsp::CompletionOptions {
17538 trigger_characters: Some(vec![".".to_string()]),
17539 resolve_provider: Some(true),
17540 ..Default::default()
17541 }),
17542 ..Default::default()
17543 },
17544 cx,
17545 )
17546 .await;
17547
17548 cx.set_state("fn main() { let a = 2ˇ; }");
17549 cx.simulate_keystroke(".");
17550
17551 let unresolved_item_1 = lsp::CompletionItem {
17552 label: "id".to_string(),
17553 filter_text: Some("id".to_string()),
17554 detail: None,
17555 documentation: None,
17556 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17557 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17558 new_text: ".id".to_string(),
17559 })),
17560 ..lsp::CompletionItem::default()
17561 };
17562 let resolved_item_1 = lsp::CompletionItem {
17563 additional_text_edits: Some(vec![lsp::TextEdit {
17564 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17565 new_text: "!!".to_string(),
17566 }]),
17567 ..unresolved_item_1.clone()
17568 };
17569 let unresolved_item_2 = lsp::CompletionItem {
17570 label: "other".to_string(),
17571 filter_text: Some("other".to_string()),
17572 detail: None,
17573 documentation: None,
17574 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17575 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17576 new_text: ".other".to_string(),
17577 })),
17578 ..lsp::CompletionItem::default()
17579 };
17580 let resolved_item_2 = lsp::CompletionItem {
17581 additional_text_edits: Some(vec![lsp::TextEdit {
17582 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17583 new_text: "??".to_string(),
17584 }]),
17585 ..unresolved_item_2.clone()
17586 };
17587
17588 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17589 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17590 cx.lsp
17591 .server
17592 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17593 let unresolved_item_1 = unresolved_item_1.clone();
17594 let resolved_item_1 = resolved_item_1.clone();
17595 let unresolved_item_2 = unresolved_item_2.clone();
17596 let resolved_item_2 = resolved_item_2.clone();
17597 let resolve_requests_1 = resolve_requests_1.clone();
17598 let resolve_requests_2 = resolve_requests_2.clone();
17599 move |unresolved_request, _| {
17600 let unresolved_item_1 = unresolved_item_1.clone();
17601 let resolved_item_1 = resolved_item_1.clone();
17602 let unresolved_item_2 = unresolved_item_2.clone();
17603 let resolved_item_2 = resolved_item_2.clone();
17604 let resolve_requests_1 = resolve_requests_1.clone();
17605 let resolve_requests_2 = resolve_requests_2.clone();
17606 async move {
17607 if unresolved_request == unresolved_item_1 {
17608 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17609 Ok(resolved_item_1.clone())
17610 } else if unresolved_request == unresolved_item_2 {
17611 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17612 Ok(resolved_item_2.clone())
17613 } else {
17614 panic!("Unexpected completion item {unresolved_request:?}")
17615 }
17616 }
17617 }
17618 })
17619 .detach();
17620
17621 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17622 let unresolved_item_1 = unresolved_item_1.clone();
17623 let unresolved_item_2 = unresolved_item_2.clone();
17624 async move {
17625 Ok(Some(lsp::CompletionResponse::Array(vec![
17626 unresolved_item_1,
17627 unresolved_item_2,
17628 ])))
17629 }
17630 })
17631 .next()
17632 .await;
17633
17634 cx.condition(|editor, _| editor.context_menu_visible())
17635 .await;
17636 cx.update_editor(|editor, _, _| {
17637 let context_menu = editor.context_menu.borrow_mut();
17638 let context_menu = context_menu
17639 .as_ref()
17640 .expect("Should have the context menu deployed");
17641 match context_menu {
17642 CodeContextMenu::Completions(completions_menu) => {
17643 let completions = completions_menu.completions.borrow_mut();
17644 assert_eq!(
17645 completions
17646 .iter()
17647 .map(|completion| &completion.label.text)
17648 .collect::<Vec<_>>(),
17649 vec!["id", "other"]
17650 )
17651 }
17652 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17653 }
17654 });
17655 cx.run_until_parked();
17656
17657 cx.update_editor(|editor, window, cx| {
17658 editor.context_menu_next(&ContextMenuNext, window, cx);
17659 });
17660 cx.run_until_parked();
17661 cx.update_editor(|editor, window, cx| {
17662 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17663 });
17664 cx.run_until_parked();
17665 cx.update_editor(|editor, window, cx| {
17666 editor.context_menu_next(&ContextMenuNext, window, cx);
17667 });
17668 cx.run_until_parked();
17669 cx.update_editor(|editor, window, cx| {
17670 editor
17671 .compose_completion(&ComposeCompletion::default(), window, cx)
17672 .expect("No task returned")
17673 })
17674 .await
17675 .expect("Completion failed");
17676 cx.run_until_parked();
17677
17678 cx.update_editor(|editor, _, cx| {
17679 assert_eq!(
17680 resolve_requests_1.load(atomic::Ordering::Acquire),
17681 1,
17682 "Should always resolve once despite multiple selections"
17683 );
17684 assert_eq!(
17685 resolve_requests_2.load(atomic::Ordering::Acquire),
17686 1,
17687 "Should always resolve once after multiple selections and applying the completion"
17688 );
17689 assert_eq!(
17690 editor.text(cx),
17691 "fn main() { let a = ??.other; }",
17692 "Should use resolved data when applying the completion"
17693 );
17694 });
17695}
17696
17697#[gpui::test]
17698async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17699 init_test(cx, |_| {});
17700
17701 let item_0 = lsp::CompletionItem {
17702 label: "abs".into(),
17703 insert_text: Some("abs".into()),
17704 data: Some(json!({ "very": "special"})),
17705 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17706 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17707 lsp::InsertReplaceEdit {
17708 new_text: "abs".to_string(),
17709 insert: lsp::Range::default(),
17710 replace: lsp::Range::default(),
17711 },
17712 )),
17713 ..lsp::CompletionItem::default()
17714 };
17715 let items = iter::once(item_0.clone())
17716 .chain((11..51).map(|i| lsp::CompletionItem {
17717 label: format!("item_{}", i),
17718 insert_text: Some(format!("item_{}", i)),
17719 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17720 ..lsp::CompletionItem::default()
17721 }))
17722 .collect::<Vec<_>>();
17723
17724 let default_commit_characters = vec!["?".to_string()];
17725 let default_data = json!({ "default": "data"});
17726 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17727 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17728 let default_edit_range = lsp::Range {
17729 start: lsp::Position {
17730 line: 0,
17731 character: 5,
17732 },
17733 end: lsp::Position {
17734 line: 0,
17735 character: 5,
17736 },
17737 };
17738
17739 let mut cx = EditorLspTestContext::new_rust(
17740 lsp::ServerCapabilities {
17741 completion_provider: Some(lsp::CompletionOptions {
17742 trigger_characters: Some(vec![".".to_string()]),
17743 resolve_provider: Some(true),
17744 ..Default::default()
17745 }),
17746 ..Default::default()
17747 },
17748 cx,
17749 )
17750 .await;
17751
17752 cx.set_state("fn main() { let a = 2ˇ; }");
17753 cx.simulate_keystroke(".");
17754
17755 let completion_data = default_data.clone();
17756 let completion_characters = default_commit_characters.clone();
17757 let completion_items = items.clone();
17758 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17759 let default_data = completion_data.clone();
17760 let default_commit_characters = completion_characters.clone();
17761 let items = completion_items.clone();
17762 async move {
17763 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17764 items,
17765 item_defaults: Some(lsp::CompletionListItemDefaults {
17766 data: Some(default_data.clone()),
17767 commit_characters: Some(default_commit_characters.clone()),
17768 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17769 default_edit_range,
17770 )),
17771 insert_text_format: Some(default_insert_text_format),
17772 insert_text_mode: Some(default_insert_text_mode),
17773 }),
17774 ..lsp::CompletionList::default()
17775 })))
17776 }
17777 })
17778 .next()
17779 .await;
17780
17781 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17782 cx.lsp
17783 .server
17784 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17785 let closure_resolved_items = resolved_items.clone();
17786 move |item_to_resolve, _| {
17787 let closure_resolved_items = closure_resolved_items.clone();
17788 async move {
17789 closure_resolved_items.lock().push(item_to_resolve.clone());
17790 Ok(item_to_resolve)
17791 }
17792 }
17793 })
17794 .detach();
17795
17796 cx.condition(|editor, _| editor.context_menu_visible())
17797 .await;
17798 cx.run_until_parked();
17799 cx.update_editor(|editor, _, _| {
17800 let menu = editor.context_menu.borrow_mut();
17801 match menu.as_ref().expect("should have the completions menu") {
17802 CodeContextMenu::Completions(completions_menu) => {
17803 assert_eq!(
17804 completions_menu
17805 .entries
17806 .borrow()
17807 .iter()
17808 .map(|mat| mat.string.clone())
17809 .collect::<Vec<String>>(),
17810 items
17811 .iter()
17812 .map(|completion| completion.label.clone())
17813 .collect::<Vec<String>>()
17814 );
17815 }
17816 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17817 }
17818 });
17819 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17820 // with 4 from the end.
17821 assert_eq!(
17822 *resolved_items.lock(),
17823 [&items[0..16], &items[items.len() - 4..items.len()]]
17824 .concat()
17825 .iter()
17826 .cloned()
17827 .map(|mut item| {
17828 if item.data.is_none() {
17829 item.data = Some(default_data.clone());
17830 }
17831 item
17832 })
17833 .collect::<Vec<lsp::CompletionItem>>(),
17834 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17835 );
17836 resolved_items.lock().clear();
17837
17838 cx.update_editor(|editor, window, cx| {
17839 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17840 });
17841 cx.run_until_parked();
17842 // Completions that have already been resolved are skipped.
17843 assert_eq!(
17844 *resolved_items.lock(),
17845 items[items.len() - 17..items.len() - 4]
17846 .iter()
17847 .cloned()
17848 .map(|mut item| {
17849 if item.data.is_none() {
17850 item.data = Some(default_data.clone());
17851 }
17852 item
17853 })
17854 .collect::<Vec<lsp::CompletionItem>>()
17855 );
17856 resolved_items.lock().clear();
17857}
17858
17859#[gpui::test]
17860async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17861 init_test(cx, |_| {});
17862
17863 let mut cx = EditorLspTestContext::new(
17864 Language::new(
17865 LanguageConfig {
17866 matcher: LanguageMatcher {
17867 path_suffixes: vec!["jsx".into()],
17868 ..Default::default()
17869 },
17870 overrides: [(
17871 "element".into(),
17872 LanguageConfigOverride {
17873 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17874 ..Default::default()
17875 },
17876 )]
17877 .into_iter()
17878 .collect(),
17879 ..Default::default()
17880 },
17881 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17882 )
17883 .with_override_query("(jsx_self_closing_element) @element")
17884 .unwrap(),
17885 lsp::ServerCapabilities {
17886 completion_provider: Some(lsp::CompletionOptions {
17887 trigger_characters: Some(vec![":".to_string()]),
17888 ..Default::default()
17889 }),
17890 ..Default::default()
17891 },
17892 cx,
17893 )
17894 .await;
17895
17896 cx.lsp
17897 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17898 Ok(Some(lsp::CompletionResponse::Array(vec![
17899 lsp::CompletionItem {
17900 label: "bg-blue".into(),
17901 ..Default::default()
17902 },
17903 lsp::CompletionItem {
17904 label: "bg-red".into(),
17905 ..Default::default()
17906 },
17907 lsp::CompletionItem {
17908 label: "bg-yellow".into(),
17909 ..Default::default()
17910 },
17911 ])))
17912 });
17913
17914 cx.set_state(r#"<p class="bgˇ" />"#);
17915
17916 // Trigger completion when typing a dash, because the dash is an extra
17917 // word character in the 'element' scope, which contains the cursor.
17918 cx.simulate_keystroke("-");
17919 cx.executor().run_until_parked();
17920 cx.update_editor(|editor, _, _| {
17921 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17922 {
17923 assert_eq!(
17924 completion_menu_entries(menu),
17925 &["bg-blue", "bg-red", "bg-yellow"]
17926 );
17927 } else {
17928 panic!("expected completion menu to be open");
17929 }
17930 });
17931
17932 cx.simulate_keystroke("l");
17933 cx.executor().run_until_parked();
17934 cx.update_editor(|editor, _, _| {
17935 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17936 {
17937 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17938 } else {
17939 panic!("expected completion menu to be open");
17940 }
17941 });
17942
17943 // When filtering completions, consider the character after the '-' to
17944 // be the start of a subword.
17945 cx.set_state(r#"<p class="yelˇ" />"#);
17946 cx.simulate_keystroke("l");
17947 cx.executor().run_until_parked();
17948 cx.update_editor(|editor, _, _| {
17949 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17950 {
17951 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
17952 } else {
17953 panic!("expected completion menu to be open");
17954 }
17955 });
17956}
17957
17958fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
17959 let entries = menu.entries.borrow();
17960 entries.iter().map(|mat| mat.string.clone()).collect()
17961}
17962
17963#[gpui::test]
17964async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
17965 init_test(cx, |settings| {
17966 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
17967 Formatter::Prettier,
17968 )))
17969 });
17970
17971 let fs = FakeFs::new(cx.executor());
17972 fs.insert_file(path!("/file.ts"), Default::default()).await;
17973
17974 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
17975 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17976
17977 language_registry.add(Arc::new(Language::new(
17978 LanguageConfig {
17979 name: "TypeScript".into(),
17980 matcher: LanguageMatcher {
17981 path_suffixes: vec!["ts".to_string()],
17982 ..Default::default()
17983 },
17984 ..Default::default()
17985 },
17986 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
17987 )));
17988 update_test_language_settings(cx, |settings| {
17989 settings.defaults.prettier.get_or_insert_default().allowed = true;
17990 });
17991
17992 let test_plugin = "test_plugin";
17993 let _ = language_registry.register_fake_lsp(
17994 "TypeScript",
17995 FakeLspAdapter {
17996 prettier_plugins: vec![test_plugin],
17997 ..Default::default()
17998 },
17999 );
18000
18001 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18002 let buffer = project
18003 .update(cx, |project, cx| {
18004 project.open_local_buffer(path!("/file.ts"), cx)
18005 })
18006 .await
18007 .unwrap();
18008
18009 let buffer_text = "one\ntwo\nthree\n";
18010 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18011 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18012 editor.update_in(cx, |editor, window, cx| {
18013 editor.set_text(buffer_text, window, cx)
18014 });
18015
18016 editor
18017 .update_in(cx, |editor, window, cx| {
18018 editor.perform_format(
18019 project.clone(),
18020 FormatTrigger::Manual,
18021 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18022 window,
18023 cx,
18024 )
18025 })
18026 .unwrap()
18027 .await;
18028 assert_eq!(
18029 editor.update(cx, |editor, cx| editor.text(cx)),
18030 buffer_text.to_string() + prettier_format_suffix,
18031 "Test prettier formatting was not applied to the original buffer text",
18032 );
18033
18034 update_test_language_settings(cx, |settings| {
18035 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18036 });
18037 let format = editor.update_in(cx, |editor, window, cx| {
18038 editor.perform_format(
18039 project.clone(),
18040 FormatTrigger::Manual,
18041 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18042 window,
18043 cx,
18044 )
18045 });
18046 format.await.unwrap();
18047 assert_eq!(
18048 editor.update(cx, |editor, cx| editor.text(cx)),
18049 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18050 "Autoformatting (via test prettier) was not applied to the original buffer text",
18051 );
18052}
18053
18054#[gpui::test]
18055async fn test_addition_reverts(cx: &mut TestAppContext) {
18056 init_test(cx, |_| {});
18057 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18058 let base_text = indoc! {r#"
18059 struct Row;
18060 struct Row1;
18061 struct Row2;
18062
18063 struct Row4;
18064 struct Row5;
18065 struct Row6;
18066
18067 struct Row8;
18068 struct Row9;
18069 struct Row10;"#};
18070
18071 // When addition hunks are not adjacent to carets, no hunk revert is performed
18072 assert_hunk_revert(
18073 indoc! {r#"struct Row;
18074 struct Row1;
18075 struct Row1.1;
18076 struct Row1.2;
18077 struct Row2;ˇ
18078
18079 struct Row4;
18080 struct Row5;
18081 struct Row6;
18082
18083 struct Row8;
18084 ˇstruct Row9;
18085 struct Row9.1;
18086 struct Row9.2;
18087 struct Row9.3;
18088 struct Row10;"#},
18089 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18090 indoc! {r#"struct Row;
18091 struct Row1;
18092 struct Row1.1;
18093 struct Row1.2;
18094 struct Row2;ˇ
18095
18096 struct Row4;
18097 struct Row5;
18098 struct Row6;
18099
18100 struct Row8;
18101 ˇstruct Row9;
18102 struct Row9.1;
18103 struct Row9.2;
18104 struct Row9.3;
18105 struct Row10;"#},
18106 base_text,
18107 &mut cx,
18108 );
18109 // Same for selections
18110 assert_hunk_revert(
18111 indoc! {r#"struct Row;
18112 struct Row1;
18113 struct Row2;
18114 struct Row2.1;
18115 struct Row2.2;
18116 «ˇ
18117 struct Row4;
18118 struct» Row5;
18119 «struct Row6;
18120 ˇ»
18121 struct Row9.1;
18122 struct Row9.2;
18123 struct Row9.3;
18124 struct Row8;
18125 struct Row9;
18126 struct Row10;"#},
18127 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18128 indoc! {r#"struct Row;
18129 struct Row1;
18130 struct Row2;
18131 struct Row2.1;
18132 struct Row2.2;
18133 «ˇ
18134 struct Row4;
18135 struct» Row5;
18136 «struct Row6;
18137 ˇ»
18138 struct Row9.1;
18139 struct Row9.2;
18140 struct Row9.3;
18141 struct Row8;
18142 struct Row9;
18143 struct Row10;"#},
18144 base_text,
18145 &mut cx,
18146 );
18147
18148 // When carets and selections intersect the addition hunks, those are reverted.
18149 // Adjacent carets got merged.
18150 assert_hunk_revert(
18151 indoc! {r#"struct Row;
18152 ˇ// something on the top
18153 struct Row1;
18154 struct Row2;
18155 struct Roˇw3.1;
18156 struct Row2.2;
18157 struct Row2.3;ˇ
18158
18159 struct Row4;
18160 struct ˇRow5.1;
18161 struct Row5.2;
18162 struct «Rowˇ»5.3;
18163 struct Row5;
18164 struct Row6;
18165 ˇ
18166 struct Row9.1;
18167 struct «Rowˇ»9.2;
18168 struct «ˇRow»9.3;
18169 struct Row8;
18170 struct Row9;
18171 «ˇ// something on bottom»
18172 struct Row10;"#},
18173 vec![
18174 DiffHunkStatusKind::Added,
18175 DiffHunkStatusKind::Added,
18176 DiffHunkStatusKind::Added,
18177 DiffHunkStatusKind::Added,
18178 DiffHunkStatusKind::Added,
18179 ],
18180 indoc! {r#"struct Row;
18181 ˇstruct Row1;
18182 struct Row2;
18183 ˇ
18184 struct Row4;
18185 ˇstruct Row5;
18186 struct Row6;
18187 ˇ
18188 ˇstruct Row8;
18189 struct Row9;
18190 ˇstruct Row10;"#},
18191 base_text,
18192 &mut cx,
18193 );
18194}
18195
18196#[gpui::test]
18197async fn test_modification_reverts(cx: &mut TestAppContext) {
18198 init_test(cx, |_| {});
18199 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18200 let base_text = indoc! {r#"
18201 struct Row;
18202 struct Row1;
18203 struct Row2;
18204
18205 struct Row4;
18206 struct Row5;
18207 struct Row6;
18208
18209 struct Row8;
18210 struct Row9;
18211 struct Row10;"#};
18212
18213 // Modification hunks behave the same as the addition ones.
18214 assert_hunk_revert(
18215 indoc! {r#"struct Row;
18216 struct Row1;
18217 struct Row33;
18218 ˇ
18219 struct Row4;
18220 struct Row5;
18221 struct Row6;
18222 ˇ
18223 struct Row99;
18224 struct Row9;
18225 struct Row10;"#},
18226 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18227 indoc! {r#"struct Row;
18228 struct Row1;
18229 struct Row33;
18230 ˇ
18231 struct Row4;
18232 struct Row5;
18233 struct Row6;
18234 ˇ
18235 struct Row99;
18236 struct Row9;
18237 struct Row10;"#},
18238 base_text,
18239 &mut cx,
18240 );
18241 assert_hunk_revert(
18242 indoc! {r#"struct Row;
18243 struct Row1;
18244 struct Row33;
18245 «ˇ
18246 struct Row4;
18247 struct» Row5;
18248 «struct Row6;
18249 ˇ»
18250 struct Row99;
18251 struct Row9;
18252 struct Row10;"#},
18253 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18254 indoc! {r#"struct Row;
18255 struct Row1;
18256 struct Row33;
18257 «ˇ
18258 struct Row4;
18259 struct» Row5;
18260 «struct Row6;
18261 ˇ»
18262 struct Row99;
18263 struct Row9;
18264 struct Row10;"#},
18265 base_text,
18266 &mut cx,
18267 );
18268
18269 assert_hunk_revert(
18270 indoc! {r#"ˇstruct Row1.1;
18271 struct Row1;
18272 «ˇstr»uct Row22;
18273
18274 struct ˇRow44;
18275 struct Row5;
18276 struct «Rˇ»ow66;ˇ
18277
18278 «struˇ»ct Row88;
18279 struct Row9;
18280 struct Row1011;ˇ"#},
18281 vec![
18282 DiffHunkStatusKind::Modified,
18283 DiffHunkStatusKind::Modified,
18284 DiffHunkStatusKind::Modified,
18285 DiffHunkStatusKind::Modified,
18286 DiffHunkStatusKind::Modified,
18287 DiffHunkStatusKind::Modified,
18288 ],
18289 indoc! {r#"struct Row;
18290 ˇstruct Row1;
18291 struct Row2;
18292 ˇ
18293 struct Row4;
18294 ˇstruct Row5;
18295 struct Row6;
18296 ˇ
18297 struct Row8;
18298 ˇstruct Row9;
18299 struct Row10;ˇ"#},
18300 base_text,
18301 &mut cx,
18302 );
18303}
18304
18305#[gpui::test]
18306async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18307 init_test(cx, |_| {});
18308 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18309 let base_text = indoc! {r#"
18310 one
18311
18312 two
18313 three
18314 "#};
18315
18316 cx.set_head_text(base_text);
18317 cx.set_state("\nˇ\n");
18318 cx.executor().run_until_parked();
18319 cx.update_editor(|editor, _window, cx| {
18320 editor.expand_selected_diff_hunks(cx);
18321 });
18322 cx.executor().run_until_parked();
18323 cx.update_editor(|editor, window, cx| {
18324 editor.backspace(&Default::default(), window, cx);
18325 });
18326 cx.run_until_parked();
18327 cx.assert_state_with_diff(
18328 indoc! {r#"
18329
18330 - two
18331 - threeˇ
18332 +
18333 "#}
18334 .to_string(),
18335 );
18336}
18337
18338#[gpui::test]
18339async fn test_deletion_reverts(cx: &mut TestAppContext) {
18340 init_test(cx, |_| {});
18341 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18342 let base_text = indoc! {r#"struct Row;
18343struct Row1;
18344struct Row2;
18345
18346struct Row4;
18347struct Row5;
18348struct Row6;
18349
18350struct Row8;
18351struct Row9;
18352struct Row10;"#};
18353
18354 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18355 assert_hunk_revert(
18356 indoc! {r#"struct Row;
18357 struct Row2;
18358
18359 ˇstruct Row4;
18360 struct Row5;
18361 struct Row6;
18362 ˇ
18363 struct Row8;
18364 struct Row10;"#},
18365 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18366 indoc! {r#"struct Row;
18367 struct Row2;
18368
18369 ˇstruct Row4;
18370 struct Row5;
18371 struct Row6;
18372 ˇ
18373 struct Row8;
18374 struct Row10;"#},
18375 base_text,
18376 &mut cx,
18377 );
18378 assert_hunk_revert(
18379 indoc! {r#"struct Row;
18380 struct Row2;
18381
18382 «ˇstruct Row4;
18383 struct» Row5;
18384 «struct Row6;
18385 ˇ»
18386 struct Row8;
18387 struct Row10;"#},
18388 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18389 indoc! {r#"struct Row;
18390 struct Row2;
18391
18392 «ˇstruct Row4;
18393 struct» Row5;
18394 «struct Row6;
18395 ˇ»
18396 struct Row8;
18397 struct Row10;"#},
18398 base_text,
18399 &mut cx,
18400 );
18401
18402 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18403 assert_hunk_revert(
18404 indoc! {r#"struct Row;
18405 ˇstruct Row2;
18406
18407 struct Row4;
18408 struct Row5;
18409 struct Row6;
18410
18411 struct Row8;ˇ
18412 struct Row10;"#},
18413 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18414 indoc! {r#"struct Row;
18415 struct Row1;
18416 ˇstruct Row2;
18417
18418 struct Row4;
18419 struct Row5;
18420 struct Row6;
18421
18422 struct Row8;ˇ
18423 struct Row9;
18424 struct Row10;"#},
18425 base_text,
18426 &mut cx,
18427 );
18428 assert_hunk_revert(
18429 indoc! {r#"struct Row;
18430 struct Row2«ˇ;
18431 struct Row4;
18432 struct» Row5;
18433 «struct Row6;
18434
18435 struct Row8;ˇ»
18436 struct Row10;"#},
18437 vec![
18438 DiffHunkStatusKind::Deleted,
18439 DiffHunkStatusKind::Deleted,
18440 DiffHunkStatusKind::Deleted,
18441 ],
18442 indoc! {r#"struct Row;
18443 struct Row1;
18444 struct Row2«ˇ;
18445
18446 struct Row4;
18447 struct» Row5;
18448 «struct Row6;
18449
18450 struct Row8;ˇ»
18451 struct Row9;
18452 struct Row10;"#},
18453 base_text,
18454 &mut cx,
18455 );
18456}
18457
18458#[gpui::test]
18459async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18460 init_test(cx, |_| {});
18461
18462 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18463 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18464 let base_text_3 =
18465 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18466
18467 let text_1 = edit_first_char_of_every_line(base_text_1);
18468 let text_2 = edit_first_char_of_every_line(base_text_2);
18469 let text_3 = edit_first_char_of_every_line(base_text_3);
18470
18471 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18472 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18473 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18474
18475 let multibuffer = cx.new(|cx| {
18476 let mut multibuffer = MultiBuffer::new(ReadWrite);
18477 multibuffer.push_excerpts(
18478 buffer_1.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_2.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.push_excerpts(
18496 buffer_3.clone(),
18497 [
18498 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18499 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18500 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18501 ],
18502 cx,
18503 );
18504 multibuffer
18505 });
18506
18507 let fs = FakeFs::new(cx.executor());
18508 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18509 let (editor, cx) = cx
18510 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18511 editor.update_in(cx, |editor, _window, cx| {
18512 for (buffer, diff_base) in [
18513 (buffer_1.clone(), base_text_1),
18514 (buffer_2.clone(), base_text_2),
18515 (buffer_3.clone(), base_text_3),
18516 ] {
18517 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18518 editor
18519 .buffer
18520 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18521 }
18522 });
18523 cx.executor().run_until_parked();
18524
18525 editor.update_in(cx, |editor, window, cx| {
18526 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}");
18527 editor.select_all(&SelectAll, window, cx);
18528 editor.git_restore(&Default::default(), window, cx);
18529 });
18530 cx.executor().run_until_parked();
18531
18532 // When all ranges are selected, all buffer hunks are reverted.
18533 editor.update(cx, |editor, cx| {
18534 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");
18535 });
18536 buffer_1.update(cx, |buffer, _| {
18537 assert_eq!(buffer.text(), base_text_1);
18538 });
18539 buffer_2.update(cx, |buffer, _| {
18540 assert_eq!(buffer.text(), base_text_2);
18541 });
18542 buffer_3.update(cx, |buffer, _| {
18543 assert_eq!(buffer.text(), base_text_3);
18544 });
18545
18546 editor.update_in(cx, |editor, window, cx| {
18547 editor.undo(&Default::default(), window, cx);
18548 });
18549
18550 editor.update_in(cx, |editor, window, cx| {
18551 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18552 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18553 });
18554 editor.git_restore(&Default::default(), window, cx);
18555 });
18556
18557 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18558 // but not affect buffer_2 and its related excerpts.
18559 editor.update(cx, |editor, cx| {
18560 assert_eq!(
18561 editor.text(cx),
18562 "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}"
18563 );
18564 });
18565 buffer_1.update(cx, |buffer, _| {
18566 assert_eq!(buffer.text(), base_text_1);
18567 });
18568 buffer_2.update(cx, |buffer, _| {
18569 assert_eq!(
18570 buffer.text(),
18571 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18572 );
18573 });
18574 buffer_3.update(cx, |buffer, _| {
18575 assert_eq!(
18576 buffer.text(),
18577 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18578 );
18579 });
18580
18581 fn edit_first_char_of_every_line(text: &str) -> String {
18582 text.split('\n')
18583 .map(|line| format!("X{}", &line[1..]))
18584 .collect::<Vec<_>>()
18585 .join("\n")
18586 }
18587}
18588
18589#[gpui::test]
18590async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18591 init_test(cx, |_| {});
18592
18593 let cols = 4;
18594 let rows = 10;
18595 let sample_text_1 = sample_text(rows, cols, 'a');
18596 assert_eq!(
18597 sample_text_1,
18598 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18599 );
18600 let sample_text_2 = sample_text(rows, cols, 'l');
18601 assert_eq!(
18602 sample_text_2,
18603 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18604 );
18605 let sample_text_3 = sample_text(rows, cols, 'v');
18606 assert_eq!(
18607 sample_text_3,
18608 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18609 );
18610
18611 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18612 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18613 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18614
18615 let multi_buffer = cx.new(|cx| {
18616 let mut multibuffer = MultiBuffer::new(ReadWrite);
18617 multibuffer.push_excerpts(
18618 buffer_1.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_2.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.push_excerpts(
18636 buffer_3.clone(),
18637 [
18638 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18639 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18640 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18641 ],
18642 cx,
18643 );
18644 multibuffer
18645 });
18646
18647 let fs = FakeFs::new(cx.executor());
18648 fs.insert_tree(
18649 "/a",
18650 json!({
18651 "main.rs": sample_text_1,
18652 "other.rs": sample_text_2,
18653 "lib.rs": sample_text_3,
18654 }),
18655 )
18656 .await;
18657 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18658 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18659 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18660 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18661 Editor::new(
18662 EditorMode::full(),
18663 multi_buffer,
18664 Some(project.clone()),
18665 window,
18666 cx,
18667 )
18668 });
18669 let multibuffer_item_id = workspace
18670 .update(cx, |workspace, window, cx| {
18671 assert!(
18672 workspace.active_item(cx).is_none(),
18673 "active item should be None before the first item is added"
18674 );
18675 workspace.add_item_to_active_pane(
18676 Box::new(multi_buffer_editor.clone()),
18677 None,
18678 true,
18679 window,
18680 cx,
18681 );
18682 let active_item = workspace
18683 .active_item(cx)
18684 .expect("should have an active item after adding the multi buffer");
18685 assert!(
18686 !active_item.is_singleton(cx),
18687 "A multi buffer was expected to active after adding"
18688 );
18689 active_item.item_id()
18690 })
18691 .unwrap();
18692 cx.executor().run_until_parked();
18693
18694 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18695 editor.change_selections(
18696 SelectionEffects::scroll(Autoscroll::Next),
18697 window,
18698 cx,
18699 |s| s.select_ranges(Some(1..2)),
18700 );
18701 editor.open_excerpts(&OpenExcerpts, window, cx);
18702 });
18703 cx.executor().run_until_parked();
18704 let first_item_id = workspace
18705 .update(cx, |workspace, window, cx| {
18706 let active_item = workspace
18707 .active_item(cx)
18708 .expect("should have an active item after navigating into the 1st buffer");
18709 let first_item_id = active_item.item_id();
18710 assert_ne!(
18711 first_item_id, multibuffer_item_id,
18712 "Should navigate into the 1st buffer and activate it"
18713 );
18714 assert!(
18715 active_item.is_singleton(cx),
18716 "New active item should be a singleton buffer"
18717 );
18718 assert_eq!(
18719 active_item
18720 .act_as::<Editor>(cx)
18721 .expect("should have navigated into an editor for the 1st buffer")
18722 .read(cx)
18723 .text(cx),
18724 sample_text_1
18725 );
18726
18727 workspace
18728 .go_back(workspace.active_pane().downgrade(), window, cx)
18729 .detach_and_log_err(cx);
18730
18731 first_item_id
18732 })
18733 .unwrap();
18734 cx.executor().run_until_parked();
18735 workspace
18736 .update(cx, |workspace, _, cx| {
18737 let active_item = workspace
18738 .active_item(cx)
18739 .expect("should have an active item after navigating back");
18740 assert_eq!(
18741 active_item.item_id(),
18742 multibuffer_item_id,
18743 "Should navigate back to the multi buffer"
18744 );
18745 assert!(!active_item.is_singleton(cx));
18746 })
18747 .unwrap();
18748
18749 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18750 editor.change_selections(
18751 SelectionEffects::scroll(Autoscroll::Next),
18752 window,
18753 cx,
18754 |s| s.select_ranges(Some(39..40)),
18755 );
18756 editor.open_excerpts(&OpenExcerpts, window, cx);
18757 });
18758 cx.executor().run_until_parked();
18759 let second_item_id = workspace
18760 .update(cx, |workspace, window, cx| {
18761 let active_item = workspace
18762 .active_item(cx)
18763 .expect("should have an active item after navigating into the 2nd buffer");
18764 let second_item_id = active_item.item_id();
18765 assert_ne!(
18766 second_item_id, multibuffer_item_id,
18767 "Should navigate away from the multibuffer"
18768 );
18769 assert_ne!(
18770 second_item_id, first_item_id,
18771 "Should navigate into the 2nd buffer and activate it"
18772 );
18773 assert!(
18774 active_item.is_singleton(cx),
18775 "New active item should be a singleton buffer"
18776 );
18777 assert_eq!(
18778 active_item
18779 .act_as::<Editor>(cx)
18780 .expect("should have navigated into an editor")
18781 .read(cx)
18782 .text(cx),
18783 sample_text_2
18784 );
18785
18786 workspace
18787 .go_back(workspace.active_pane().downgrade(), window, cx)
18788 .detach_and_log_err(cx);
18789
18790 second_item_id
18791 })
18792 .unwrap();
18793 cx.executor().run_until_parked();
18794 workspace
18795 .update(cx, |workspace, _, cx| {
18796 let active_item = workspace
18797 .active_item(cx)
18798 .expect("should have an active item after navigating back from the 2nd buffer");
18799 assert_eq!(
18800 active_item.item_id(),
18801 multibuffer_item_id,
18802 "Should navigate back from the 2nd buffer to the multi buffer"
18803 );
18804 assert!(!active_item.is_singleton(cx));
18805 })
18806 .unwrap();
18807
18808 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18809 editor.change_selections(
18810 SelectionEffects::scroll(Autoscroll::Next),
18811 window,
18812 cx,
18813 |s| s.select_ranges(Some(70..70)),
18814 );
18815 editor.open_excerpts(&OpenExcerpts, window, cx);
18816 });
18817 cx.executor().run_until_parked();
18818 workspace
18819 .update(cx, |workspace, window, cx| {
18820 let active_item = workspace
18821 .active_item(cx)
18822 .expect("should have an active item after navigating into the 3rd buffer");
18823 let third_item_id = active_item.item_id();
18824 assert_ne!(
18825 third_item_id, multibuffer_item_id,
18826 "Should navigate into the 3rd buffer and activate it"
18827 );
18828 assert_ne!(third_item_id, first_item_id);
18829 assert_ne!(third_item_id, second_item_id);
18830 assert!(
18831 active_item.is_singleton(cx),
18832 "New active item should be a singleton buffer"
18833 );
18834 assert_eq!(
18835 active_item
18836 .act_as::<Editor>(cx)
18837 .expect("should have navigated into an editor")
18838 .read(cx)
18839 .text(cx),
18840 sample_text_3
18841 );
18842
18843 workspace
18844 .go_back(workspace.active_pane().downgrade(), window, cx)
18845 .detach_and_log_err(cx);
18846 })
18847 .unwrap();
18848 cx.executor().run_until_parked();
18849 workspace
18850 .update(cx, |workspace, _, cx| {
18851 let active_item = workspace
18852 .active_item(cx)
18853 .expect("should have an active item after navigating back from the 3rd buffer");
18854 assert_eq!(
18855 active_item.item_id(),
18856 multibuffer_item_id,
18857 "Should navigate back from the 3rd buffer to the multi buffer"
18858 );
18859 assert!(!active_item.is_singleton(cx));
18860 })
18861 .unwrap();
18862}
18863
18864#[gpui::test]
18865async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18866 init_test(cx, |_| {});
18867
18868 let mut cx = EditorTestContext::new(cx).await;
18869
18870 let diff_base = r#"
18871 use some::mod;
18872
18873 const A: u32 = 42;
18874
18875 fn main() {
18876 println!("hello");
18877
18878 println!("world");
18879 }
18880 "#
18881 .unindent();
18882
18883 cx.set_state(
18884 &r#"
18885 use some::modified;
18886
18887 ˇ
18888 fn main() {
18889 println!("hello there");
18890
18891 println!("around the");
18892 println!("world");
18893 }
18894 "#
18895 .unindent(),
18896 );
18897
18898 cx.set_head_text(&diff_base);
18899 executor.run_until_parked();
18900
18901 cx.update_editor(|editor, window, cx| {
18902 editor.go_to_next_hunk(&GoToHunk, window, cx);
18903 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18904 });
18905 executor.run_until_parked();
18906 cx.assert_state_with_diff(
18907 r#"
18908 use some::modified;
18909
18910
18911 fn main() {
18912 - println!("hello");
18913 + ˇ println!("hello there");
18914
18915 println!("around the");
18916 println!("world");
18917 }
18918 "#
18919 .unindent(),
18920 );
18921
18922 cx.update_editor(|editor, window, cx| {
18923 for _ in 0..2 {
18924 editor.go_to_next_hunk(&GoToHunk, window, cx);
18925 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18926 }
18927 });
18928 executor.run_until_parked();
18929 cx.assert_state_with_diff(
18930 r#"
18931 - use some::mod;
18932 + ˇuse some::modified;
18933
18934
18935 fn main() {
18936 - println!("hello");
18937 + println!("hello there");
18938
18939 + println!("around the");
18940 println!("world");
18941 }
18942 "#
18943 .unindent(),
18944 );
18945
18946 cx.update_editor(|editor, window, cx| {
18947 editor.go_to_next_hunk(&GoToHunk, window, cx);
18948 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18949 });
18950 executor.run_until_parked();
18951 cx.assert_state_with_diff(
18952 r#"
18953 - use some::mod;
18954 + use some::modified;
18955
18956 - const A: u32 = 42;
18957 ˇ
18958 fn main() {
18959 - println!("hello");
18960 + println!("hello there");
18961
18962 + println!("around the");
18963 println!("world");
18964 }
18965 "#
18966 .unindent(),
18967 );
18968
18969 cx.update_editor(|editor, window, cx| {
18970 editor.cancel(&Cancel, window, cx);
18971 });
18972
18973 cx.assert_state_with_diff(
18974 r#"
18975 use some::modified;
18976
18977 ˇ
18978 fn main() {
18979 println!("hello there");
18980
18981 println!("around the");
18982 println!("world");
18983 }
18984 "#
18985 .unindent(),
18986 );
18987}
18988
18989#[gpui::test]
18990async fn test_diff_base_change_with_expanded_diff_hunks(
18991 executor: BackgroundExecutor,
18992 cx: &mut TestAppContext,
18993) {
18994 init_test(cx, |_| {});
18995
18996 let mut cx = EditorTestContext::new(cx).await;
18997
18998 let diff_base = r#"
18999 use some::mod1;
19000 use some::mod2;
19001
19002 const A: u32 = 42;
19003 const B: u32 = 42;
19004 const C: u32 = 42;
19005
19006 fn main() {
19007 println!("hello");
19008
19009 println!("world");
19010 }
19011 "#
19012 .unindent();
19013
19014 cx.set_state(
19015 &r#"
19016 use some::mod2;
19017
19018 const A: u32 = 42;
19019 const C: u32 = 42;
19020
19021 fn main(ˇ) {
19022 //println!("hello");
19023
19024 println!("world");
19025 //
19026 //
19027 }
19028 "#
19029 .unindent(),
19030 );
19031
19032 cx.set_head_text(&diff_base);
19033 executor.run_until_parked();
19034
19035 cx.update_editor(|editor, window, cx| {
19036 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19037 });
19038 executor.run_until_parked();
19039 cx.assert_state_with_diff(
19040 r#"
19041 - use some::mod1;
19042 use some::mod2;
19043
19044 const A: u32 = 42;
19045 - const B: u32 = 42;
19046 const C: u32 = 42;
19047
19048 fn main(ˇ) {
19049 - println!("hello");
19050 + //println!("hello");
19051
19052 println!("world");
19053 + //
19054 + //
19055 }
19056 "#
19057 .unindent(),
19058 );
19059
19060 cx.set_head_text("new diff base!");
19061 executor.run_until_parked();
19062 cx.assert_state_with_diff(
19063 r#"
19064 - new diff base!
19065 + use some::mod2;
19066 +
19067 + const A: u32 = 42;
19068 + const C: u32 = 42;
19069 +
19070 + fn main(ˇ) {
19071 + //println!("hello");
19072 +
19073 + println!("world");
19074 + //
19075 + //
19076 + }
19077 "#
19078 .unindent(),
19079 );
19080}
19081
19082#[gpui::test]
19083async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19084 init_test(cx, |_| {});
19085
19086 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19087 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19088 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19089 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19090 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19091 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19092
19093 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19094 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19095 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19096
19097 let multi_buffer = cx.new(|cx| {
19098 let mut multibuffer = MultiBuffer::new(ReadWrite);
19099 multibuffer.push_excerpts(
19100 buffer_1.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_2.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.push_excerpts(
19118 buffer_3.clone(),
19119 [
19120 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19121 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19122 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19123 ],
19124 cx,
19125 );
19126 multibuffer
19127 });
19128
19129 let editor =
19130 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19131 editor
19132 .update(cx, |editor, _window, cx| {
19133 for (buffer, diff_base) in [
19134 (buffer_1.clone(), file_1_old),
19135 (buffer_2.clone(), file_2_old),
19136 (buffer_3.clone(), file_3_old),
19137 ] {
19138 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19139 editor
19140 .buffer
19141 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19142 }
19143 })
19144 .unwrap();
19145
19146 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19147 cx.run_until_parked();
19148
19149 cx.assert_editor_state(
19150 &"
19151 ˇaaa
19152 ccc
19153 ddd
19154
19155 ggg
19156 hhh
19157
19158
19159 lll
19160 mmm
19161 NNN
19162
19163 qqq
19164 rrr
19165
19166 uuu
19167 111
19168 222
19169 333
19170
19171 666
19172 777
19173
19174 000
19175 !!!"
19176 .unindent(),
19177 );
19178
19179 cx.update_editor(|editor, window, cx| {
19180 editor.select_all(&SelectAll, window, cx);
19181 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19182 });
19183 cx.executor().run_until_parked();
19184
19185 cx.assert_state_with_diff(
19186 "
19187 «aaa
19188 - bbb
19189 ccc
19190 ddd
19191
19192 ggg
19193 hhh
19194
19195
19196 lll
19197 mmm
19198 - nnn
19199 + NNN
19200
19201 qqq
19202 rrr
19203
19204 uuu
19205 111
19206 222
19207 333
19208
19209 + 666
19210 777
19211
19212 000
19213 !!!ˇ»"
19214 .unindent(),
19215 );
19216}
19217
19218#[gpui::test]
19219async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19220 init_test(cx, |_| {});
19221
19222 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19223 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19224
19225 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19226 let multi_buffer = cx.new(|cx| {
19227 let mut multibuffer = MultiBuffer::new(ReadWrite);
19228 multibuffer.push_excerpts(
19229 buffer.clone(),
19230 [
19231 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19232 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19233 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19234 ],
19235 cx,
19236 );
19237 multibuffer
19238 });
19239
19240 let editor =
19241 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19242 editor
19243 .update(cx, |editor, _window, cx| {
19244 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19245 editor
19246 .buffer
19247 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19248 })
19249 .unwrap();
19250
19251 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19252 cx.run_until_parked();
19253
19254 cx.update_editor(|editor, window, cx| {
19255 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19256 });
19257 cx.executor().run_until_parked();
19258
19259 // When the start of a hunk coincides with the start of its excerpt,
19260 // the hunk is expanded. When the start of a a hunk is earlier than
19261 // the start of its excerpt, the hunk is not expanded.
19262 cx.assert_state_with_diff(
19263 "
19264 ˇaaa
19265 - bbb
19266 + BBB
19267
19268 - ddd
19269 - eee
19270 + DDD
19271 + EEE
19272 fff
19273
19274 iii
19275 "
19276 .unindent(),
19277 );
19278}
19279
19280#[gpui::test]
19281async fn test_edits_around_expanded_insertion_hunks(
19282 executor: BackgroundExecutor,
19283 cx: &mut TestAppContext,
19284) {
19285 init_test(cx, |_| {});
19286
19287 let mut cx = EditorTestContext::new(cx).await;
19288
19289 let diff_base = r#"
19290 use some::mod1;
19291 use some::mod2;
19292
19293 const A: u32 = 42;
19294
19295 fn main() {
19296 println!("hello");
19297
19298 println!("world");
19299 }
19300 "#
19301 .unindent();
19302 executor.run_until_parked();
19303 cx.set_state(
19304 &r#"
19305 use some::mod1;
19306 use some::mod2;
19307
19308 const A: u32 = 42;
19309 const B: u32 = 42;
19310 const C: u32 = 42;
19311 ˇ
19312
19313 fn main() {
19314 println!("hello");
19315
19316 println!("world");
19317 }
19318 "#
19319 .unindent(),
19320 );
19321
19322 cx.set_head_text(&diff_base);
19323 executor.run_until_parked();
19324
19325 cx.update_editor(|editor, window, cx| {
19326 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19327 });
19328 executor.run_until_parked();
19329
19330 cx.assert_state_with_diff(
19331 r#"
19332 use some::mod1;
19333 use some::mod2;
19334
19335 const A: u32 = 42;
19336 + const B: u32 = 42;
19337 + const C: u32 = 42;
19338 + ˇ
19339
19340 fn main() {
19341 println!("hello");
19342
19343 println!("world");
19344 }
19345 "#
19346 .unindent(),
19347 );
19348
19349 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19350 executor.run_until_parked();
19351
19352 cx.assert_state_with_diff(
19353 r#"
19354 use some::mod1;
19355 use some::mod2;
19356
19357 const A: u32 = 42;
19358 + const B: u32 = 42;
19359 + const C: u32 = 42;
19360 + const D: u32 = 42;
19361 + ˇ
19362
19363 fn main() {
19364 println!("hello");
19365
19366 println!("world");
19367 }
19368 "#
19369 .unindent(),
19370 );
19371
19372 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19373 executor.run_until_parked();
19374
19375 cx.assert_state_with_diff(
19376 r#"
19377 use some::mod1;
19378 use some::mod2;
19379
19380 const A: u32 = 42;
19381 + const B: u32 = 42;
19382 + const C: u32 = 42;
19383 + const D: u32 = 42;
19384 + const E: u32 = 42;
19385 + ˇ
19386
19387 fn main() {
19388 println!("hello");
19389
19390 println!("world");
19391 }
19392 "#
19393 .unindent(),
19394 );
19395
19396 cx.update_editor(|editor, window, cx| {
19397 editor.delete_line(&DeleteLine, window, cx);
19398 });
19399 executor.run_until_parked();
19400
19401 cx.assert_state_with_diff(
19402 r#"
19403 use some::mod1;
19404 use some::mod2;
19405
19406 const A: u32 = 42;
19407 + const B: u32 = 42;
19408 + const C: u32 = 42;
19409 + const D: u32 = 42;
19410 + const E: u32 = 42;
19411 ˇ
19412 fn main() {
19413 println!("hello");
19414
19415 println!("world");
19416 }
19417 "#
19418 .unindent(),
19419 );
19420
19421 cx.update_editor(|editor, window, cx| {
19422 editor.move_up(&MoveUp, window, cx);
19423 editor.delete_line(&DeleteLine, window, cx);
19424 editor.move_up(&MoveUp, window, cx);
19425 editor.delete_line(&DeleteLine, window, cx);
19426 editor.move_up(&MoveUp, window, cx);
19427 editor.delete_line(&DeleteLine, window, cx);
19428 });
19429 executor.run_until_parked();
19430 cx.assert_state_with_diff(
19431 r#"
19432 use some::mod1;
19433 use some::mod2;
19434
19435 const A: u32 = 42;
19436 + const B: u32 = 42;
19437 ˇ
19438 fn main() {
19439 println!("hello");
19440
19441 println!("world");
19442 }
19443 "#
19444 .unindent(),
19445 );
19446
19447 cx.update_editor(|editor, window, cx| {
19448 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19449 editor.delete_line(&DeleteLine, window, cx);
19450 });
19451 executor.run_until_parked();
19452 cx.assert_state_with_diff(
19453 r#"
19454 ˇ
19455 fn main() {
19456 println!("hello");
19457
19458 println!("world");
19459 }
19460 "#
19461 .unindent(),
19462 );
19463}
19464
19465#[gpui::test]
19466async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19467 init_test(cx, |_| {});
19468
19469 let mut cx = EditorTestContext::new(cx).await;
19470 cx.set_head_text(indoc! { "
19471 one
19472 two
19473 three
19474 four
19475 five
19476 "
19477 });
19478 cx.set_state(indoc! { "
19479 one
19480 ˇthree
19481 five
19482 "});
19483 cx.run_until_parked();
19484 cx.update_editor(|editor, window, cx| {
19485 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19486 });
19487 cx.assert_state_with_diff(
19488 indoc! { "
19489 one
19490 - two
19491 ˇthree
19492 - four
19493 five
19494 "}
19495 .to_string(),
19496 );
19497 cx.update_editor(|editor, window, cx| {
19498 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19499 });
19500
19501 cx.assert_state_with_diff(
19502 indoc! { "
19503 one
19504 ˇthree
19505 five
19506 "}
19507 .to_string(),
19508 );
19509
19510 cx.set_state(indoc! { "
19511 one
19512 ˇTWO
19513 three
19514 four
19515 five
19516 "});
19517 cx.run_until_parked();
19518 cx.update_editor(|editor, window, cx| {
19519 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19520 });
19521
19522 cx.assert_state_with_diff(
19523 indoc! { "
19524 one
19525 - two
19526 + ˇTWO
19527 three
19528 four
19529 five
19530 "}
19531 .to_string(),
19532 );
19533 cx.update_editor(|editor, window, cx| {
19534 editor.move_up(&Default::default(), window, cx);
19535 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19536 });
19537 cx.assert_state_with_diff(
19538 indoc! { "
19539 one
19540 ˇTWO
19541 three
19542 four
19543 five
19544 "}
19545 .to_string(),
19546 );
19547}
19548
19549#[gpui::test]
19550async fn test_edits_around_expanded_deletion_hunks(
19551 executor: BackgroundExecutor,
19552 cx: &mut TestAppContext,
19553) {
19554 init_test(cx, |_| {});
19555
19556 let mut cx = EditorTestContext::new(cx).await;
19557
19558 let diff_base = r#"
19559 use some::mod1;
19560 use some::mod2;
19561
19562 const A: u32 = 42;
19563 const B: u32 = 42;
19564 const C: u32 = 42;
19565
19566
19567 fn main() {
19568 println!("hello");
19569
19570 println!("world");
19571 }
19572 "#
19573 .unindent();
19574 executor.run_until_parked();
19575 cx.set_state(
19576 &r#"
19577 use some::mod1;
19578 use some::mod2;
19579
19580 ˇconst B: u32 = 42;
19581 const C: u32 = 42;
19582
19583
19584 fn main() {
19585 println!("hello");
19586
19587 println!("world");
19588 }
19589 "#
19590 .unindent(),
19591 );
19592
19593 cx.set_head_text(&diff_base);
19594 executor.run_until_parked();
19595
19596 cx.update_editor(|editor, window, cx| {
19597 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19598 });
19599 executor.run_until_parked();
19600
19601 cx.assert_state_with_diff(
19602 r#"
19603 use some::mod1;
19604 use some::mod2;
19605
19606 - const A: u32 = 42;
19607 ˇconst B: u32 = 42;
19608 const C: u32 = 42;
19609
19610
19611 fn main() {
19612 println!("hello");
19613
19614 println!("world");
19615 }
19616 "#
19617 .unindent(),
19618 );
19619
19620 cx.update_editor(|editor, window, cx| {
19621 editor.delete_line(&DeleteLine, window, cx);
19622 });
19623 executor.run_until_parked();
19624 cx.assert_state_with_diff(
19625 r#"
19626 use some::mod1;
19627 use some::mod2;
19628
19629 - const A: u32 = 42;
19630 - const B: u32 = 42;
19631 ˇconst C: u32 = 42;
19632
19633
19634 fn main() {
19635 println!("hello");
19636
19637 println!("world");
19638 }
19639 "#
19640 .unindent(),
19641 );
19642
19643 cx.update_editor(|editor, window, cx| {
19644 editor.delete_line(&DeleteLine, window, cx);
19645 });
19646 executor.run_until_parked();
19647 cx.assert_state_with_diff(
19648 r#"
19649 use some::mod1;
19650 use some::mod2;
19651
19652 - const A: u32 = 42;
19653 - const B: u32 = 42;
19654 - const C: u32 = 42;
19655 ˇ
19656
19657 fn main() {
19658 println!("hello");
19659
19660 println!("world");
19661 }
19662 "#
19663 .unindent(),
19664 );
19665
19666 cx.update_editor(|editor, window, cx| {
19667 editor.handle_input("replacement", window, cx);
19668 });
19669 executor.run_until_parked();
19670 cx.assert_state_with_diff(
19671 r#"
19672 use some::mod1;
19673 use some::mod2;
19674
19675 - const A: u32 = 42;
19676 - const B: u32 = 42;
19677 - const C: u32 = 42;
19678 -
19679 + replacementˇ
19680
19681 fn main() {
19682 println!("hello");
19683
19684 println!("world");
19685 }
19686 "#
19687 .unindent(),
19688 );
19689}
19690
19691#[gpui::test]
19692async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19693 init_test(cx, |_| {});
19694
19695 let mut cx = EditorTestContext::new(cx).await;
19696
19697 let base_text = r#"
19698 one
19699 two
19700 three
19701 four
19702 five
19703 "#
19704 .unindent();
19705 executor.run_until_parked();
19706 cx.set_state(
19707 &r#"
19708 one
19709 two
19710 fˇour
19711 five
19712 "#
19713 .unindent(),
19714 );
19715
19716 cx.set_head_text(&base_text);
19717 executor.run_until_parked();
19718
19719 cx.update_editor(|editor, window, cx| {
19720 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19721 });
19722 executor.run_until_parked();
19723
19724 cx.assert_state_with_diff(
19725 r#"
19726 one
19727 two
19728 - three
19729 fˇour
19730 five
19731 "#
19732 .unindent(),
19733 );
19734
19735 cx.update_editor(|editor, window, cx| {
19736 editor.backspace(&Backspace, window, cx);
19737 editor.backspace(&Backspace, window, cx);
19738 });
19739 executor.run_until_parked();
19740 cx.assert_state_with_diff(
19741 r#"
19742 one
19743 two
19744 - threeˇ
19745 - four
19746 + our
19747 five
19748 "#
19749 .unindent(),
19750 );
19751}
19752
19753#[gpui::test]
19754async fn test_edit_after_expanded_modification_hunk(
19755 executor: BackgroundExecutor,
19756 cx: &mut TestAppContext,
19757) {
19758 init_test(cx, |_| {});
19759
19760 let mut cx = EditorTestContext::new(cx).await;
19761
19762 let diff_base = r#"
19763 use some::mod1;
19764 use some::mod2;
19765
19766 const A: u32 = 42;
19767 const B: u32 = 42;
19768 const C: u32 = 42;
19769 const D: u32 = 42;
19770
19771
19772 fn main() {
19773 println!("hello");
19774
19775 println!("world");
19776 }"#
19777 .unindent();
19778
19779 cx.set_state(
19780 &r#"
19781 use some::mod1;
19782 use some::mod2;
19783
19784 const A: u32 = 42;
19785 const B: u32 = 42;
19786 const C: u32 = 43ˇ
19787 const D: u32 = 42;
19788
19789
19790 fn main() {
19791 println!("hello");
19792
19793 println!("world");
19794 }"#
19795 .unindent(),
19796 );
19797
19798 cx.set_head_text(&diff_base);
19799 executor.run_until_parked();
19800 cx.update_editor(|editor, window, cx| {
19801 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19802 });
19803 executor.run_until_parked();
19804
19805 cx.assert_state_with_diff(
19806 r#"
19807 use some::mod1;
19808 use some::mod2;
19809
19810 const A: u32 = 42;
19811 const B: u32 = 42;
19812 - const C: u32 = 42;
19813 + const C: u32 = 43ˇ
19814 const D: u32 = 42;
19815
19816
19817 fn main() {
19818 println!("hello");
19819
19820 println!("world");
19821 }"#
19822 .unindent(),
19823 );
19824
19825 cx.update_editor(|editor, window, cx| {
19826 editor.handle_input("\nnew_line\n", window, cx);
19827 });
19828 executor.run_until_parked();
19829
19830 cx.assert_state_with_diff(
19831 r#"
19832 use some::mod1;
19833 use some::mod2;
19834
19835 const A: u32 = 42;
19836 const B: u32 = 42;
19837 - const C: u32 = 42;
19838 + const C: u32 = 43
19839 + new_line
19840 + ˇ
19841 const D: u32 = 42;
19842
19843
19844 fn main() {
19845 println!("hello");
19846
19847 println!("world");
19848 }"#
19849 .unindent(),
19850 );
19851}
19852
19853#[gpui::test]
19854async fn test_stage_and_unstage_added_file_hunk(
19855 executor: BackgroundExecutor,
19856 cx: &mut TestAppContext,
19857) {
19858 init_test(cx, |_| {});
19859
19860 let mut cx = EditorTestContext::new(cx).await;
19861 cx.update_editor(|editor, _, cx| {
19862 editor.set_expand_all_diff_hunks(cx);
19863 });
19864
19865 let working_copy = r#"
19866 ˇfn main() {
19867 println!("hello, world!");
19868 }
19869 "#
19870 .unindent();
19871
19872 cx.set_state(&working_copy);
19873 executor.run_until_parked();
19874
19875 cx.assert_state_with_diff(
19876 r#"
19877 + ˇfn main() {
19878 + println!("hello, world!");
19879 + }
19880 "#
19881 .unindent(),
19882 );
19883 cx.assert_index_text(None);
19884
19885 cx.update_editor(|editor, window, cx| {
19886 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19887 });
19888 executor.run_until_parked();
19889 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19890 cx.assert_state_with_diff(
19891 r#"
19892 + ˇfn main() {
19893 + println!("hello, world!");
19894 + }
19895 "#
19896 .unindent(),
19897 );
19898
19899 cx.update_editor(|editor, window, cx| {
19900 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19901 });
19902 executor.run_until_parked();
19903 cx.assert_index_text(None);
19904}
19905
19906async fn setup_indent_guides_editor(
19907 text: &str,
19908 cx: &mut TestAppContext,
19909) -> (BufferId, EditorTestContext) {
19910 init_test(cx, |_| {});
19911
19912 let mut cx = EditorTestContext::new(cx).await;
19913
19914 let buffer_id = cx.update_editor(|editor, window, cx| {
19915 editor.set_text(text, window, cx);
19916 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19917
19918 buffer_ids[0]
19919 });
19920
19921 (buffer_id, cx)
19922}
19923
19924fn assert_indent_guides(
19925 range: Range<u32>,
19926 expected: Vec<IndentGuide>,
19927 active_indices: Option<Vec<usize>>,
19928 cx: &mut EditorTestContext,
19929) {
19930 let indent_guides = cx.update_editor(|editor, window, cx| {
19931 let snapshot = editor.snapshot(window, cx).display_snapshot;
19932 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19933 editor,
19934 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19935 true,
19936 &snapshot,
19937 cx,
19938 );
19939
19940 indent_guides.sort_by(|a, b| {
19941 a.depth.cmp(&b.depth).then(
19942 a.start_row
19943 .cmp(&b.start_row)
19944 .then(a.end_row.cmp(&b.end_row)),
19945 )
19946 });
19947 indent_guides
19948 });
19949
19950 if let Some(expected) = active_indices {
19951 let active_indices = cx.update_editor(|editor, window, cx| {
19952 let snapshot = editor.snapshot(window, cx).display_snapshot;
19953 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
19954 });
19955
19956 assert_eq!(
19957 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
19958 expected,
19959 "Active indent guide indices do not match"
19960 );
19961 }
19962
19963 assert_eq!(indent_guides, expected, "Indent guides do not match");
19964}
19965
19966fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
19967 IndentGuide {
19968 buffer_id,
19969 start_row: MultiBufferRow(start_row),
19970 end_row: MultiBufferRow(end_row),
19971 depth,
19972 tab_size: 4,
19973 settings: IndentGuideSettings {
19974 enabled: true,
19975 line_width: 1,
19976 active_line_width: 1,
19977 ..Default::default()
19978 },
19979 }
19980}
19981
19982#[gpui::test]
19983async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
19984 let (buffer_id, mut cx) = setup_indent_guides_editor(
19985 &"
19986 fn main() {
19987 let a = 1;
19988 }"
19989 .unindent(),
19990 cx,
19991 )
19992 .await;
19993
19994 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
19995}
19996
19997#[gpui::test]
19998async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
19999 let (buffer_id, mut cx) = setup_indent_guides_editor(
20000 &"
20001 fn main() {
20002 let a = 1;
20003 let b = 2;
20004 }"
20005 .unindent(),
20006 cx,
20007 )
20008 .await;
20009
20010 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20011}
20012
20013#[gpui::test]
20014async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20015 let (buffer_id, mut cx) = setup_indent_guides_editor(
20016 &"
20017 fn main() {
20018 let a = 1;
20019 if a == 3 {
20020 let b = 2;
20021 } else {
20022 let c = 3;
20023 }
20024 }"
20025 .unindent(),
20026 cx,
20027 )
20028 .await;
20029
20030 assert_indent_guides(
20031 0..8,
20032 vec![
20033 indent_guide(buffer_id, 1, 6, 0),
20034 indent_guide(buffer_id, 3, 3, 1),
20035 indent_guide(buffer_id, 5, 5, 1),
20036 ],
20037 None,
20038 &mut cx,
20039 );
20040}
20041
20042#[gpui::test]
20043async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20044 let (buffer_id, mut cx) = setup_indent_guides_editor(
20045 &"
20046 fn main() {
20047 let a = 1;
20048 let b = 2;
20049 let c = 3;
20050 }"
20051 .unindent(),
20052 cx,
20053 )
20054 .await;
20055
20056 assert_indent_guides(
20057 0..5,
20058 vec![
20059 indent_guide(buffer_id, 1, 3, 0),
20060 indent_guide(buffer_id, 2, 2, 1),
20061 ],
20062 None,
20063 &mut cx,
20064 );
20065}
20066
20067#[gpui::test]
20068async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20069 let (buffer_id, mut cx) = setup_indent_guides_editor(
20070 &"
20071 fn main() {
20072 let a = 1;
20073
20074 let c = 3;
20075 }"
20076 .unindent(),
20077 cx,
20078 )
20079 .await;
20080
20081 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20082}
20083
20084#[gpui::test]
20085async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20086 let (buffer_id, mut cx) = setup_indent_guides_editor(
20087 &"
20088 fn main() {
20089 let a = 1;
20090
20091 let c = 3;
20092
20093 if a == 3 {
20094 let b = 2;
20095 } else {
20096 let c = 3;
20097 }
20098 }"
20099 .unindent(),
20100 cx,
20101 )
20102 .await;
20103
20104 assert_indent_guides(
20105 0..11,
20106 vec![
20107 indent_guide(buffer_id, 1, 9, 0),
20108 indent_guide(buffer_id, 6, 6, 1),
20109 indent_guide(buffer_id, 8, 8, 1),
20110 ],
20111 None,
20112 &mut cx,
20113 );
20114}
20115
20116#[gpui::test]
20117async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20118 let (buffer_id, mut cx) = setup_indent_guides_editor(
20119 &"
20120 fn main() {
20121 let a = 1;
20122
20123 let c = 3;
20124
20125 if a == 3 {
20126 let b = 2;
20127 } else {
20128 let c = 3;
20129 }
20130 }"
20131 .unindent(),
20132 cx,
20133 )
20134 .await;
20135
20136 assert_indent_guides(
20137 1..11,
20138 vec![
20139 indent_guide(buffer_id, 1, 9, 0),
20140 indent_guide(buffer_id, 6, 6, 1),
20141 indent_guide(buffer_id, 8, 8, 1),
20142 ],
20143 None,
20144 &mut cx,
20145 );
20146}
20147
20148#[gpui::test]
20149async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20150 let (buffer_id, mut cx) = setup_indent_guides_editor(
20151 &"
20152 fn main() {
20153 let a = 1;
20154
20155 let c = 3;
20156
20157 if a == 3 {
20158 let b = 2;
20159 } else {
20160 let c = 3;
20161 }
20162 }"
20163 .unindent(),
20164 cx,
20165 )
20166 .await;
20167
20168 assert_indent_guides(
20169 1..10,
20170 vec![
20171 indent_guide(buffer_id, 1, 9, 0),
20172 indent_guide(buffer_id, 6, 6, 1),
20173 indent_guide(buffer_id, 8, 8, 1),
20174 ],
20175 None,
20176 &mut cx,
20177 );
20178}
20179
20180#[gpui::test]
20181async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20182 let (buffer_id, mut cx) = setup_indent_guides_editor(
20183 &"
20184 fn main() {
20185 if a {
20186 b(
20187 c,
20188 d,
20189 )
20190 } else {
20191 e(
20192 f
20193 )
20194 }
20195 }"
20196 .unindent(),
20197 cx,
20198 )
20199 .await;
20200
20201 assert_indent_guides(
20202 0..11,
20203 vec![
20204 indent_guide(buffer_id, 1, 10, 0),
20205 indent_guide(buffer_id, 2, 5, 1),
20206 indent_guide(buffer_id, 7, 9, 1),
20207 indent_guide(buffer_id, 3, 4, 2),
20208 indent_guide(buffer_id, 8, 8, 2),
20209 ],
20210 None,
20211 &mut cx,
20212 );
20213
20214 cx.update_editor(|editor, window, cx| {
20215 editor.fold_at(MultiBufferRow(2), window, cx);
20216 assert_eq!(
20217 editor.display_text(cx),
20218 "
20219 fn main() {
20220 if a {
20221 b(⋯
20222 )
20223 } else {
20224 e(
20225 f
20226 )
20227 }
20228 }"
20229 .unindent()
20230 );
20231 });
20232
20233 assert_indent_guides(
20234 0..11,
20235 vec![
20236 indent_guide(buffer_id, 1, 10, 0),
20237 indent_guide(buffer_id, 2, 5, 1),
20238 indent_guide(buffer_id, 7, 9, 1),
20239 indent_guide(buffer_id, 8, 8, 2),
20240 ],
20241 None,
20242 &mut cx,
20243 );
20244}
20245
20246#[gpui::test]
20247async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20248 let (buffer_id, mut cx) = setup_indent_guides_editor(
20249 &"
20250 block1
20251 block2
20252 block3
20253 block4
20254 block2
20255 block1
20256 block1"
20257 .unindent(),
20258 cx,
20259 )
20260 .await;
20261
20262 assert_indent_guides(
20263 1..10,
20264 vec![
20265 indent_guide(buffer_id, 1, 4, 0),
20266 indent_guide(buffer_id, 2, 3, 1),
20267 indent_guide(buffer_id, 3, 3, 2),
20268 ],
20269 None,
20270 &mut cx,
20271 );
20272}
20273
20274#[gpui::test]
20275async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20276 let (buffer_id, mut cx) = setup_indent_guides_editor(
20277 &"
20278 block1
20279 block2
20280 block3
20281
20282 block1
20283 block1"
20284 .unindent(),
20285 cx,
20286 )
20287 .await;
20288
20289 assert_indent_guides(
20290 0..6,
20291 vec![
20292 indent_guide(buffer_id, 1, 2, 0),
20293 indent_guide(buffer_id, 2, 2, 1),
20294 ],
20295 None,
20296 &mut cx,
20297 );
20298}
20299
20300#[gpui::test]
20301async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20302 let (buffer_id, mut cx) = setup_indent_guides_editor(
20303 &"
20304 function component() {
20305 \treturn (
20306 \t\t\t
20307 \t\t<div>
20308 \t\t\t<abc></abc>
20309 \t\t</div>
20310 \t)
20311 }"
20312 .unindent(),
20313 cx,
20314 )
20315 .await;
20316
20317 assert_indent_guides(
20318 0..8,
20319 vec![
20320 indent_guide(buffer_id, 1, 6, 0),
20321 indent_guide(buffer_id, 2, 5, 1),
20322 indent_guide(buffer_id, 4, 4, 2),
20323 ],
20324 None,
20325 &mut cx,
20326 );
20327}
20328
20329#[gpui::test]
20330async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20331 let (buffer_id, mut cx) = setup_indent_guides_editor(
20332 &"
20333 function component() {
20334 \treturn (
20335 \t
20336 \t\t<div>
20337 \t\t\t<abc></abc>
20338 \t\t</div>
20339 \t)
20340 }"
20341 .unindent(),
20342 cx,
20343 )
20344 .await;
20345
20346 assert_indent_guides(
20347 0..8,
20348 vec![
20349 indent_guide(buffer_id, 1, 6, 0),
20350 indent_guide(buffer_id, 2, 5, 1),
20351 indent_guide(buffer_id, 4, 4, 2),
20352 ],
20353 None,
20354 &mut cx,
20355 );
20356}
20357
20358#[gpui::test]
20359async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20360 let (buffer_id, mut cx) = setup_indent_guides_editor(
20361 &"
20362 block1
20363
20364
20365
20366 block2
20367 "
20368 .unindent(),
20369 cx,
20370 )
20371 .await;
20372
20373 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20374}
20375
20376#[gpui::test]
20377async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20378 let (buffer_id, mut cx) = setup_indent_guides_editor(
20379 &"
20380 def a:
20381 \tb = 3
20382 \tif True:
20383 \t\tc = 4
20384 \t\td = 5
20385 \tprint(b)
20386 "
20387 .unindent(),
20388 cx,
20389 )
20390 .await;
20391
20392 assert_indent_guides(
20393 0..6,
20394 vec![
20395 indent_guide(buffer_id, 1, 5, 0),
20396 indent_guide(buffer_id, 3, 4, 1),
20397 ],
20398 None,
20399 &mut cx,
20400 );
20401}
20402
20403#[gpui::test]
20404async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20405 let (buffer_id, mut cx) = setup_indent_guides_editor(
20406 &"
20407 fn main() {
20408 let a = 1;
20409 }"
20410 .unindent(),
20411 cx,
20412 )
20413 .await;
20414
20415 cx.update_editor(|editor, window, cx| {
20416 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20417 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20418 });
20419 });
20420
20421 assert_indent_guides(
20422 0..3,
20423 vec![indent_guide(buffer_id, 1, 1, 0)],
20424 Some(vec![0]),
20425 &mut cx,
20426 );
20427}
20428
20429#[gpui::test]
20430async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20431 let (buffer_id, mut cx) = setup_indent_guides_editor(
20432 &"
20433 fn main() {
20434 if 1 == 2 {
20435 let a = 1;
20436 }
20437 }"
20438 .unindent(),
20439 cx,
20440 )
20441 .await;
20442
20443 cx.update_editor(|editor, window, cx| {
20444 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20445 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20446 });
20447 });
20448
20449 assert_indent_guides(
20450 0..4,
20451 vec![
20452 indent_guide(buffer_id, 1, 3, 0),
20453 indent_guide(buffer_id, 2, 2, 1),
20454 ],
20455 Some(vec![1]),
20456 &mut cx,
20457 );
20458
20459 cx.update_editor(|editor, window, cx| {
20460 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20461 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20462 });
20463 });
20464
20465 assert_indent_guides(
20466 0..4,
20467 vec![
20468 indent_guide(buffer_id, 1, 3, 0),
20469 indent_guide(buffer_id, 2, 2, 1),
20470 ],
20471 Some(vec![1]),
20472 &mut cx,
20473 );
20474
20475 cx.update_editor(|editor, window, cx| {
20476 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20477 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20478 });
20479 });
20480
20481 assert_indent_guides(
20482 0..4,
20483 vec![
20484 indent_guide(buffer_id, 1, 3, 0),
20485 indent_guide(buffer_id, 2, 2, 1),
20486 ],
20487 Some(vec![0]),
20488 &mut cx,
20489 );
20490}
20491
20492#[gpui::test]
20493async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20494 let (buffer_id, mut cx) = setup_indent_guides_editor(
20495 &"
20496 fn main() {
20497 let a = 1;
20498
20499 let b = 2;
20500 }"
20501 .unindent(),
20502 cx,
20503 )
20504 .await;
20505
20506 cx.update_editor(|editor, window, cx| {
20507 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20508 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20509 });
20510 });
20511
20512 assert_indent_guides(
20513 0..5,
20514 vec![indent_guide(buffer_id, 1, 3, 0)],
20515 Some(vec![0]),
20516 &mut cx,
20517 );
20518}
20519
20520#[gpui::test]
20521async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20522 let (buffer_id, mut cx) = setup_indent_guides_editor(
20523 &"
20524 def m:
20525 a = 1
20526 pass"
20527 .unindent(),
20528 cx,
20529 )
20530 .await;
20531
20532 cx.update_editor(|editor, window, cx| {
20533 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20534 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20535 });
20536 });
20537
20538 assert_indent_guides(
20539 0..3,
20540 vec![indent_guide(buffer_id, 1, 2, 0)],
20541 Some(vec![0]),
20542 &mut cx,
20543 );
20544}
20545
20546#[gpui::test]
20547async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20548 init_test(cx, |_| {});
20549 let mut cx = EditorTestContext::new(cx).await;
20550 let text = indoc! {
20551 "
20552 impl A {
20553 fn b() {
20554 0;
20555 3;
20556 5;
20557 6;
20558 7;
20559 }
20560 }
20561 "
20562 };
20563 let base_text = indoc! {
20564 "
20565 impl A {
20566 fn b() {
20567 0;
20568 1;
20569 2;
20570 3;
20571 4;
20572 }
20573 fn c() {
20574 5;
20575 6;
20576 7;
20577 }
20578 }
20579 "
20580 };
20581
20582 cx.update_editor(|editor, window, cx| {
20583 editor.set_text(text, window, cx);
20584
20585 editor.buffer().update(cx, |multibuffer, cx| {
20586 let buffer = multibuffer.as_singleton().unwrap();
20587 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20588
20589 multibuffer.set_all_diff_hunks_expanded(cx);
20590 multibuffer.add_diff(diff, cx);
20591
20592 buffer.read(cx).remote_id()
20593 })
20594 });
20595 cx.run_until_parked();
20596
20597 cx.assert_state_with_diff(
20598 indoc! { "
20599 impl A {
20600 fn b() {
20601 0;
20602 - 1;
20603 - 2;
20604 3;
20605 - 4;
20606 - }
20607 - fn c() {
20608 5;
20609 6;
20610 7;
20611 }
20612 }
20613 ˇ"
20614 }
20615 .to_string(),
20616 );
20617
20618 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20619 editor
20620 .snapshot(window, cx)
20621 .buffer_snapshot
20622 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20623 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20624 .collect::<Vec<_>>()
20625 });
20626 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20627 assert_eq!(
20628 actual_guides,
20629 vec![
20630 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20631 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20632 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20633 ]
20634 );
20635}
20636
20637#[gpui::test]
20638async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20639 init_test(cx, |_| {});
20640 let mut cx = EditorTestContext::new(cx).await;
20641
20642 let diff_base = r#"
20643 a
20644 b
20645 c
20646 "#
20647 .unindent();
20648
20649 cx.set_state(
20650 &r#"
20651 ˇA
20652 b
20653 C
20654 "#
20655 .unindent(),
20656 );
20657 cx.set_head_text(&diff_base);
20658 cx.update_editor(|editor, window, cx| {
20659 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20660 });
20661 executor.run_until_parked();
20662
20663 let both_hunks_expanded = r#"
20664 - a
20665 + ˇA
20666 b
20667 - c
20668 + C
20669 "#
20670 .unindent();
20671
20672 cx.assert_state_with_diff(both_hunks_expanded.clone());
20673
20674 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20675 let snapshot = editor.snapshot(window, cx);
20676 let hunks = editor
20677 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20678 .collect::<Vec<_>>();
20679 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20680 let buffer_id = hunks[0].buffer_id;
20681 hunks
20682 .into_iter()
20683 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20684 .collect::<Vec<_>>()
20685 });
20686 assert_eq!(hunk_ranges.len(), 2);
20687
20688 cx.update_editor(|editor, _, cx| {
20689 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20690 });
20691 executor.run_until_parked();
20692
20693 let second_hunk_expanded = r#"
20694 ˇA
20695 b
20696 - c
20697 + C
20698 "#
20699 .unindent();
20700
20701 cx.assert_state_with_diff(second_hunk_expanded);
20702
20703 cx.update_editor(|editor, _, cx| {
20704 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20705 });
20706 executor.run_until_parked();
20707
20708 cx.assert_state_with_diff(both_hunks_expanded.clone());
20709
20710 cx.update_editor(|editor, _, cx| {
20711 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20712 });
20713 executor.run_until_parked();
20714
20715 let first_hunk_expanded = r#"
20716 - a
20717 + ˇA
20718 b
20719 C
20720 "#
20721 .unindent();
20722
20723 cx.assert_state_with_diff(first_hunk_expanded);
20724
20725 cx.update_editor(|editor, _, cx| {
20726 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20727 });
20728 executor.run_until_parked();
20729
20730 cx.assert_state_with_diff(both_hunks_expanded);
20731
20732 cx.set_state(
20733 &r#"
20734 ˇA
20735 b
20736 "#
20737 .unindent(),
20738 );
20739 cx.run_until_parked();
20740
20741 // TODO this cursor position seems bad
20742 cx.assert_state_with_diff(
20743 r#"
20744 - ˇa
20745 + A
20746 b
20747 "#
20748 .unindent(),
20749 );
20750
20751 cx.update_editor(|editor, window, cx| {
20752 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20753 });
20754
20755 cx.assert_state_with_diff(
20756 r#"
20757 - ˇa
20758 + A
20759 b
20760 - c
20761 "#
20762 .unindent(),
20763 );
20764
20765 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20766 let snapshot = editor.snapshot(window, cx);
20767 let hunks = editor
20768 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20769 .collect::<Vec<_>>();
20770 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20771 let buffer_id = hunks[0].buffer_id;
20772 hunks
20773 .into_iter()
20774 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20775 .collect::<Vec<_>>()
20776 });
20777 assert_eq!(hunk_ranges.len(), 2);
20778
20779 cx.update_editor(|editor, _, cx| {
20780 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20781 });
20782 executor.run_until_parked();
20783
20784 cx.assert_state_with_diff(
20785 r#"
20786 - ˇa
20787 + A
20788 b
20789 "#
20790 .unindent(),
20791 );
20792}
20793
20794#[gpui::test]
20795async fn test_toggle_deletion_hunk_at_start_of_file(
20796 executor: BackgroundExecutor,
20797 cx: &mut TestAppContext,
20798) {
20799 init_test(cx, |_| {});
20800 let mut cx = EditorTestContext::new(cx).await;
20801
20802 let diff_base = r#"
20803 a
20804 b
20805 c
20806 "#
20807 .unindent();
20808
20809 cx.set_state(
20810 &r#"
20811 ˇb
20812 c
20813 "#
20814 .unindent(),
20815 );
20816 cx.set_head_text(&diff_base);
20817 cx.update_editor(|editor, window, cx| {
20818 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20819 });
20820 executor.run_until_parked();
20821
20822 let hunk_expanded = r#"
20823 - a
20824 ˇb
20825 c
20826 "#
20827 .unindent();
20828
20829 cx.assert_state_with_diff(hunk_expanded.clone());
20830
20831 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20832 let snapshot = editor.snapshot(window, cx);
20833 let hunks = editor
20834 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20835 .collect::<Vec<_>>();
20836 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20837 let buffer_id = hunks[0].buffer_id;
20838 hunks
20839 .into_iter()
20840 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20841 .collect::<Vec<_>>()
20842 });
20843 assert_eq!(hunk_ranges.len(), 1);
20844
20845 cx.update_editor(|editor, _, cx| {
20846 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20847 });
20848 executor.run_until_parked();
20849
20850 let hunk_collapsed = r#"
20851 ˇb
20852 c
20853 "#
20854 .unindent();
20855
20856 cx.assert_state_with_diff(hunk_collapsed);
20857
20858 cx.update_editor(|editor, _, cx| {
20859 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20860 });
20861 executor.run_until_parked();
20862
20863 cx.assert_state_with_diff(hunk_expanded);
20864}
20865
20866#[gpui::test]
20867async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20868 init_test(cx, |_| {});
20869
20870 let fs = FakeFs::new(cx.executor());
20871 fs.insert_tree(
20872 path!("/test"),
20873 json!({
20874 ".git": {},
20875 "file-1": "ONE\n",
20876 "file-2": "TWO\n",
20877 "file-3": "THREE\n",
20878 }),
20879 )
20880 .await;
20881
20882 fs.set_head_for_repo(
20883 path!("/test/.git").as_ref(),
20884 &[
20885 ("file-1".into(), "one\n".into()),
20886 ("file-2".into(), "two\n".into()),
20887 ("file-3".into(), "three\n".into()),
20888 ],
20889 "deadbeef",
20890 );
20891
20892 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20893 let mut buffers = vec![];
20894 for i in 1..=3 {
20895 let buffer = project
20896 .update(cx, |project, cx| {
20897 let path = format!(path!("/test/file-{}"), i);
20898 project.open_local_buffer(path, cx)
20899 })
20900 .await
20901 .unwrap();
20902 buffers.push(buffer);
20903 }
20904
20905 let multibuffer = cx.new(|cx| {
20906 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20907 multibuffer.set_all_diff_hunks_expanded(cx);
20908 for buffer in &buffers {
20909 let snapshot = buffer.read(cx).snapshot();
20910 multibuffer.set_excerpts_for_path(
20911 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20912 buffer.clone(),
20913 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20914 2,
20915 cx,
20916 );
20917 }
20918 multibuffer
20919 });
20920
20921 let editor = cx.add_window(|window, cx| {
20922 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20923 });
20924 cx.run_until_parked();
20925
20926 let snapshot = editor
20927 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20928 .unwrap();
20929 let hunks = snapshot
20930 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20931 .map(|hunk| match hunk {
20932 DisplayDiffHunk::Unfolded {
20933 display_row_range, ..
20934 } => display_row_range,
20935 DisplayDiffHunk::Folded { .. } => unreachable!(),
20936 })
20937 .collect::<Vec<_>>();
20938 assert_eq!(
20939 hunks,
20940 [
20941 DisplayRow(2)..DisplayRow(4),
20942 DisplayRow(7)..DisplayRow(9),
20943 DisplayRow(12)..DisplayRow(14),
20944 ]
20945 );
20946}
20947
20948#[gpui::test]
20949async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
20950 init_test(cx, |_| {});
20951
20952 let mut cx = EditorTestContext::new(cx).await;
20953 cx.set_head_text(indoc! { "
20954 one
20955 two
20956 three
20957 four
20958 five
20959 "
20960 });
20961 cx.set_index_text(indoc! { "
20962 one
20963 two
20964 three
20965 four
20966 five
20967 "
20968 });
20969 cx.set_state(indoc! {"
20970 one
20971 TWO
20972 ˇTHREE
20973 FOUR
20974 five
20975 "});
20976 cx.run_until_parked();
20977 cx.update_editor(|editor, window, cx| {
20978 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20979 });
20980 cx.run_until_parked();
20981 cx.assert_index_text(Some(indoc! {"
20982 one
20983 TWO
20984 THREE
20985 FOUR
20986 five
20987 "}));
20988 cx.set_state(indoc! { "
20989 one
20990 TWO
20991 ˇTHREE-HUNDRED
20992 FOUR
20993 five
20994 "});
20995 cx.run_until_parked();
20996 cx.update_editor(|editor, window, cx| {
20997 let snapshot = editor.snapshot(window, cx);
20998 let hunks = editor
20999 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
21000 .collect::<Vec<_>>();
21001 assert_eq!(hunks.len(), 1);
21002 assert_eq!(
21003 hunks[0].status(),
21004 DiffHunkStatus {
21005 kind: DiffHunkStatusKind::Modified,
21006 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21007 }
21008 );
21009
21010 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21011 });
21012 cx.run_until_parked();
21013 cx.assert_index_text(Some(indoc! {"
21014 one
21015 TWO
21016 THREE-HUNDRED
21017 FOUR
21018 five
21019 "}));
21020}
21021
21022#[gpui::test]
21023fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21024 init_test(cx, |_| {});
21025
21026 let editor = cx.add_window(|window, cx| {
21027 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21028 build_editor(buffer, window, cx)
21029 });
21030
21031 let render_args = Arc::new(Mutex::new(None));
21032 let snapshot = editor
21033 .update(cx, |editor, window, cx| {
21034 let snapshot = editor.buffer().read(cx).snapshot(cx);
21035 let range =
21036 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21037
21038 struct RenderArgs {
21039 row: MultiBufferRow,
21040 folded: bool,
21041 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21042 }
21043
21044 let crease = Crease::inline(
21045 range,
21046 FoldPlaceholder::test(),
21047 {
21048 let toggle_callback = render_args.clone();
21049 move |row, folded, callback, _window, _cx| {
21050 *toggle_callback.lock() = Some(RenderArgs {
21051 row,
21052 folded,
21053 callback,
21054 });
21055 div()
21056 }
21057 },
21058 |_row, _folded, _window, _cx| div(),
21059 );
21060
21061 editor.insert_creases(Some(crease), cx);
21062 let snapshot = editor.snapshot(window, cx);
21063 let _div =
21064 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21065 snapshot
21066 })
21067 .unwrap();
21068
21069 let render_args = render_args.lock().take().unwrap();
21070 assert_eq!(render_args.row, MultiBufferRow(1));
21071 assert!(!render_args.folded);
21072 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21073
21074 cx.update_window(*editor, |_, window, cx| {
21075 (render_args.callback)(true, 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 cx.update_window(*editor, |_, window, cx| {
21084 (render_args.callback)(false, window, cx)
21085 })
21086 .unwrap();
21087 let snapshot = editor
21088 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21089 .unwrap();
21090 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21091}
21092
21093#[gpui::test]
21094async fn test_input_text(cx: &mut TestAppContext) {
21095 init_test(cx, |_| {});
21096 let mut cx = EditorTestContext::new(cx).await;
21097
21098 cx.set_state(
21099 &r#"ˇone
21100 two
21101
21102 three
21103 fourˇ
21104 five
21105
21106 siˇx"#
21107 .unindent(),
21108 );
21109
21110 cx.dispatch_action(HandleInput(String::new()));
21111 cx.assert_editor_state(
21112 &r#"ˇone
21113 two
21114
21115 three
21116 fourˇ
21117 five
21118
21119 siˇx"#
21120 .unindent(),
21121 );
21122
21123 cx.dispatch_action(HandleInput("AAAA".to_string()));
21124 cx.assert_editor_state(
21125 &r#"AAAAˇone
21126 two
21127
21128 three
21129 fourAAAAˇ
21130 five
21131
21132 siAAAAˇx"#
21133 .unindent(),
21134 );
21135}
21136
21137#[gpui::test]
21138async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21139 init_test(cx, |_| {});
21140
21141 let mut cx = EditorTestContext::new(cx).await;
21142 cx.set_state(
21143 r#"let foo = 1;
21144let foo = 2;
21145let foo = 3;
21146let fooˇ = 4;
21147let foo = 5;
21148let foo = 6;
21149let foo = 7;
21150let foo = 8;
21151let foo = 9;
21152let foo = 10;
21153let foo = 11;
21154let foo = 12;
21155let foo = 13;
21156let foo = 14;
21157let foo = 15;"#,
21158 );
21159
21160 cx.update_editor(|e, window, cx| {
21161 assert_eq!(
21162 e.next_scroll_position,
21163 NextScrollCursorCenterTopBottom::Center,
21164 "Default next scroll direction is center",
21165 );
21166
21167 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21168 assert_eq!(
21169 e.next_scroll_position,
21170 NextScrollCursorCenterTopBottom::Top,
21171 "After center, next scroll direction should be top",
21172 );
21173
21174 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21175 assert_eq!(
21176 e.next_scroll_position,
21177 NextScrollCursorCenterTopBottom::Bottom,
21178 "After top, next scroll direction should be bottom",
21179 );
21180
21181 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21182 assert_eq!(
21183 e.next_scroll_position,
21184 NextScrollCursorCenterTopBottom::Center,
21185 "After bottom, scrolling should start over",
21186 );
21187
21188 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21189 assert_eq!(
21190 e.next_scroll_position,
21191 NextScrollCursorCenterTopBottom::Top,
21192 "Scrolling continues if retriggered fast enough"
21193 );
21194 });
21195
21196 cx.executor()
21197 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21198 cx.executor().run_until_parked();
21199 cx.update_editor(|e, _, _| {
21200 assert_eq!(
21201 e.next_scroll_position,
21202 NextScrollCursorCenterTopBottom::Center,
21203 "If scrolling is not triggered fast enough, it should reset"
21204 );
21205 });
21206}
21207
21208#[gpui::test]
21209async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21210 init_test(cx, |_| {});
21211 let mut cx = EditorLspTestContext::new_rust(
21212 lsp::ServerCapabilities {
21213 definition_provider: Some(lsp::OneOf::Left(true)),
21214 references_provider: Some(lsp::OneOf::Left(true)),
21215 ..lsp::ServerCapabilities::default()
21216 },
21217 cx,
21218 )
21219 .await;
21220
21221 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21222 let go_to_definition = cx
21223 .lsp
21224 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21225 move |params, _| async move {
21226 if empty_go_to_definition {
21227 Ok(None)
21228 } else {
21229 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21230 uri: params.text_document_position_params.text_document.uri,
21231 range: lsp::Range::new(
21232 lsp::Position::new(4, 3),
21233 lsp::Position::new(4, 6),
21234 ),
21235 })))
21236 }
21237 },
21238 );
21239 let references = cx
21240 .lsp
21241 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21242 Ok(Some(vec![lsp::Location {
21243 uri: params.text_document_position.text_document.uri,
21244 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21245 }]))
21246 });
21247 (go_to_definition, references)
21248 };
21249
21250 cx.set_state(
21251 &r#"fn one() {
21252 let mut a = ˇtwo();
21253 }
21254
21255 fn two() {}"#
21256 .unindent(),
21257 );
21258 set_up_lsp_handlers(false, &mut cx);
21259 let navigated = cx
21260 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21261 .await
21262 .expect("Failed to navigate to definition");
21263 assert_eq!(
21264 navigated,
21265 Navigated::Yes,
21266 "Should have navigated to definition from the GetDefinition response"
21267 );
21268 cx.assert_editor_state(
21269 &r#"fn one() {
21270 let mut a = two();
21271 }
21272
21273 fn «twoˇ»() {}"#
21274 .unindent(),
21275 );
21276
21277 let editors = cx.update_workspace(|workspace, _, cx| {
21278 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21279 });
21280 cx.update_editor(|_, _, test_editor_cx| {
21281 assert_eq!(
21282 editors.len(),
21283 1,
21284 "Initially, only one, test, editor should be open in the workspace"
21285 );
21286 assert_eq!(
21287 test_editor_cx.entity(),
21288 editors.last().expect("Asserted len is 1").clone()
21289 );
21290 });
21291
21292 set_up_lsp_handlers(true, &mut cx);
21293 let navigated = cx
21294 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21295 .await
21296 .expect("Failed to navigate to lookup references");
21297 assert_eq!(
21298 navigated,
21299 Navigated::Yes,
21300 "Should have navigated to references as a fallback after empty GoToDefinition response"
21301 );
21302 // We should not change the selections in the existing file,
21303 // if opening another milti buffer with the references
21304 cx.assert_editor_state(
21305 &r#"fn one() {
21306 let mut a = two();
21307 }
21308
21309 fn «twoˇ»() {}"#
21310 .unindent(),
21311 );
21312 let editors = cx.update_workspace(|workspace, _, cx| {
21313 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21314 });
21315 cx.update_editor(|_, _, test_editor_cx| {
21316 assert_eq!(
21317 editors.len(),
21318 2,
21319 "After falling back to references search, we open a new editor with the results"
21320 );
21321 let references_fallback_text = editors
21322 .into_iter()
21323 .find(|new_editor| *new_editor != test_editor_cx.entity())
21324 .expect("Should have one non-test editor now")
21325 .read(test_editor_cx)
21326 .text(test_editor_cx);
21327 assert_eq!(
21328 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21329 "Should use the range from the references response and not the GoToDefinition one"
21330 );
21331 });
21332}
21333
21334#[gpui::test]
21335async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21336 init_test(cx, |_| {});
21337 cx.update(|cx| {
21338 let mut editor_settings = EditorSettings::get_global(cx).clone();
21339 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21340 EditorSettings::override_global(editor_settings, cx);
21341 });
21342 let mut cx = EditorLspTestContext::new_rust(
21343 lsp::ServerCapabilities {
21344 definition_provider: Some(lsp::OneOf::Left(true)),
21345 references_provider: Some(lsp::OneOf::Left(true)),
21346 ..lsp::ServerCapabilities::default()
21347 },
21348 cx,
21349 )
21350 .await;
21351 let original_state = r#"fn one() {
21352 let mut a = ˇtwo();
21353 }
21354
21355 fn two() {}"#
21356 .unindent();
21357 cx.set_state(&original_state);
21358
21359 let mut go_to_definition = cx
21360 .lsp
21361 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21362 move |_, _| async move { Ok(None) },
21363 );
21364 let _references = cx
21365 .lsp
21366 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21367 panic!("Should not call for references with no go to definition fallback")
21368 });
21369
21370 let navigated = cx
21371 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21372 .await
21373 .expect("Failed to navigate to lookup references");
21374 go_to_definition
21375 .next()
21376 .await
21377 .expect("Should have called the go_to_definition handler");
21378
21379 assert_eq!(
21380 navigated,
21381 Navigated::No,
21382 "Should have navigated to references as a fallback after empty GoToDefinition response"
21383 );
21384 cx.assert_editor_state(&original_state);
21385 let editors = cx.update_workspace(|workspace, _, cx| {
21386 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21387 });
21388 cx.update_editor(|_, _, _| {
21389 assert_eq!(
21390 editors.len(),
21391 1,
21392 "After unsuccessful fallback, no other editor should have been opened"
21393 );
21394 });
21395}
21396
21397#[gpui::test]
21398async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21399 init_test(cx, |_| {});
21400 let mut cx = EditorLspTestContext::new_rust(
21401 lsp::ServerCapabilities {
21402 references_provider: Some(lsp::OneOf::Left(true)),
21403 ..lsp::ServerCapabilities::default()
21404 },
21405 cx,
21406 )
21407 .await;
21408
21409 cx.set_state(
21410 &r#"
21411 fn one() {
21412 let mut a = two();
21413 }
21414
21415 fn ˇtwo() {}"#
21416 .unindent(),
21417 );
21418 cx.lsp
21419 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21420 Ok(Some(vec![
21421 lsp::Location {
21422 uri: params.text_document_position.text_document.uri.clone(),
21423 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21424 },
21425 lsp::Location {
21426 uri: params.text_document_position.text_document.uri,
21427 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21428 },
21429 ]))
21430 });
21431 let navigated = cx
21432 .update_editor(|editor, window, cx| {
21433 editor.find_all_references(&FindAllReferences, window, cx)
21434 })
21435 .unwrap()
21436 .await
21437 .expect("Failed to navigate to references");
21438 assert_eq!(
21439 navigated,
21440 Navigated::Yes,
21441 "Should have navigated to references from the FindAllReferences response"
21442 );
21443 cx.assert_editor_state(
21444 &r#"fn one() {
21445 let mut a = two();
21446 }
21447
21448 fn ˇtwo() {}"#
21449 .unindent(),
21450 );
21451
21452 let editors = cx.update_workspace(|workspace, _, cx| {
21453 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21454 });
21455 cx.update_editor(|_, _, _| {
21456 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21457 });
21458
21459 cx.set_state(
21460 &r#"fn one() {
21461 let mut a = ˇtwo();
21462 }
21463
21464 fn two() {}"#
21465 .unindent(),
21466 );
21467 let navigated = cx
21468 .update_editor(|editor, window, cx| {
21469 editor.find_all_references(&FindAllReferences, window, cx)
21470 })
21471 .unwrap()
21472 .await
21473 .expect("Failed to navigate to references");
21474 assert_eq!(
21475 navigated,
21476 Navigated::Yes,
21477 "Should have navigated to references from the FindAllReferences response"
21478 );
21479 cx.assert_editor_state(
21480 &r#"fn one() {
21481 let mut a = ˇtwo();
21482 }
21483
21484 fn two() {}"#
21485 .unindent(),
21486 );
21487 let editors = cx.update_workspace(|workspace, _, cx| {
21488 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21489 });
21490 cx.update_editor(|_, _, _| {
21491 assert_eq!(
21492 editors.len(),
21493 2,
21494 "should have re-used the previous multibuffer"
21495 );
21496 });
21497
21498 cx.set_state(
21499 &r#"fn one() {
21500 let mut a = ˇtwo();
21501 }
21502 fn three() {}
21503 fn two() {}"#
21504 .unindent(),
21505 );
21506 cx.lsp
21507 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21508 Ok(Some(vec![
21509 lsp::Location {
21510 uri: params.text_document_position.text_document.uri.clone(),
21511 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21512 },
21513 lsp::Location {
21514 uri: params.text_document_position.text_document.uri,
21515 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21516 },
21517 ]))
21518 });
21519 let navigated = cx
21520 .update_editor(|editor, window, cx| {
21521 editor.find_all_references(&FindAllReferences, window, cx)
21522 })
21523 .unwrap()
21524 .await
21525 .expect("Failed to navigate to references");
21526 assert_eq!(
21527 navigated,
21528 Navigated::Yes,
21529 "Should have navigated to references from the FindAllReferences response"
21530 );
21531 cx.assert_editor_state(
21532 &r#"fn one() {
21533 let mut a = ˇtwo();
21534 }
21535 fn three() {}
21536 fn two() {}"#
21537 .unindent(),
21538 );
21539 let editors = cx.update_workspace(|workspace, _, cx| {
21540 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21541 });
21542 cx.update_editor(|_, _, _| {
21543 assert_eq!(
21544 editors.len(),
21545 3,
21546 "should have used a new multibuffer as offsets changed"
21547 );
21548 });
21549}
21550#[gpui::test]
21551async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21552 init_test(cx, |_| {});
21553
21554 let language = Arc::new(Language::new(
21555 LanguageConfig::default(),
21556 Some(tree_sitter_rust::LANGUAGE.into()),
21557 ));
21558
21559 let text = r#"
21560 #[cfg(test)]
21561 mod tests() {
21562 #[test]
21563 fn runnable_1() {
21564 let a = 1;
21565 }
21566
21567 #[test]
21568 fn runnable_2() {
21569 let a = 1;
21570 let b = 2;
21571 }
21572 }
21573 "#
21574 .unindent();
21575
21576 let fs = FakeFs::new(cx.executor());
21577 fs.insert_file("/file.rs", Default::default()).await;
21578
21579 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21580 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21581 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21582 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21583 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21584
21585 let editor = cx.new_window_entity(|window, cx| {
21586 Editor::new(
21587 EditorMode::full(),
21588 multi_buffer,
21589 Some(project.clone()),
21590 window,
21591 cx,
21592 )
21593 });
21594
21595 editor.update_in(cx, |editor, window, cx| {
21596 let snapshot = editor.buffer().read(cx).snapshot(cx);
21597 editor.tasks.insert(
21598 (buffer.read(cx).remote_id(), 3),
21599 RunnableTasks {
21600 templates: vec![],
21601 offset: snapshot.anchor_before(43),
21602 column: 0,
21603 extra_variables: HashMap::default(),
21604 context_range: BufferOffset(43)..BufferOffset(85),
21605 },
21606 );
21607 editor.tasks.insert(
21608 (buffer.read(cx).remote_id(), 8),
21609 RunnableTasks {
21610 templates: vec![],
21611 offset: snapshot.anchor_before(86),
21612 column: 0,
21613 extra_variables: HashMap::default(),
21614 context_range: BufferOffset(86)..BufferOffset(191),
21615 },
21616 );
21617
21618 // Test finding task when cursor is inside function body
21619 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21620 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21621 });
21622 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21623 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21624
21625 // Test finding task when cursor is on function name
21626 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21627 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21628 });
21629 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21630 assert_eq!(row, 8, "Should find task when cursor is on function name");
21631 });
21632}
21633
21634#[gpui::test]
21635async fn test_folding_buffers(cx: &mut TestAppContext) {
21636 init_test(cx, |_| {});
21637
21638 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21639 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21640 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21641
21642 let fs = FakeFs::new(cx.executor());
21643 fs.insert_tree(
21644 path!("/a"),
21645 json!({
21646 "first.rs": sample_text_1,
21647 "second.rs": sample_text_2,
21648 "third.rs": sample_text_3,
21649 }),
21650 )
21651 .await;
21652 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21653 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21654 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21655 let worktree = project.update(cx, |project, cx| {
21656 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21657 assert_eq!(worktrees.len(), 1);
21658 worktrees.pop().unwrap()
21659 });
21660 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21661
21662 let buffer_1 = project
21663 .update(cx, |project, cx| {
21664 project.open_buffer((worktree_id, "first.rs"), cx)
21665 })
21666 .await
21667 .unwrap();
21668 let buffer_2 = project
21669 .update(cx, |project, cx| {
21670 project.open_buffer((worktree_id, "second.rs"), cx)
21671 })
21672 .await
21673 .unwrap();
21674 let buffer_3 = project
21675 .update(cx, |project, cx| {
21676 project.open_buffer((worktree_id, "third.rs"), cx)
21677 })
21678 .await
21679 .unwrap();
21680
21681 let multi_buffer = cx.new(|cx| {
21682 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21683 multi_buffer.push_excerpts(
21684 buffer_1.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_2.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.push_excerpts(
21702 buffer_3.clone(),
21703 [
21704 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21705 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21706 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21707 ],
21708 cx,
21709 );
21710 multi_buffer
21711 });
21712 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21713 Editor::new(
21714 EditorMode::full(),
21715 multi_buffer.clone(),
21716 Some(project.clone()),
21717 window,
21718 cx,
21719 )
21720 });
21721
21722 assert_eq!(
21723 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21724 "\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",
21725 );
21726
21727 multi_buffer_editor.update(cx, |editor, cx| {
21728 editor.fold_buffer(buffer_1.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\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21733 "After folding the first buffer, its text should not be displayed"
21734 );
21735
21736 multi_buffer_editor.update(cx, |editor, cx| {
21737 editor.fold_buffer(buffer_2.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\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21742 "After folding the second buffer, its text should not be displayed"
21743 );
21744
21745 multi_buffer_editor.update(cx, |editor, cx| {
21746 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21747 });
21748 assert_eq!(
21749 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21750 "\n\n\n\n\n",
21751 "After folding the third buffer, its text should not be displayed"
21752 );
21753
21754 // Emulate selection inside the fold logic, that should work
21755 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21756 editor
21757 .snapshot(window, cx)
21758 .next_line_boundary(Point::new(0, 4));
21759 });
21760
21761 multi_buffer_editor.update(cx, |editor, cx| {
21762 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21763 });
21764 assert_eq!(
21765 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21766 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21767 "After unfolding the second buffer, its text should be displayed"
21768 );
21769
21770 // Typing inside of buffer 1 causes that buffer to be unfolded.
21771 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21772 assert_eq!(
21773 multi_buffer
21774 .read(cx)
21775 .snapshot(cx)
21776 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21777 .collect::<String>(),
21778 "bbbb"
21779 );
21780 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21781 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21782 });
21783 editor.handle_input("B", window, cx);
21784 });
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",
21789 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21790 );
21791
21792 multi_buffer_editor.update(cx, |editor, cx| {
21793 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21794 });
21795 assert_eq!(
21796 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21797 "\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",
21798 "After unfolding the all buffers, all original text should be displayed"
21799 );
21800}
21801
21802#[gpui::test]
21803async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21804 init_test(cx, |_| {});
21805
21806 let sample_text_1 = "1111\n2222\n3333".to_string();
21807 let sample_text_2 = "4444\n5555\n6666".to_string();
21808 let sample_text_3 = "7777\n8888\n9999".to_string();
21809
21810 let fs = FakeFs::new(cx.executor());
21811 fs.insert_tree(
21812 path!("/a"),
21813 json!({
21814 "first.rs": sample_text_1,
21815 "second.rs": sample_text_2,
21816 "third.rs": sample_text_3,
21817 }),
21818 )
21819 .await;
21820 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21821 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21822 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21823 let worktree = project.update(cx, |project, cx| {
21824 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21825 assert_eq!(worktrees.len(), 1);
21826 worktrees.pop().unwrap()
21827 });
21828 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21829
21830 let buffer_1 = project
21831 .update(cx, |project, cx| {
21832 project.open_buffer((worktree_id, "first.rs"), cx)
21833 })
21834 .await
21835 .unwrap();
21836 let buffer_2 = project
21837 .update(cx, |project, cx| {
21838 project.open_buffer((worktree_id, "second.rs"), cx)
21839 })
21840 .await
21841 .unwrap();
21842 let buffer_3 = project
21843 .update(cx, |project, cx| {
21844 project.open_buffer((worktree_id, "third.rs"), cx)
21845 })
21846 .await
21847 .unwrap();
21848
21849 let multi_buffer = cx.new(|cx| {
21850 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21851 multi_buffer.push_excerpts(
21852 buffer_1.clone(),
21853 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21854 cx,
21855 );
21856 multi_buffer.push_excerpts(
21857 buffer_2.clone(),
21858 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21859 cx,
21860 );
21861 multi_buffer.push_excerpts(
21862 buffer_3.clone(),
21863 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21864 cx,
21865 );
21866 multi_buffer
21867 });
21868
21869 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21870 Editor::new(
21871 EditorMode::full(),
21872 multi_buffer,
21873 Some(project.clone()),
21874 window,
21875 cx,
21876 )
21877 });
21878
21879 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21880 assert_eq!(
21881 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21882 full_text,
21883 );
21884
21885 multi_buffer_editor.update(cx, |editor, cx| {
21886 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21887 });
21888 assert_eq!(
21889 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21890 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21891 "After folding the first buffer, its text should not be displayed"
21892 );
21893
21894 multi_buffer_editor.update(cx, |editor, cx| {
21895 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21896 });
21897
21898 assert_eq!(
21899 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21900 "\n\n\n\n\n\n7777\n8888\n9999",
21901 "After folding the second buffer, its text should not be displayed"
21902 );
21903
21904 multi_buffer_editor.update(cx, |editor, cx| {
21905 editor.fold_buffer(buffer_3.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\n\n",
21910 "After folding the third buffer, its text should not be displayed"
21911 );
21912
21913 multi_buffer_editor.update(cx, |editor, cx| {
21914 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21915 });
21916 assert_eq!(
21917 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21918 "\n\n\n\n4444\n5555\n6666\n\n",
21919 "After unfolding the second buffer, its text should be displayed"
21920 );
21921
21922 multi_buffer_editor.update(cx, |editor, cx| {
21923 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21924 });
21925 assert_eq!(
21926 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21927 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21928 "After unfolding the first buffer, its text should be displayed"
21929 );
21930
21931 multi_buffer_editor.update(cx, |editor, cx| {
21932 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21933 });
21934 assert_eq!(
21935 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21936 full_text,
21937 "After unfolding all buffers, all original text should be displayed"
21938 );
21939}
21940
21941#[gpui::test]
21942async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21943 init_test(cx, |_| {});
21944
21945 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21946
21947 let fs = FakeFs::new(cx.executor());
21948 fs.insert_tree(
21949 path!("/a"),
21950 json!({
21951 "main.rs": sample_text,
21952 }),
21953 )
21954 .await;
21955 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21956 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21957 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21958 let worktree = project.update(cx, |project, cx| {
21959 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21960 assert_eq!(worktrees.len(), 1);
21961 worktrees.pop().unwrap()
21962 });
21963 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21964
21965 let buffer_1 = project
21966 .update(cx, |project, cx| {
21967 project.open_buffer((worktree_id, "main.rs"), cx)
21968 })
21969 .await
21970 .unwrap();
21971
21972 let multi_buffer = cx.new(|cx| {
21973 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21974 multi_buffer.push_excerpts(
21975 buffer_1.clone(),
21976 [ExcerptRange::new(
21977 Point::new(0, 0)
21978 ..Point::new(
21979 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
21980 0,
21981 ),
21982 )],
21983 cx,
21984 );
21985 multi_buffer
21986 });
21987 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21988 Editor::new(
21989 EditorMode::full(),
21990 multi_buffer,
21991 Some(project.clone()),
21992 window,
21993 cx,
21994 )
21995 });
21996
21997 let selection_range = Point::new(1, 0)..Point::new(2, 0);
21998 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21999 enum TestHighlight {}
22000 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22001 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22002 editor.highlight_text::<TestHighlight>(
22003 vec![highlight_range.clone()],
22004 HighlightStyle::color(Hsla::green()),
22005 cx,
22006 );
22007 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22008 s.select_ranges(Some(highlight_range))
22009 });
22010 });
22011
22012 let full_text = format!("\n\n{sample_text}");
22013 assert_eq!(
22014 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22015 full_text,
22016 );
22017}
22018
22019#[gpui::test]
22020async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22021 init_test(cx, |_| {});
22022 cx.update(|cx| {
22023 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22024 "keymaps/default-linux.json",
22025 cx,
22026 )
22027 .unwrap();
22028 cx.bind_keys(default_key_bindings);
22029 });
22030
22031 let (editor, cx) = cx.add_window_view(|window, cx| {
22032 let multi_buffer = MultiBuffer::build_multi(
22033 [
22034 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22035 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22036 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22037 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22038 ],
22039 cx,
22040 );
22041 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22042
22043 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22044 // fold all but the second buffer, so that we test navigating between two
22045 // adjacent folded buffers, as well as folded buffers at the start and
22046 // end the multibuffer
22047 editor.fold_buffer(buffer_ids[0], cx);
22048 editor.fold_buffer(buffer_ids[2], cx);
22049 editor.fold_buffer(buffer_ids[3], cx);
22050
22051 editor
22052 });
22053 cx.simulate_resize(size(px(1000.), px(1000.)));
22054
22055 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22056 cx.assert_excerpts_with_selections(indoc! {"
22057 [EXCERPT]
22058 ˇ[FOLDED]
22059 [EXCERPT]
22060 a1
22061 b1
22062 [EXCERPT]
22063 [FOLDED]
22064 [EXCERPT]
22065 [FOLDED]
22066 "
22067 });
22068 cx.simulate_keystroke("down");
22069 cx.assert_excerpts_with_selections(indoc! {"
22070 [EXCERPT]
22071 [FOLDED]
22072 [EXCERPT]
22073 ˇa1
22074 b1
22075 [EXCERPT]
22076 [FOLDED]
22077 [EXCERPT]
22078 [FOLDED]
22079 "
22080 });
22081 cx.simulate_keystroke("down");
22082 cx.assert_excerpts_with_selections(indoc! {"
22083 [EXCERPT]
22084 [FOLDED]
22085 [EXCERPT]
22086 a1
22087 ˇb1
22088 [EXCERPT]
22089 [FOLDED]
22090 [EXCERPT]
22091 [FOLDED]
22092 "
22093 });
22094 cx.simulate_keystroke("down");
22095 cx.assert_excerpts_with_selections(indoc! {"
22096 [EXCERPT]
22097 [FOLDED]
22098 [EXCERPT]
22099 a1
22100 b1
22101 ˇ[EXCERPT]
22102 [FOLDED]
22103 [EXCERPT]
22104 [FOLDED]
22105 "
22106 });
22107 cx.simulate_keystroke("down");
22108 cx.assert_excerpts_with_selections(indoc! {"
22109 [EXCERPT]
22110 [FOLDED]
22111 [EXCERPT]
22112 a1
22113 b1
22114 [EXCERPT]
22115 ˇ[FOLDED]
22116 [EXCERPT]
22117 [FOLDED]
22118 "
22119 });
22120 for _ in 0..5 {
22121 cx.simulate_keystroke("down");
22122 cx.assert_excerpts_with_selections(indoc! {"
22123 [EXCERPT]
22124 [FOLDED]
22125 [EXCERPT]
22126 a1
22127 b1
22128 [EXCERPT]
22129 [FOLDED]
22130 [EXCERPT]
22131 ˇ[FOLDED]
22132 "
22133 });
22134 }
22135
22136 cx.simulate_keystroke("up");
22137 cx.assert_excerpts_with_selections(indoc! {"
22138 [EXCERPT]
22139 [FOLDED]
22140 [EXCERPT]
22141 a1
22142 b1
22143 [EXCERPT]
22144 ˇ[FOLDED]
22145 [EXCERPT]
22146 [FOLDED]
22147 "
22148 });
22149 cx.simulate_keystroke("up");
22150 cx.assert_excerpts_with_selections(indoc! {"
22151 [EXCERPT]
22152 [FOLDED]
22153 [EXCERPT]
22154 a1
22155 b1
22156 ˇ[EXCERPT]
22157 [FOLDED]
22158 [EXCERPT]
22159 [FOLDED]
22160 "
22161 });
22162 cx.simulate_keystroke("up");
22163 cx.assert_excerpts_with_selections(indoc! {"
22164 [EXCERPT]
22165 [FOLDED]
22166 [EXCERPT]
22167 a1
22168 ˇb1
22169 [EXCERPT]
22170 [FOLDED]
22171 [EXCERPT]
22172 [FOLDED]
22173 "
22174 });
22175 cx.simulate_keystroke("up");
22176 cx.assert_excerpts_with_selections(indoc! {"
22177 [EXCERPT]
22178 [FOLDED]
22179 [EXCERPT]
22180 ˇa1
22181 b1
22182 [EXCERPT]
22183 [FOLDED]
22184 [EXCERPT]
22185 [FOLDED]
22186 "
22187 });
22188 for _ in 0..5 {
22189 cx.simulate_keystroke("up");
22190 cx.assert_excerpts_with_selections(indoc! {"
22191 [EXCERPT]
22192 ˇ[FOLDED]
22193 [EXCERPT]
22194 a1
22195 b1
22196 [EXCERPT]
22197 [FOLDED]
22198 [EXCERPT]
22199 [FOLDED]
22200 "
22201 });
22202 }
22203}
22204
22205#[gpui::test]
22206async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22207 init_test(cx, |_| {});
22208
22209 // Simple insertion
22210 assert_highlighted_edits(
22211 "Hello, world!",
22212 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22213 true,
22214 cx,
22215 |highlighted_edits, cx| {
22216 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22217 assert_eq!(highlighted_edits.highlights.len(), 1);
22218 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22219 assert_eq!(
22220 highlighted_edits.highlights[0].1.background_color,
22221 Some(cx.theme().status().created_background)
22222 );
22223 },
22224 )
22225 .await;
22226
22227 // Replacement
22228 assert_highlighted_edits(
22229 "This is a test.",
22230 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22231 false,
22232 cx,
22233 |highlighted_edits, cx| {
22234 assert_eq!(highlighted_edits.text, "That is a test.");
22235 assert_eq!(highlighted_edits.highlights.len(), 1);
22236 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22237 assert_eq!(
22238 highlighted_edits.highlights[0].1.background_color,
22239 Some(cx.theme().status().created_background)
22240 );
22241 },
22242 )
22243 .await;
22244
22245 // Multiple edits
22246 assert_highlighted_edits(
22247 "Hello, world!",
22248 vec![
22249 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22250 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22251 ],
22252 false,
22253 cx,
22254 |highlighted_edits, cx| {
22255 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22256 assert_eq!(highlighted_edits.highlights.len(), 2);
22257 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22258 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22259 assert_eq!(
22260 highlighted_edits.highlights[0].1.background_color,
22261 Some(cx.theme().status().created_background)
22262 );
22263 assert_eq!(
22264 highlighted_edits.highlights[1].1.background_color,
22265 Some(cx.theme().status().created_background)
22266 );
22267 },
22268 )
22269 .await;
22270
22271 // Multiple lines with edits
22272 assert_highlighted_edits(
22273 "First line\nSecond line\nThird line\nFourth line",
22274 vec![
22275 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22276 (
22277 Point::new(2, 0)..Point::new(2, 10),
22278 "New third line".to_string(),
22279 ),
22280 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22281 ],
22282 false,
22283 cx,
22284 |highlighted_edits, cx| {
22285 assert_eq!(
22286 highlighted_edits.text,
22287 "Second modified\nNew third line\nFourth updated line"
22288 );
22289 assert_eq!(highlighted_edits.highlights.len(), 3);
22290 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22291 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22292 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22293 for highlight in &highlighted_edits.highlights {
22294 assert_eq!(
22295 highlight.1.background_color,
22296 Some(cx.theme().status().created_background)
22297 );
22298 }
22299 },
22300 )
22301 .await;
22302}
22303
22304#[gpui::test]
22305async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22306 init_test(cx, |_| {});
22307
22308 // Deletion
22309 assert_highlighted_edits(
22310 "Hello, world!",
22311 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22312 true,
22313 cx,
22314 |highlighted_edits, cx| {
22315 assert_eq!(highlighted_edits.text, "Hello, world!");
22316 assert_eq!(highlighted_edits.highlights.len(), 1);
22317 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22318 assert_eq!(
22319 highlighted_edits.highlights[0].1.background_color,
22320 Some(cx.theme().status().deleted_background)
22321 );
22322 },
22323 )
22324 .await;
22325
22326 // Insertion
22327 assert_highlighted_edits(
22328 "Hello, world!",
22329 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22330 true,
22331 cx,
22332 |highlighted_edits, cx| {
22333 assert_eq!(highlighted_edits.highlights.len(), 1);
22334 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22335 assert_eq!(
22336 highlighted_edits.highlights[0].1.background_color,
22337 Some(cx.theme().status().created_background)
22338 );
22339 },
22340 )
22341 .await;
22342}
22343
22344async fn assert_highlighted_edits(
22345 text: &str,
22346 edits: Vec<(Range<Point>, String)>,
22347 include_deletions: bool,
22348 cx: &mut TestAppContext,
22349 assertion_fn: impl Fn(HighlightedText, &App),
22350) {
22351 let window = cx.add_window(|window, cx| {
22352 let buffer = MultiBuffer::build_simple(text, cx);
22353 Editor::new(EditorMode::full(), buffer, None, window, cx)
22354 });
22355 let cx = &mut VisualTestContext::from_window(*window, cx);
22356
22357 let (buffer, snapshot) = window
22358 .update(cx, |editor, _window, cx| {
22359 (
22360 editor.buffer().clone(),
22361 editor.buffer().read(cx).snapshot(cx),
22362 )
22363 })
22364 .unwrap();
22365
22366 let edits = edits
22367 .into_iter()
22368 .map(|(range, edit)| {
22369 (
22370 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22371 edit,
22372 )
22373 })
22374 .collect::<Vec<_>>();
22375
22376 let text_anchor_edits = edits
22377 .clone()
22378 .into_iter()
22379 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22380 .collect::<Vec<_>>();
22381
22382 let edit_preview = window
22383 .update(cx, |_, _window, cx| {
22384 buffer
22385 .read(cx)
22386 .as_singleton()
22387 .unwrap()
22388 .read(cx)
22389 .preview_edits(text_anchor_edits.into(), cx)
22390 })
22391 .unwrap()
22392 .await;
22393
22394 cx.update(|_window, cx| {
22395 let highlighted_edits = edit_prediction_edit_text(
22396 snapshot.as_singleton().unwrap().2,
22397 &edits,
22398 &edit_preview,
22399 include_deletions,
22400 cx,
22401 );
22402 assertion_fn(highlighted_edits, cx)
22403 });
22404}
22405
22406#[track_caller]
22407fn assert_breakpoint(
22408 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22409 path: &Arc<Path>,
22410 expected: Vec<(u32, Breakpoint)>,
22411) {
22412 if expected.is_empty() {
22413 assert!(!breakpoints.contains_key(path), "{}", path.display());
22414 } else {
22415 let mut breakpoint = breakpoints
22416 .get(path)
22417 .unwrap()
22418 .iter()
22419 .map(|breakpoint| {
22420 (
22421 breakpoint.row,
22422 Breakpoint {
22423 message: breakpoint.message.clone(),
22424 state: breakpoint.state,
22425 condition: breakpoint.condition.clone(),
22426 hit_condition: breakpoint.hit_condition.clone(),
22427 },
22428 )
22429 })
22430 .collect::<Vec<_>>();
22431
22432 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22433
22434 assert_eq!(expected, breakpoint);
22435 }
22436}
22437
22438fn add_log_breakpoint_at_cursor(
22439 editor: &mut Editor,
22440 log_message: &str,
22441 window: &mut Window,
22442 cx: &mut Context<Editor>,
22443) {
22444 let (anchor, bp) = editor
22445 .breakpoints_at_cursors(window, cx)
22446 .first()
22447 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22448 .unwrap_or_else(|| {
22449 let cursor_position: Point = editor.selections.newest(cx).head();
22450
22451 let breakpoint_position = editor
22452 .snapshot(window, cx)
22453 .display_snapshot
22454 .buffer_snapshot
22455 .anchor_before(Point::new(cursor_position.row, 0));
22456
22457 (breakpoint_position, Breakpoint::new_log(log_message))
22458 });
22459
22460 editor.edit_breakpoint_at_anchor(
22461 anchor,
22462 bp,
22463 BreakpointEditAction::EditLogMessage(log_message.into()),
22464 cx,
22465 );
22466}
22467
22468#[gpui::test]
22469async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22470 init_test(cx, |_| {});
22471
22472 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22473 let fs = FakeFs::new(cx.executor());
22474 fs.insert_tree(
22475 path!("/a"),
22476 json!({
22477 "main.rs": sample_text,
22478 }),
22479 )
22480 .await;
22481 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22482 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22483 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22484
22485 let fs = FakeFs::new(cx.executor());
22486 fs.insert_tree(
22487 path!("/a"),
22488 json!({
22489 "main.rs": sample_text,
22490 }),
22491 )
22492 .await;
22493 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22494 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22495 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22496 let worktree_id = workspace
22497 .update(cx, |workspace, _window, cx| {
22498 workspace.project().update(cx, |project, cx| {
22499 project.worktrees(cx).next().unwrap().read(cx).id()
22500 })
22501 })
22502 .unwrap();
22503
22504 let buffer = project
22505 .update(cx, |project, cx| {
22506 project.open_buffer((worktree_id, "main.rs"), cx)
22507 })
22508 .await
22509 .unwrap();
22510
22511 let (editor, cx) = cx.add_window_view(|window, cx| {
22512 Editor::new(
22513 EditorMode::full(),
22514 MultiBuffer::build_from_buffer(buffer, cx),
22515 Some(project.clone()),
22516 window,
22517 cx,
22518 )
22519 });
22520
22521 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22522 let abs_path = project.read_with(cx, |project, cx| {
22523 project
22524 .absolute_path(&project_path, cx)
22525 .map(Arc::from)
22526 .unwrap()
22527 });
22528
22529 // assert we can add breakpoint on the first line
22530 editor.update_in(cx, |editor, window, cx| {
22531 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22532 editor.move_to_end(&MoveToEnd, window, cx);
22533 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22534 });
22535
22536 let breakpoints = editor.update(cx, |editor, cx| {
22537 editor
22538 .breakpoint_store()
22539 .as_ref()
22540 .unwrap()
22541 .read(cx)
22542 .all_source_breakpoints(cx)
22543 });
22544
22545 assert_eq!(1, breakpoints.len());
22546 assert_breakpoint(
22547 &breakpoints,
22548 &abs_path,
22549 vec![
22550 (0, Breakpoint::new_standard()),
22551 (3, Breakpoint::new_standard()),
22552 ],
22553 );
22554
22555 editor.update_in(cx, |editor, window, cx| {
22556 editor.move_to_beginning(&MoveToBeginning, window, cx);
22557 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22558 });
22559
22560 let breakpoints = editor.update(cx, |editor, cx| {
22561 editor
22562 .breakpoint_store()
22563 .as_ref()
22564 .unwrap()
22565 .read(cx)
22566 .all_source_breakpoints(cx)
22567 });
22568
22569 assert_eq!(1, breakpoints.len());
22570 assert_breakpoint(
22571 &breakpoints,
22572 &abs_path,
22573 vec![(3, Breakpoint::new_standard())],
22574 );
22575
22576 editor.update_in(cx, |editor, window, cx| {
22577 editor.move_to_end(&MoveToEnd, window, cx);
22578 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22579 });
22580
22581 let breakpoints = editor.update(cx, |editor, cx| {
22582 editor
22583 .breakpoint_store()
22584 .as_ref()
22585 .unwrap()
22586 .read(cx)
22587 .all_source_breakpoints(cx)
22588 });
22589
22590 assert_eq!(0, breakpoints.len());
22591 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22592}
22593
22594#[gpui::test]
22595async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22596 init_test(cx, |_| {});
22597
22598 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22599
22600 let fs = FakeFs::new(cx.executor());
22601 fs.insert_tree(
22602 path!("/a"),
22603 json!({
22604 "main.rs": sample_text,
22605 }),
22606 )
22607 .await;
22608 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22609 let (workspace, cx) =
22610 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22611
22612 let worktree_id = workspace.update(cx, |workspace, cx| {
22613 workspace.project().update(cx, |project, cx| {
22614 project.worktrees(cx).next().unwrap().read(cx).id()
22615 })
22616 });
22617
22618 let buffer = project
22619 .update(cx, |project, cx| {
22620 project.open_buffer((worktree_id, "main.rs"), cx)
22621 })
22622 .await
22623 .unwrap();
22624
22625 let (editor, cx) = cx.add_window_view(|window, cx| {
22626 Editor::new(
22627 EditorMode::full(),
22628 MultiBuffer::build_from_buffer(buffer, cx),
22629 Some(project.clone()),
22630 window,
22631 cx,
22632 )
22633 });
22634
22635 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22636 let abs_path = project.read_with(cx, |project, cx| {
22637 project
22638 .absolute_path(&project_path, cx)
22639 .map(Arc::from)
22640 .unwrap()
22641 });
22642
22643 editor.update_in(cx, |editor, window, cx| {
22644 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22645 });
22646
22647 let breakpoints = editor.update(cx, |editor, cx| {
22648 editor
22649 .breakpoint_store()
22650 .as_ref()
22651 .unwrap()
22652 .read(cx)
22653 .all_source_breakpoints(cx)
22654 });
22655
22656 assert_breakpoint(
22657 &breakpoints,
22658 &abs_path,
22659 vec![(0, Breakpoint::new_log("hello world"))],
22660 );
22661
22662 // Removing a log message from a log breakpoint should remove it
22663 editor.update_in(cx, |editor, window, cx| {
22664 add_log_breakpoint_at_cursor(editor, "", window, cx);
22665 });
22666
22667 let breakpoints = editor.update(cx, |editor, cx| {
22668 editor
22669 .breakpoint_store()
22670 .as_ref()
22671 .unwrap()
22672 .read(cx)
22673 .all_source_breakpoints(cx)
22674 });
22675
22676 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22677
22678 editor.update_in(cx, |editor, window, cx| {
22679 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22680 editor.move_to_end(&MoveToEnd, window, cx);
22681 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22682 // Not adding a log message to a standard breakpoint shouldn't remove it
22683 add_log_breakpoint_at_cursor(editor, "", window, cx);
22684 });
22685
22686 let breakpoints = editor.update(cx, |editor, cx| {
22687 editor
22688 .breakpoint_store()
22689 .as_ref()
22690 .unwrap()
22691 .read(cx)
22692 .all_source_breakpoints(cx)
22693 });
22694
22695 assert_breakpoint(
22696 &breakpoints,
22697 &abs_path,
22698 vec![
22699 (0, Breakpoint::new_standard()),
22700 (3, Breakpoint::new_standard()),
22701 ],
22702 );
22703
22704 editor.update_in(cx, |editor, window, cx| {
22705 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22706 });
22707
22708 let breakpoints = editor.update(cx, |editor, cx| {
22709 editor
22710 .breakpoint_store()
22711 .as_ref()
22712 .unwrap()
22713 .read(cx)
22714 .all_source_breakpoints(cx)
22715 });
22716
22717 assert_breakpoint(
22718 &breakpoints,
22719 &abs_path,
22720 vec![
22721 (0, Breakpoint::new_standard()),
22722 (3, Breakpoint::new_log("hello world")),
22723 ],
22724 );
22725
22726 editor.update_in(cx, |editor, window, cx| {
22727 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22728 });
22729
22730 let breakpoints = editor.update(cx, |editor, cx| {
22731 editor
22732 .breakpoint_store()
22733 .as_ref()
22734 .unwrap()
22735 .read(cx)
22736 .all_source_breakpoints(cx)
22737 });
22738
22739 assert_breakpoint(
22740 &breakpoints,
22741 &abs_path,
22742 vec![
22743 (0, Breakpoint::new_standard()),
22744 (3, Breakpoint::new_log("hello Earth!!")),
22745 ],
22746 );
22747}
22748
22749/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22750/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22751/// or when breakpoints were placed out of order. This tests for a regression too
22752#[gpui::test]
22753async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22754 init_test(cx, |_| {});
22755
22756 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22757 let fs = FakeFs::new(cx.executor());
22758 fs.insert_tree(
22759 path!("/a"),
22760 json!({
22761 "main.rs": sample_text,
22762 }),
22763 )
22764 .await;
22765 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22766 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22767 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22768
22769 let fs = FakeFs::new(cx.executor());
22770 fs.insert_tree(
22771 path!("/a"),
22772 json!({
22773 "main.rs": sample_text,
22774 }),
22775 )
22776 .await;
22777 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22778 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22779 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22780 let worktree_id = workspace
22781 .update(cx, |workspace, _window, cx| {
22782 workspace.project().update(cx, |project, cx| {
22783 project.worktrees(cx).next().unwrap().read(cx).id()
22784 })
22785 })
22786 .unwrap();
22787
22788 let buffer = project
22789 .update(cx, |project, cx| {
22790 project.open_buffer((worktree_id, "main.rs"), cx)
22791 })
22792 .await
22793 .unwrap();
22794
22795 let (editor, cx) = cx.add_window_view(|window, cx| {
22796 Editor::new(
22797 EditorMode::full(),
22798 MultiBuffer::build_from_buffer(buffer, cx),
22799 Some(project.clone()),
22800 window,
22801 cx,
22802 )
22803 });
22804
22805 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22806 let abs_path = project.read_with(cx, |project, cx| {
22807 project
22808 .absolute_path(&project_path, cx)
22809 .map(Arc::from)
22810 .unwrap()
22811 });
22812
22813 // assert we can add breakpoint on the first line
22814 editor.update_in(cx, |editor, window, cx| {
22815 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22816 editor.move_to_end(&MoveToEnd, window, cx);
22817 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22818 editor.move_up(&MoveUp, window, cx);
22819 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22820 });
22821
22822 let breakpoints = editor.update(cx, |editor, cx| {
22823 editor
22824 .breakpoint_store()
22825 .as_ref()
22826 .unwrap()
22827 .read(cx)
22828 .all_source_breakpoints(cx)
22829 });
22830
22831 assert_eq!(1, breakpoints.len());
22832 assert_breakpoint(
22833 &breakpoints,
22834 &abs_path,
22835 vec![
22836 (0, Breakpoint::new_standard()),
22837 (2, Breakpoint::new_standard()),
22838 (3, Breakpoint::new_standard()),
22839 ],
22840 );
22841
22842 editor.update_in(cx, |editor, window, cx| {
22843 editor.move_to_beginning(&MoveToBeginning, window, cx);
22844 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22845 editor.move_to_end(&MoveToEnd, window, cx);
22846 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22847 // Disabling a breakpoint that doesn't exist should do nothing
22848 editor.move_up(&MoveUp, window, cx);
22849 editor.move_up(&MoveUp, window, cx);
22850 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22851 });
22852
22853 let breakpoints = editor.update(cx, |editor, cx| {
22854 editor
22855 .breakpoint_store()
22856 .as_ref()
22857 .unwrap()
22858 .read(cx)
22859 .all_source_breakpoints(cx)
22860 });
22861
22862 let disable_breakpoint = {
22863 let mut bp = Breakpoint::new_standard();
22864 bp.state = BreakpointState::Disabled;
22865 bp
22866 };
22867
22868 assert_eq!(1, breakpoints.len());
22869 assert_breakpoint(
22870 &breakpoints,
22871 &abs_path,
22872 vec![
22873 (0, disable_breakpoint.clone()),
22874 (2, Breakpoint::new_standard()),
22875 (3, disable_breakpoint.clone()),
22876 ],
22877 );
22878
22879 editor.update_in(cx, |editor, window, cx| {
22880 editor.move_to_beginning(&MoveToBeginning, window, cx);
22881 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22882 editor.move_to_end(&MoveToEnd, window, cx);
22883 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22884 editor.move_up(&MoveUp, window, cx);
22885 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22886 });
22887
22888 let breakpoints = editor.update(cx, |editor, cx| {
22889 editor
22890 .breakpoint_store()
22891 .as_ref()
22892 .unwrap()
22893 .read(cx)
22894 .all_source_breakpoints(cx)
22895 });
22896
22897 assert_eq!(1, breakpoints.len());
22898 assert_breakpoint(
22899 &breakpoints,
22900 &abs_path,
22901 vec![
22902 (0, Breakpoint::new_standard()),
22903 (2, disable_breakpoint),
22904 (3, Breakpoint::new_standard()),
22905 ],
22906 );
22907}
22908
22909#[gpui::test]
22910async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22911 init_test(cx, |_| {});
22912 let capabilities = lsp::ServerCapabilities {
22913 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22914 prepare_provider: Some(true),
22915 work_done_progress_options: Default::default(),
22916 })),
22917 ..Default::default()
22918 };
22919 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22920
22921 cx.set_state(indoc! {"
22922 struct Fˇoo {}
22923 "});
22924
22925 cx.update_editor(|editor, _, cx| {
22926 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22927 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22928 editor.highlight_background::<DocumentHighlightRead>(
22929 &[highlight_range],
22930 |theme| theme.colors().editor_document_highlight_read_background,
22931 cx,
22932 );
22933 });
22934
22935 let mut prepare_rename_handler = cx
22936 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22937 move |_, _, _| async move {
22938 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22939 start: lsp::Position {
22940 line: 0,
22941 character: 7,
22942 },
22943 end: lsp::Position {
22944 line: 0,
22945 character: 10,
22946 },
22947 })))
22948 },
22949 );
22950 let prepare_rename_task = cx
22951 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
22952 .expect("Prepare rename was not started");
22953 prepare_rename_handler.next().await.unwrap();
22954 prepare_rename_task.await.expect("Prepare rename failed");
22955
22956 let mut rename_handler =
22957 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
22958 let edit = lsp::TextEdit {
22959 range: lsp::Range {
22960 start: lsp::Position {
22961 line: 0,
22962 character: 7,
22963 },
22964 end: lsp::Position {
22965 line: 0,
22966 character: 10,
22967 },
22968 },
22969 new_text: "FooRenamed".to_string(),
22970 };
22971 Ok(Some(lsp::WorkspaceEdit::new(
22972 // Specify the same edit twice
22973 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
22974 )))
22975 });
22976 let rename_task = cx
22977 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
22978 .expect("Confirm rename was not started");
22979 rename_handler.next().await.unwrap();
22980 rename_task.await.expect("Confirm rename failed");
22981 cx.run_until_parked();
22982
22983 // Despite two edits, only one is actually applied as those are identical
22984 cx.assert_editor_state(indoc! {"
22985 struct FooRenamedˇ {}
22986 "});
22987}
22988
22989#[gpui::test]
22990async fn test_rename_without_prepare(cx: &mut TestAppContext) {
22991 init_test(cx, |_| {});
22992 // These capabilities indicate that the server does not support prepare rename.
22993 let capabilities = lsp::ServerCapabilities {
22994 rename_provider: Some(lsp::OneOf::Left(true)),
22995 ..Default::default()
22996 };
22997 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22998
22999 cx.set_state(indoc! {"
23000 struct Fˇoo {}
23001 "});
23002
23003 cx.update_editor(|editor, _window, cx| {
23004 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23005 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23006 editor.highlight_background::<DocumentHighlightRead>(
23007 &[highlight_range],
23008 |theme| theme.colors().editor_document_highlight_read_background,
23009 cx,
23010 );
23011 });
23012
23013 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23014 .expect("Prepare rename was not started")
23015 .await
23016 .expect("Prepare rename failed");
23017
23018 let mut rename_handler =
23019 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23020 let edit = lsp::TextEdit {
23021 range: lsp::Range {
23022 start: lsp::Position {
23023 line: 0,
23024 character: 7,
23025 },
23026 end: lsp::Position {
23027 line: 0,
23028 character: 10,
23029 },
23030 },
23031 new_text: "FooRenamed".to_string(),
23032 };
23033 Ok(Some(lsp::WorkspaceEdit::new(
23034 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23035 )))
23036 });
23037 let rename_task = cx
23038 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23039 .expect("Confirm rename was not started");
23040 rename_handler.next().await.unwrap();
23041 rename_task.await.expect("Confirm rename failed");
23042 cx.run_until_parked();
23043
23044 // Correct range is renamed, as `surrounding_word` is used to find it.
23045 cx.assert_editor_state(indoc! {"
23046 struct FooRenamedˇ {}
23047 "});
23048}
23049
23050#[gpui::test]
23051async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23052 init_test(cx, |_| {});
23053 let mut cx = EditorTestContext::new(cx).await;
23054
23055 let language = Arc::new(
23056 Language::new(
23057 LanguageConfig::default(),
23058 Some(tree_sitter_html::LANGUAGE.into()),
23059 )
23060 .with_brackets_query(
23061 r#"
23062 ("<" @open "/>" @close)
23063 ("</" @open ">" @close)
23064 ("<" @open ">" @close)
23065 ("\"" @open "\"" @close)
23066 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23067 "#,
23068 )
23069 .unwrap(),
23070 );
23071 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23072
23073 cx.set_state(indoc! {"
23074 <span>ˇ</span>
23075 "});
23076 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23077 cx.assert_editor_state(indoc! {"
23078 <span>
23079 ˇ
23080 </span>
23081 "});
23082
23083 cx.set_state(indoc! {"
23084 <span><span></span>ˇ</span>
23085 "});
23086 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23087 cx.assert_editor_state(indoc! {"
23088 <span><span></span>
23089 ˇ</span>
23090 "});
23091
23092 cx.set_state(indoc! {"
23093 <span>ˇ
23094 </span>
23095 "});
23096 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23097 cx.assert_editor_state(indoc! {"
23098 <span>
23099 ˇ
23100 </span>
23101 "});
23102}
23103
23104#[gpui::test(iterations = 10)]
23105async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23106 init_test(cx, |_| {});
23107
23108 let fs = FakeFs::new(cx.executor());
23109 fs.insert_tree(
23110 path!("/dir"),
23111 json!({
23112 "a.ts": "a",
23113 }),
23114 )
23115 .await;
23116
23117 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23118 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23119 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23120
23121 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23122 language_registry.add(Arc::new(Language::new(
23123 LanguageConfig {
23124 name: "TypeScript".into(),
23125 matcher: LanguageMatcher {
23126 path_suffixes: vec!["ts".to_string()],
23127 ..Default::default()
23128 },
23129 ..Default::default()
23130 },
23131 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23132 )));
23133 let mut fake_language_servers = language_registry.register_fake_lsp(
23134 "TypeScript",
23135 FakeLspAdapter {
23136 capabilities: lsp::ServerCapabilities {
23137 code_lens_provider: Some(lsp::CodeLensOptions {
23138 resolve_provider: Some(true),
23139 }),
23140 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23141 commands: vec!["_the/command".to_string()],
23142 ..lsp::ExecuteCommandOptions::default()
23143 }),
23144 ..lsp::ServerCapabilities::default()
23145 },
23146 ..FakeLspAdapter::default()
23147 },
23148 );
23149
23150 let editor = workspace
23151 .update(cx, |workspace, window, cx| {
23152 workspace.open_abs_path(
23153 PathBuf::from(path!("/dir/a.ts")),
23154 OpenOptions::default(),
23155 window,
23156 cx,
23157 )
23158 })
23159 .unwrap()
23160 .await
23161 .unwrap()
23162 .downcast::<Editor>()
23163 .unwrap();
23164 cx.executor().run_until_parked();
23165
23166 let fake_server = fake_language_servers.next().await.unwrap();
23167
23168 let buffer = editor.update(cx, |editor, cx| {
23169 editor
23170 .buffer()
23171 .read(cx)
23172 .as_singleton()
23173 .expect("have opened a single file by path")
23174 });
23175
23176 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23177 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23178 drop(buffer_snapshot);
23179 let actions = cx
23180 .update_window(*workspace, |_, window, cx| {
23181 project.code_actions(&buffer, anchor..anchor, window, cx)
23182 })
23183 .unwrap();
23184
23185 fake_server
23186 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23187 Ok(Some(vec![
23188 lsp::CodeLens {
23189 range: lsp::Range::default(),
23190 command: Some(lsp::Command {
23191 title: "Code lens command".to_owned(),
23192 command: "_the/command".to_owned(),
23193 arguments: None,
23194 }),
23195 data: None,
23196 },
23197 lsp::CodeLens {
23198 range: lsp::Range::default(),
23199 command: Some(lsp::Command {
23200 title: "Command not in capabilities".to_owned(),
23201 command: "not in capabilities".to_owned(),
23202 arguments: None,
23203 }),
23204 data: None,
23205 },
23206 lsp::CodeLens {
23207 range: lsp::Range {
23208 start: lsp::Position {
23209 line: 1,
23210 character: 1,
23211 },
23212 end: lsp::Position {
23213 line: 1,
23214 character: 1,
23215 },
23216 },
23217 command: Some(lsp::Command {
23218 title: "Command not in range".to_owned(),
23219 command: "_the/command".to_owned(),
23220 arguments: None,
23221 }),
23222 data: None,
23223 },
23224 ]))
23225 })
23226 .next()
23227 .await;
23228
23229 let actions = actions.await.unwrap();
23230 assert_eq!(
23231 actions.len(),
23232 1,
23233 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23234 );
23235 let action = actions[0].clone();
23236 let apply = project.update(cx, |project, cx| {
23237 project.apply_code_action(buffer.clone(), action, true, cx)
23238 });
23239
23240 // Resolving the code action does not populate its edits. In absence of
23241 // edits, we must execute the given command.
23242 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23243 |mut lens, _| async move {
23244 let lens_command = lens.command.as_mut().expect("should have a command");
23245 assert_eq!(lens_command.title, "Code lens command");
23246 lens_command.arguments = Some(vec![json!("the-argument")]);
23247 Ok(lens)
23248 },
23249 );
23250
23251 // While executing the command, the language server sends the editor
23252 // a `workspaceEdit` request.
23253 fake_server
23254 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23255 let fake = fake_server.clone();
23256 move |params, _| {
23257 assert_eq!(params.command, "_the/command");
23258 let fake = fake.clone();
23259 async move {
23260 fake.server
23261 .request::<lsp::request::ApplyWorkspaceEdit>(
23262 lsp::ApplyWorkspaceEditParams {
23263 label: None,
23264 edit: lsp::WorkspaceEdit {
23265 changes: Some(
23266 [(
23267 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23268 vec![lsp::TextEdit {
23269 range: lsp::Range::new(
23270 lsp::Position::new(0, 0),
23271 lsp::Position::new(0, 0),
23272 ),
23273 new_text: "X".into(),
23274 }],
23275 )]
23276 .into_iter()
23277 .collect(),
23278 ),
23279 ..lsp::WorkspaceEdit::default()
23280 },
23281 },
23282 )
23283 .await
23284 .into_response()
23285 .unwrap();
23286 Ok(Some(json!(null)))
23287 }
23288 }
23289 })
23290 .next()
23291 .await;
23292
23293 // Applying the code lens command returns a project transaction containing the edits
23294 // sent by the language server in its `workspaceEdit` request.
23295 let transaction = apply.await.unwrap();
23296 assert!(transaction.0.contains_key(&buffer));
23297 buffer.update(cx, |buffer, cx| {
23298 assert_eq!(buffer.text(), "Xa");
23299 buffer.undo(cx);
23300 assert_eq!(buffer.text(), "a");
23301 });
23302
23303 let actions_after_edits = cx
23304 .update_window(*workspace, |_, window, cx| {
23305 project.code_actions(&buffer, anchor..anchor, window, cx)
23306 })
23307 .unwrap()
23308 .await
23309 .unwrap();
23310 assert_eq!(
23311 actions, actions_after_edits,
23312 "For the same selection, same code lens actions should be returned"
23313 );
23314
23315 let _responses =
23316 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23317 panic!("No more code lens requests are expected");
23318 });
23319 editor.update_in(cx, |editor, window, cx| {
23320 editor.select_all(&SelectAll, window, cx);
23321 });
23322 cx.executor().run_until_parked();
23323 let new_actions = cx
23324 .update_window(*workspace, |_, window, cx| {
23325 project.code_actions(&buffer, anchor..anchor, window, cx)
23326 })
23327 .unwrap()
23328 .await
23329 .unwrap();
23330 assert_eq!(
23331 actions, new_actions,
23332 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23333 );
23334}
23335
23336#[gpui::test]
23337async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23338 init_test(cx, |_| {});
23339
23340 let fs = FakeFs::new(cx.executor());
23341 let main_text = r#"fn main() {
23342println!("1");
23343println!("2");
23344println!("3");
23345println!("4");
23346println!("5");
23347}"#;
23348 let lib_text = "mod foo {}";
23349 fs.insert_tree(
23350 path!("/a"),
23351 json!({
23352 "lib.rs": lib_text,
23353 "main.rs": main_text,
23354 }),
23355 )
23356 .await;
23357
23358 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23359 let (workspace, cx) =
23360 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23361 let worktree_id = workspace.update(cx, |workspace, cx| {
23362 workspace.project().update(cx, |project, cx| {
23363 project.worktrees(cx).next().unwrap().read(cx).id()
23364 })
23365 });
23366
23367 let expected_ranges = vec![
23368 Point::new(0, 0)..Point::new(0, 0),
23369 Point::new(1, 0)..Point::new(1, 1),
23370 Point::new(2, 0)..Point::new(2, 2),
23371 Point::new(3, 0)..Point::new(3, 3),
23372 ];
23373
23374 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23375 let editor_1 = workspace
23376 .update_in(cx, |workspace, window, cx| {
23377 workspace.open_path(
23378 (worktree_id, "main.rs"),
23379 Some(pane_1.downgrade()),
23380 true,
23381 window,
23382 cx,
23383 )
23384 })
23385 .unwrap()
23386 .await
23387 .downcast::<Editor>()
23388 .unwrap();
23389 pane_1.update(cx, |pane, cx| {
23390 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23391 open_editor.update(cx, |editor, cx| {
23392 assert_eq!(
23393 editor.display_text(cx),
23394 main_text,
23395 "Original main.rs text on initial open",
23396 );
23397 assert_eq!(
23398 editor
23399 .selections
23400 .all::<Point>(cx)
23401 .into_iter()
23402 .map(|s| s.range())
23403 .collect::<Vec<_>>(),
23404 vec![Point::zero()..Point::zero()],
23405 "Default selections on initial open",
23406 );
23407 })
23408 });
23409 editor_1.update_in(cx, |editor, window, cx| {
23410 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23411 s.select_ranges(expected_ranges.clone());
23412 });
23413 });
23414
23415 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23416 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23417 });
23418 let editor_2 = workspace
23419 .update_in(cx, |workspace, window, cx| {
23420 workspace.open_path(
23421 (worktree_id, "main.rs"),
23422 Some(pane_2.downgrade()),
23423 true,
23424 window,
23425 cx,
23426 )
23427 })
23428 .unwrap()
23429 .await
23430 .downcast::<Editor>()
23431 .unwrap();
23432 pane_2.update(cx, |pane, cx| {
23433 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23434 open_editor.update(cx, |editor, cx| {
23435 assert_eq!(
23436 editor.display_text(cx),
23437 main_text,
23438 "Original main.rs text on initial open in another panel",
23439 );
23440 assert_eq!(
23441 editor
23442 .selections
23443 .all::<Point>(cx)
23444 .into_iter()
23445 .map(|s| s.range())
23446 .collect::<Vec<_>>(),
23447 vec![Point::zero()..Point::zero()],
23448 "Default selections on initial open in another panel",
23449 );
23450 })
23451 });
23452
23453 editor_2.update_in(cx, |editor, window, cx| {
23454 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23455 });
23456
23457 let _other_editor_1 = workspace
23458 .update_in(cx, |workspace, window, cx| {
23459 workspace.open_path(
23460 (worktree_id, "lib.rs"),
23461 Some(pane_1.downgrade()),
23462 true,
23463 window,
23464 cx,
23465 )
23466 })
23467 .unwrap()
23468 .await
23469 .downcast::<Editor>()
23470 .unwrap();
23471 pane_1
23472 .update_in(cx, |pane, window, cx| {
23473 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23474 })
23475 .await
23476 .unwrap();
23477 drop(editor_1);
23478 pane_1.update(cx, |pane, cx| {
23479 pane.active_item()
23480 .unwrap()
23481 .downcast::<Editor>()
23482 .unwrap()
23483 .update(cx, |editor, cx| {
23484 assert_eq!(
23485 editor.display_text(cx),
23486 lib_text,
23487 "Other file should be open and active",
23488 );
23489 });
23490 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23491 });
23492
23493 let _other_editor_2 = workspace
23494 .update_in(cx, |workspace, window, cx| {
23495 workspace.open_path(
23496 (worktree_id, "lib.rs"),
23497 Some(pane_2.downgrade()),
23498 true,
23499 window,
23500 cx,
23501 )
23502 })
23503 .unwrap()
23504 .await
23505 .downcast::<Editor>()
23506 .unwrap();
23507 pane_2
23508 .update_in(cx, |pane, window, cx| {
23509 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23510 })
23511 .await
23512 .unwrap();
23513 drop(editor_2);
23514 pane_2.update(cx, |pane, cx| {
23515 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23516 open_editor.update(cx, |editor, cx| {
23517 assert_eq!(
23518 editor.display_text(cx),
23519 lib_text,
23520 "Other file should be open and active in another panel too",
23521 );
23522 });
23523 assert_eq!(
23524 pane.items().count(),
23525 1,
23526 "No other editors should be open in another pane",
23527 );
23528 });
23529
23530 let _editor_1_reopened = workspace
23531 .update_in(cx, |workspace, window, cx| {
23532 workspace.open_path(
23533 (worktree_id, "main.rs"),
23534 Some(pane_1.downgrade()),
23535 true,
23536 window,
23537 cx,
23538 )
23539 })
23540 .unwrap()
23541 .await
23542 .downcast::<Editor>()
23543 .unwrap();
23544 let _editor_2_reopened = workspace
23545 .update_in(cx, |workspace, window, cx| {
23546 workspace.open_path(
23547 (worktree_id, "main.rs"),
23548 Some(pane_2.downgrade()),
23549 true,
23550 window,
23551 cx,
23552 )
23553 })
23554 .unwrap()
23555 .await
23556 .downcast::<Editor>()
23557 .unwrap();
23558 pane_1.update(cx, |pane, cx| {
23559 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23560 open_editor.update(cx, |editor, cx| {
23561 assert_eq!(
23562 editor.display_text(cx),
23563 main_text,
23564 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23565 );
23566 assert_eq!(
23567 editor
23568 .selections
23569 .all::<Point>(cx)
23570 .into_iter()
23571 .map(|s| s.range())
23572 .collect::<Vec<_>>(),
23573 expected_ranges,
23574 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23575 );
23576 })
23577 });
23578 pane_2.update(cx, |pane, cx| {
23579 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23580 open_editor.update(cx, |editor, cx| {
23581 assert_eq!(
23582 editor.display_text(cx),
23583 r#"fn main() {
23584⋯rintln!("1");
23585⋯intln!("2");
23586⋯ntln!("3");
23587println!("4");
23588println!("5");
23589}"#,
23590 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23591 );
23592 assert_eq!(
23593 editor
23594 .selections
23595 .all::<Point>(cx)
23596 .into_iter()
23597 .map(|s| s.range())
23598 .collect::<Vec<_>>(),
23599 vec![Point::zero()..Point::zero()],
23600 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23601 );
23602 })
23603 });
23604}
23605
23606#[gpui::test]
23607async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23608 init_test(cx, |_| {});
23609
23610 let fs = FakeFs::new(cx.executor());
23611 let main_text = r#"fn main() {
23612println!("1");
23613println!("2");
23614println!("3");
23615println!("4");
23616println!("5");
23617}"#;
23618 let lib_text = "mod foo {}";
23619 fs.insert_tree(
23620 path!("/a"),
23621 json!({
23622 "lib.rs": lib_text,
23623 "main.rs": main_text,
23624 }),
23625 )
23626 .await;
23627
23628 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23629 let (workspace, cx) =
23630 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23631 let worktree_id = workspace.update(cx, |workspace, cx| {
23632 workspace.project().update(cx, |project, cx| {
23633 project.worktrees(cx).next().unwrap().read(cx).id()
23634 })
23635 });
23636
23637 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23638 let editor = workspace
23639 .update_in(cx, |workspace, window, cx| {
23640 workspace.open_path(
23641 (worktree_id, "main.rs"),
23642 Some(pane.downgrade()),
23643 true,
23644 window,
23645 cx,
23646 )
23647 })
23648 .unwrap()
23649 .await
23650 .downcast::<Editor>()
23651 .unwrap();
23652 pane.update(cx, |pane, cx| {
23653 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23654 open_editor.update(cx, |editor, cx| {
23655 assert_eq!(
23656 editor.display_text(cx),
23657 main_text,
23658 "Original main.rs text on initial open",
23659 );
23660 })
23661 });
23662 editor.update_in(cx, |editor, window, cx| {
23663 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23664 });
23665
23666 cx.update_global(|store: &mut SettingsStore, cx| {
23667 store.update_user_settings(cx, |s| {
23668 s.workspace.restore_on_file_reopen = Some(false);
23669 });
23670 });
23671 editor.update_in(cx, |editor, window, cx| {
23672 editor.fold_ranges(
23673 vec![
23674 Point::new(1, 0)..Point::new(1, 1),
23675 Point::new(2, 0)..Point::new(2, 2),
23676 Point::new(3, 0)..Point::new(3, 3),
23677 ],
23678 false,
23679 window,
23680 cx,
23681 );
23682 });
23683 pane.update_in(cx, |pane, window, cx| {
23684 pane.close_all_items(&CloseAllItems::default(), window, cx)
23685 })
23686 .await
23687 .unwrap();
23688 pane.update(cx, |pane, _| {
23689 assert!(pane.active_item().is_none());
23690 });
23691 cx.update_global(|store: &mut SettingsStore, cx| {
23692 store.update_user_settings(cx, |s| {
23693 s.workspace.restore_on_file_reopen = Some(true);
23694 });
23695 });
23696
23697 let _editor_reopened = workspace
23698 .update_in(cx, |workspace, window, cx| {
23699 workspace.open_path(
23700 (worktree_id, "main.rs"),
23701 Some(pane.downgrade()),
23702 true,
23703 window,
23704 cx,
23705 )
23706 })
23707 .unwrap()
23708 .await
23709 .downcast::<Editor>()
23710 .unwrap();
23711 pane.update(cx, |pane, cx| {
23712 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23713 open_editor.update(cx, |editor, cx| {
23714 assert_eq!(
23715 editor.display_text(cx),
23716 main_text,
23717 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23718 );
23719 })
23720 });
23721}
23722
23723#[gpui::test]
23724async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23725 struct EmptyModalView {
23726 focus_handle: gpui::FocusHandle,
23727 }
23728 impl EventEmitter<DismissEvent> for EmptyModalView {}
23729 impl Render for EmptyModalView {
23730 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23731 div()
23732 }
23733 }
23734 impl Focusable for EmptyModalView {
23735 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23736 self.focus_handle.clone()
23737 }
23738 }
23739 impl workspace::ModalView for EmptyModalView {}
23740 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23741 EmptyModalView {
23742 focus_handle: cx.focus_handle(),
23743 }
23744 }
23745
23746 init_test(cx, |_| {});
23747
23748 let fs = FakeFs::new(cx.executor());
23749 let project = Project::test(fs, [], cx).await;
23750 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23751 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23752 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23753 let editor = cx.new_window_entity(|window, cx| {
23754 Editor::new(
23755 EditorMode::full(),
23756 buffer,
23757 Some(project.clone()),
23758 window,
23759 cx,
23760 )
23761 });
23762 workspace
23763 .update(cx, |workspace, window, cx| {
23764 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23765 })
23766 .unwrap();
23767 editor.update_in(cx, |editor, window, cx| {
23768 editor.open_context_menu(&OpenContextMenu, window, cx);
23769 assert!(editor.mouse_context_menu.is_some());
23770 });
23771 workspace
23772 .update(cx, |workspace, window, cx| {
23773 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23774 })
23775 .unwrap();
23776 cx.read(|cx| {
23777 assert!(editor.read(cx).mouse_context_menu.is_none());
23778 });
23779}
23780
23781#[gpui::test]
23782async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23783 init_test(cx, |_| {});
23784
23785 let fs = FakeFs::new(cx.executor());
23786 fs.insert_file(path!("/file.html"), Default::default())
23787 .await;
23788
23789 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23790
23791 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23792 let html_language = Arc::new(Language::new(
23793 LanguageConfig {
23794 name: "HTML".into(),
23795 matcher: LanguageMatcher {
23796 path_suffixes: vec!["html".to_string()],
23797 ..LanguageMatcher::default()
23798 },
23799 brackets: BracketPairConfig {
23800 pairs: vec![BracketPair {
23801 start: "<".into(),
23802 end: ">".into(),
23803 close: true,
23804 ..Default::default()
23805 }],
23806 ..Default::default()
23807 },
23808 ..Default::default()
23809 },
23810 Some(tree_sitter_html::LANGUAGE.into()),
23811 ));
23812 language_registry.add(html_language);
23813 let mut fake_servers = language_registry.register_fake_lsp(
23814 "HTML",
23815 FakeLspAdapter {
23816 capabilities: lsp::ServerCapabilities {
23817 completion_provider: Some(lsp::CompletionOptions {
23818 resolve_provider: Some(true),
23819 ..Default::default()
23820 }),
23821 ..Default::default()
23822 },
23823 ..Default::default()
23824 },
23825 );
23826
23827 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23828 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23829
23830 let worktree_id = workspace
23831 .update(cx, |workspace, _window, cx| {
23832 workspace.project().update(cx, |project, cx| {
23833 project.worktrees(cx).next().unwrap().read(cx).id()
23834 })
23835 })
23836 .unwrap();
23837 project
23838 .update(cx, |project, cx| {
23839 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23840 })
23841 .await
23842 .unwrap();
23843 let editor = workspace
23844 .update(cx, |workspace, window, cx| {
23845 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23846 })
23847 .unwrap()
23848 .await
23849 .unwrap()
23850 .downcast::<Editor>()
23851 .unwrap();
23852
23853 let fake_server = fake_servers.next().await.unwrap();
23854 editor.update_in(cx, |editor, window, cx| {
23855 editor.set_text("<ad></ad>", window, cx);
23856 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23857 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23858 });
23859 let Some((buffer, _)) = editor
23860 .buffer
23861 .read(cx)
23862 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23863 else {
23864 panic!("Failed to get buffer for selection position");
23865 };
23866 let buffer = buffer.read(cx);
23867 let buffer_id = buffer.remote_id();
23868 let opening_range =
23869 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
23870 let closing_range =
23871 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
23872 let mut linked_ranges = HashMap::default();
23873 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23874 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23875 });
23876 let mut completion_handle =
23877 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23878 Ok(Some(lsp::CompletionResponse::Array(vec![
23879 lsp::CompletionItem {
23880 label: "head".to_string(),
23881 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23882 lsp::InsertReplaceEdit {
23883 new_text: "head".to_string(),
23884 insert: lsp::Range::new(
23885 lsp::Position::new(0, 1),
23886 lsp::Position::new(0, 3),
23887 ),
23888 replace: lsp::Range::new(
23889 lsp::Position::new(0, 1),
23890 lsp::Position::new(0, 3),
23891 ),
23892 },
23893 )),
23894 ..Default::default()
23895 },
23896 ])))
23897 });
23898 editor.update_in(cx, |editor, window, cx| {
23899 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23900 });
23901 cx.run_until_parked();
23902 completion_handle.next().await.unwrap();
23903 editor.update(cx, |editor, _| {
23904 assert!(
23905 editor.context_menu_visible(),
23906 "Completion menu should be visible"
23907 );
23908 });
23909 editor.update_in(cx, |editor, window, cx| {
23910 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23911 });
23912 cx.executor().run_until_parked();
23913 editor.update(cx, |editor, cx| {
23914 assert_eq!(editor.text(cx), "<head></head>");
23915 });
23916}
23917
23918#[gpui::test]
23919async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
23920 init_test(cx, |_| {});
23921
23922 let fs = FakeFs::new(cx.executor());
23923 fs.insert_tree(
23924 path!("/root"),
23925 json!({
23926 "a": {
23927 "main.rs": "fn main() {}",
23928 },
23929 "foo": {
23930 "bar": {
23931 "external_file.rs": "pub mod external {}",
23932 }
23933 }
23934 }),
23935 )
23936 .await;
23937
23938 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
23939 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23940 language_registry.add(rust_lang());
23941 let _fake_servers = language_registry.register_fake_lsp(
23942 "Rust",
23943 FakeLspAdapter {
23944 ..FakeLspAdapter::default()
23945 },
23946 );
23947 let (workspace, cx) =
23948 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23949 let worktree_id = workspace.update(cx, |workspace, cx| {
23950 workspace.project().update(cx, |project, cx| {
23951 project.worktrees(cx).next().unwrap().read(cx).id()
23952 })
23953 });
23954
23955 let assert_language_servers_count =
23956 |expected: usize, context: &str, cx: &mut VisualTestContext| {
23957 project.update(cx, |project, cx| {
23958 let current = project
23959 .lsp_store()
23960 .read(cx)
23961 .as_local()
23962 .unwrap()
23963 .language_servers
23964 .len();
23965 assert_eq!(expected, current, "{context}");
23966 });
23967 };
23968
23969 assert_language_servers_count(
23970 0,
23971 "No servers should be running before any file is open",
23972 cx,
23973 );
23974 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23975 let main_editor = workspace
23976 .update_in(cx, |workspace, window, cx| {
23977 workspace.open_path(
23978 (worktree_id, "main.rs"),
23979 Some(pane.downgrade()),
23980 true,
23981 window,
23982 cx,
23983 )
23984 })
23985 .unwrap()
23986 .await
23987 .downcast::<Editor>()
23988 .unwrap();
23989 pane.update(cx, |pane, cx| {
23990 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23991 open_editor.update(cx, |editor, cx| {
23992 assert_eq!(
23993 editor.display_text(cx),
23994 "fn main() {}",
23995 "Original main.rs text on initial open",
23996 );
23997 });
23998 assert_eq!(open_editor, main_editor);
23999 });
24000 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24001
24002 let external_editor = workspace
24003 .update_in(cx, |workspace, window, cx| {
24004 workspace.open_abs_path(
24005 PathBuf::from("/root/foo/bar/external_file.rs"),
24006 OpenOptions::default(),
24007 window,
24008 cx,
24009 )
24010 })
24011 .await
24012 .expect("opening external file")
24013 .downcast::<Editor>()
24014 .expect("downcasted external file's open element to editor");
24015 pane.update(cx, |pane, cx| {
24016 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24017 open_editor.update(cx, |editor, cx| {
24018 assert_eq!(
24019 editor.display_text(cx),
24020 "pub mod external {}",
24021 "External file is open now",
24022 );
24023 });
24024 assert_eq!(open_editor, external_editor);
24025 });
24026 assert_language_servers_count(
24027 1,
24028 "Second, external, *.rs file should join the existing server",
24029 cx,
24030 );
24031
24032 pane.update_in(cx, |pane, window, cx| {
24033 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24034 })
24035 .await
24036 .unwrap();
24037 pane.update_in(cx, |pane, window, cx| {
24038 pane.navigate_backward(&Default::default(), window, cx);
24039 });
24040 cx.run_until_parked();
24041 pane.update(cx, |pane, cx| {
24042 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24043 open_editor.update(cx, |editor, cx| {
24044 assert_eq!(
24045 editor.display_text(cx),
24046 "pub mod external {}",
24047 "External file is open now",
24048 );
24049 });
24050 });
24051 assert_language_servers_count(
24052 1,
24053 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24054 cx,
24055 );
24056
24057 cx.update(|_, cx| {
24058 workspace::reload(cx);
24059 });
24060 assert_language_servers_count(
24061 1,
24062 "After reloading the worktree with local and external files opened, only one project should be started",
24063 cx,
24064 );
24065}
24066
24067#[gpui::test]
24068async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24069 init_test(cx, |_| {});
24070
24071 let mut cx = EditorTestContext::new(cx).await;
24072 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24073 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24074
24075 // test cursor move to start of each line on tab
24076 // for `if`, `elif`, `else`, `while`, `with` and `for`
24077 cx.set_state(indoc! {"
24078 def main():
24079 ˇ for item in items:
24080 ˇ while item.active:
24081 ˇ if item.value > 10:
24082 ˇ continue
24083 ˇ elif item.value < 0:
24084 ˇ break
24085 ˇ else:
24086 ˇ with item.context() as ctx:
24087 ˇ yield count
24088 ˇ else:
24089 ˇ log('while else')
24090 ˇ else:
24091 ˇ log('for else')
24092 "});
24093 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24094 cx.assert_editor_state(indoc! {"
24095 def main():
24096 ˇfor item in items:
24097 ˇwhile item.active:
24098 ˇif item.value > 10:
24099 ˇcontinue
24100 ˇelif item.value < 0:
24101 ˇbreak
24102 ˇelse:
24103 ˇwith item.context() as ctx:
24104 ˇyield count
24105 ˇelse:
24106 ˇlog('while else')
24107 ˇelse:
24108 ˇlog('for else')
24109 "});
24110 // test relative indent is preserved when tab
24111 // for `if`, `elif`, `else`, `while`, `with` and `for`
24112 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24113 cx.assert_editor_state(indoc! {"
24114 def main():
24115 ˇfor item in items:
24116 ˇwhile item.active:
24117 ˇif item.value > 10:
24118 ˇcontinue
24119 ˇelif item.value < 0:
24120 ˇbreak
24121 ˇelse:
24122 ˇwith item.context() as ctx:
24123 ˇyield count
24124 ˇelse:
24125 ˇlog('while else')
24126 ˇelse:
24127 ˇlog('for else')
24128 "});
24129
24130 // test cursor move to start of each line on tab
24131 // for `try`, `except`, `else`, `finally`, `match` and `def`
24132 cx.set_state(indoc! {"
24133 def main():
24134 ˇ try:
24135 ˇ fetch()
24136 ˇ except ValueError:
24137 ˇ handle_error()
24138 ˇ else:
24139 ˇ match value:
24140 ˇ case _:
24141 ˇ finally:
24142 ˇ def status():
24143 ˇ return 0
24144 "});
24145 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24146 cx.assert_editor_state(indoc! {"
24147 def main():
24148 ˇtry:
24149 ˇfetch()
24150 ˇexcept ValueError:
24151 ˇhandle_error()
24152 ˇelse:
24153 ˇmatch value:
24154 ˇcase _:
24155 ˇfinally:
24156 ˇdef status():
24157 ˇreturn 0
24158 "});
24159 // test relative indent is preserved when tab
24160 // for `try`, `except`, `else`, `finally`, `match` and `def`
24161 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24162 cx.assert_editor_state(indoc! {"
24163 def main():
24164 ˇtry:
24165 ˇfetch()
24166 ˇexcept ValueError:
24167 ˇhandle_error()
24168 ˇelse:
24169 ˇmatch value:
24170 ˇcase _:
24171 ˇfinally:
24172 ˇdef status():
24173 ˇreturn 0
24174 "});
24175}
24176
24177#[gpui::test]
24178async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24179 init_test(cx, |_| {});
24180
24181 let mut cx = EditorTestContext::new(cx).await;
24182 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24183 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24184
24185 // test `else` auto outdents when typed inside `if` block
24186 cx.set_state(indoc! {"
24187 def main():
24188 if i == 2:
24189 return
24190 ˇ
24191 "});
24192 cx.update_editor(|editor, window, cx| {
24193 editor.handle_input("else:", window, cx);
24194 });
24195 cx.assert_editor_state(indoc! {"
24196 def main():
24197 if i == 2:
24198 return
24199 else:ˇ
24200 "});
24201
24202 // test `except` auto outdents when typed inside `try` block
24203 cx.set_state(indoc! {"
24204 def main():
24205 try:
24206 i = 2
24207 ˇ
24208 "});
24209 cx.update_editor(|editor, window, cx| {
24210 editor.handle_input("except:", window, cx);
24211 });
24212 cx.assert_editor_state(indoc! {"
24213 def main():
24214 try:
24215 i = 2
24216 except:ˇ
24217 "});
24218
24219 // test `else` auto outdents when typed inside `except` block
24220 cx.set_state(indoc! {"
24221 def main():
24222 try:
24223 i = 2
24224 except:
24225 j = 2
24226 ˇ
24227 "});
24228 cx.update_editor(|editor, window, cx| {
24229 editor.handle_input("else:", window, cx);
24230 });
24231 cx.assert_editor_state(indoc! {"
24232 def main():
24233 try:
24234 i = 2
24235 except:
24236 j = 2
24237 else:ˇ
24238 "});
24239
24240 // test `finally` auto outdents when typed inside `else` block
24241 cx.set_state(indoc! {"
24242 def main():
24243 try:
24244 i = 2
24245 except:
24246 j = 2
24247 else:
24248 k = 2
24249 ˇ
24250 "});
24251 cx.update_editor(|editor, window, cx| {
24252 editor.handle_input("finally:", window, cx);
24253 });
24254 cx.assert_editor_state(indoc! {"
24255 def main():
24256 try:
24257 i = 2
24258 except:
24259 j = 2
24260 else:
24261 k = 2
24262 finally:ˇ
24263 "});
24264
24265 // test `else` does not outdents when typed inside `except` block right after for block
24266 cx.set_state(indoc! {"
24267 def main():
24268 try:
24269 i = 2
24270 except:
24271 for i in range(n):
24272 pass
24273 ˇ
24274 "});
24275 cx.update_editor(|editor, window, cx| {
24276 editor.handle_input("else:", window, cx);
24277 });
24278 cx.assert_editor_state(indoc! {"
24279 def main():
24280 try:
24281 i = 2
24282 except:
24283 for i in range(n):
24284 pass
24285 else:ˇ
24286 "});
24287
24288 // test `finally` auto outdents when typed inside `else` block right after for block
24289 cx.set_state(indoc! {"
24290 def main():
24291 try:
24292 i = 2
24293 except:
24294 j = 2
24295 else:
24296 for i in range(n):
24297 pass
24298 ˇ
24299 "});
24300 cx.update_editor(|editor, window, cx| {
24301 editor.handle_input("finally:", window, cx);
24302 });
24303 cx.assert_editor_state(indoc! {"
24304 def main():
24305 try:
24306 i = 2
24307 except:
24308 j = 2
24309 else:
24310 for i in range(n):
24311 pass
24312 finally:ˇ
24313 "});
24314
24315 // test `except` outdents to inner "try" block
24316 cx.set_state(indoc! {"
24317 def main():
24318 try:
24319 i = 2
24320 if i == 2:
24321 try:
24322 i = 3
24323 ˇ
24324 "});
24325 cx.update_editor(|editor, window, cx| {
24326 editor.handle_input("except:", window, cx);
24327 });
24328 cx.assert_editor_state(indoc! {"
24329 def main():
24330 try:
24331 i = 2
24332 if i == 2:
24333 try:
24334 i = 3
24335 except:ˇ
24336 "});
24337
24338 // test `except` outdents to outer "try" block
24339 cx.set_state(indoc! {"
24340 def main():
24341 try:
24342 i = 2
24343 if i == 2:
24344 try:
24345 i = 3
24346 ˇ
24347 "});
24348 cx.update_editor(|editor, window, cx| {
24349 editor.handle_input("except:", window, cx);
24350 });
24351 cx.assert_editor_state(indoc! {"
24352 def main():
24353 try:
24354 i = 2
24355 if i == 2:
24356 try:
24357 i = 3
24358 except:ˇ
24359 "});
24360
24361 // test `else` stays at correct indent when typed after `for` block
24362 cx.set_state(indoc! {"
24363 def main():
24364 for i in range(10):
24365 if i == 3:
24366 break
24367 ˇ
24368 "});
24369 cx.update_editor(|editor, window, cx| {
24370 editor.handle_input("else:", window, cx);
24371 });
24372 cx.assert_editor_state(indoc! {"
24373 def main():
24374 for i in range(10):
24375 if i == 3:
24376 break
24377 else:ˇ
24378 "});
24379
24380 // test does not outdent on typing after line with square brackets
24381 cx.set_state(indoc! {"
24382 def f() -> list[str]:
24383 ˇ
24384 "});
24385 cx.update_editor(|editor, window, cx| {
24386 editor.handle_input("a", window, cx);
24387 });
24388 cx.assert_editor_state(indoc! {"
24389 def f() -> list[str]:
24390 aˇ
24391 "});
24392
24393 // test does not outdent on typing : after case keyword
24394 cx.set_state(indoc! {"
24395 match 1:
24396 caseˇ
24397 "});
24398 cx.update_editor(|editor, window, cx| {
24399 editor.handle_input(":", window, cx);
24400 });
24401 cx.assert_editor_state(indoc! {"
24402 match 1:
24403 case:ˇ
24404 "});
24405}
24406
24407#[gpui::test]
24408async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24409 init_test(cx, |_| {});
24410 update_test_language_settings(cx, |settings| {
24411 settings.defaults.extend_comment_on_newline = Some(false);
24412 });
24413 let mut cx = EditorTestContext::new(cx).await;
24414 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24415 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24416
24417 // test correct indent after newline on comment
24418 cx.set_state(indoc! {"
24419 # COMMENT:ˇ
24420 "});
24421 cx.update_editor(|editor, window, cx| {
24422 editor.newline(&Newline, window, cx);
24423 });
24424 cx.assert_editor_state(indoc! {"
24425 # COMMENT:
24426 ˇ
24427 "});
24428
24429 // test correct indent after newline in brackets
24430 cx.set_state(indoc! {"
24431 {ˇ}
24432 "});
24433 cx.update_editor(|editor, window, cx| {
24434 editor.newline(&Newline, window, cx);
24435 });
24436 cx.run_until_parked();
24437 cx.assert_editor_state(indoc! {"
24438 {
24439 ˇ
24440 }
24441 "});
24442
24443 cx.set_state(indoc! {"
24444 (ˇ)
24445 "});
24446 cx.update_editor(|editor, window, cx| {
24447 editor.newline(&Newline, window, cx);
24448 });
24449 cx.run_until_parked();
24450 cx.assert_editor_state(indoc! {"
24451 (
24452 ˇ
24453 )
24454 "});
24455
24456 // do not indent after empty lists or dictionaries
24457 cx.set_state(indoc! {"
24458 a = []ˇ
24459 "});
24460 cx.update_editor(|editor, window, cx| {
24461 editor.newline(&Newline, window, cx);
24462 });
24463 cx.run_until_parked();
24464 cx.assert_editor_state(indoc! {"
24465 a = []
24466 ˇ
24467 "});
24468}
24469
24470#[gpui::test]
24471async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24472 init_test(cx, |_| {});
24473
24474 let mut cx = EditorTestContext::new(cx).await;
24475 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24476 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24477
24478 // test cursor move to start of each line on tab
24479 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24480 cx.set_state(indoc! {"
24481 function main() {
24482 ˇ for item in $items; do
24483 ˇ while [ -n \"$item\" ]; do
24484 ˇ if [ \"$value\" -gt 10 ]; then
24485 ˇ continue
24486 ˇ elif [ \"$value\" -lt 0 ]; then
24487 ˇ break
24488 ˇ else
24489 ˇ echo \"$item\"
24490 ˇ fi
24491 ˇ done
24492 ˇ done
24493 ˇ}
24494 "});
24495 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24496 cx.assert_editor_state(indoc! {"
24497 function main() {
24498 ˇfor item in $items; do
24499 ˇwhile [ -n \"$item\" ]; do
24500 ˇif [ \"$value\" -gt 10 ]; then
24501 ˇcontinue
24502 ˇelif [ \"$value\" -lt 0 ]; then
24503 ˇbreak
24504 ˇelse
24505 ˇecho \"$item\"
24506 ˇfi
24507 ˇdone
24508 ˇdone
24509 ˇ}
24510 "});
24511 // test relative indent is preserved when tab
24512 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24513 cx.assert_editor_state(indoc! {"
24514 function main() {
24515 ˇfor item in $items; do
24516 ˇwhile [ -n \"$item\" ]; do
24517 ˇif [ \"$value\" -gt 10 ]; then
24518 ˇcontinue
24519 ˇelif [ \"$value\" -lt 0 ]; then
24520 ˇbreak
24521 ˇelse
24522 ˇecho \"$item\"
24523 ˇfi
24524 ˇdone
24525 ˇdone
24526 ˇ}
24527 "});
24528
24529 // test cursor move to start of each line on tab
24530 // for `case` statement with patterns
24531 cx.set_state(indoc! {"
24532 function handle() {
24533 ˇ case \"$1\" in
24534 ˇ start)
24535 ˇ echo \"a\"
24536 ˇ ;;
24537 ˇ stop)
24538 ˇ echo \"b\"
24539 ˇ ;;
24540 ˇ *)
24541 ˇ echo \"c\"
24542 ˇ ;;
24543 ˇ esac
24544 ˇ}
24545 "});
24546 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24547 cx.assert_editor_state(indoc! {"
24548 function handle() {
24549 ˇcase \"$1\" in
24550 ˇstart)
24551 ˇecho \"a\"
24552 ˇ;;
24553 ˇstop)
24554 ˇecho \"b\"
24555 ˇ;;
24556 ˇ*)
24557 ˇecho \"c\"
24558 ˇ;;
24559 ˇesac
24560 ˇ}
24561 "});
24562}
24563
24564#[gpui::test]
24565async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24566 init_test(cx, |_| {});
24567
24568 let mut cx = EditorTestContext::new(cx).await;
24569 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24570 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24571
24572 // test indents on comment insert
24573 cx.set_state(indoc! {"
24574 function main() {
24575 ˇ for item in $items; do
24576 ˇ while [ -n \"$item\" ]; do
24577 ˇ if [ \"$value\" -gt 10 ]; then
24578 ˇ continue
24579 ˇ elif [ \"$value\" -lt 0 ]; then
24580 ˇ break
24581 ˇ else
24582 ˇ echo \"$item\"
24583 ˇ fi
24584 ˇ done
24585 ˇ done
24586 ˇ}
24587 "});
24588 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24589 cx.assert_editor_state(indoc! {"
24590 function main() {
24591 #ˇ for item in $items; do
24592 #ˇ while [ -n \"$item\" ]; do
24593 #ˇ if [ \"$value\" -gt 10 ]; then
24594 #ˇ continue
24595 #ˇ elif [ \"$value\" -lt 0 ]; then
24596 #ˇ break
24597 #ˇ else
24598 #ˇ echo \"$item\"
24599 #ˇ fi
24600 #ˇ done
24601 #ˇ done
24602 #ˇ}
24603 "});
24604}
24605
24606#[gpui::test]
24607async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24608 init_test(cx, |_| {});
24609
24610 let mut cx = EditorTestContext::new(cx).await;
24611 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24612 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24613
24614 // test `else` auto outdents when typed inside `if` block
24615 cx.set_state(indoc! {"
24616 if [ \"$1\" = \"test\" ]; then
24617 echo \"foo bar\"
24618 ˇ
24619 "});
24620 cx.update_editor(|editor, window, cx| {
24621 editor.handle_input("else", window, cx);
24622 });
24623 cx.assert_editor_state(indoc! {"
24624 if [ \"$1\" = \"test\" ]; then
24625 echo \"foo bar\"
24626 elseˇ
24627 "});
24628
24629 // test `elif` auto outdents when typed inside `if` block
24630 cx.set_state(indoc! {"
24631 if [ \"$1\" = \"test\" ]; then
24632 echo \"foo bar\"
24633 ˇ
24634 "});
24635 cx.update_editor(|editor, window, cx| {
24636 editor.handle_input("elif", window, cx);
24637 });
24638 cx.assert_editor_state(indoc! {"
24639 if [ \"$1\" = \"test\" ]; then
24640 echo \"foo bar\"
24641 elifˇ
24642 "});
24643
24644 // test `fi` auto outdents when typed inside `else` block
24645 cx.set_state(indoc! {"
24646 if [ \"$1\" = \"test\" ]; then
24647 echo \"foo bar\"
24648 else
24649 echo \"bar baz\"
24650 ˇ
24651 "});
24652 cx.update_editor(|editor, window, cx| {
24653 editor.handle_input("fi", window, cx);
24654 });
24655 cx.assert_editor_state(indoc! {"
24656 if [ \"$1\" = \"test\" ]; then
24657 echo \"foo bar\"
24658 else
24659 echo \"bar baz\"
24660 fiˇ
24661 "});
24662
24663 // test `done` auto outdents when typed inside `while` block
24664 cx.set_state(indoc! {"
24665 while read line; do
24666 echo \"$line\"
24667 ˇ
24668 "});
24669 cx.update_editor(|editor, window, cx| {
24670 editor.handle_input("done", window, cx);
24671 });
24672 cx.assert_editor_state(indoc! {"
24673 while read line; do
24674 echo \"$line\"
24675 doneˇ
24676 "});
24677
24678 // test `done` auto outdents when typed inside `for` block
24679 cx.set_state(indoc! {"
24680 for file in *.txt; do
24681 cat \"$file\"
24682 ˇ
24683 "});
24684 cx.update_editor(|editor, window, cx| {
24685 editor.handle_input("done", window, cx);
24686 });
24687 cx.assert_editor_state(indoc! {"
24688 for file in *.txt; do
24689 cat \"$file\"
24690 doneˇ
24691 "});
24692
24693 // test `esac` auto outdents when typed inside `case` block
24694 cx.set_state(indoc! {"
24695 case \"$1\" in
24696 start)
24697 echo \"foo bar\"
24698 ;;
24699 stop)
24700 echo \"bar baz\"
24701 ;;
24702 ˇ
24703 "});
24704 cx.update_editor(|editor, window, cx| {
24705 editor.handle_input("esac", window, cx);
24706 });
24707 cx.assert_editor_state(indoc! {"
24708 case \"$1\" in
24709 start)
24710 echo \"foo bar\"
24711 ;;
24712 stop)
24713 echo \"bar baz\"
24714 ;;
24715 esacˇ
24716 "});
24717
24718 // test `*)` auto outdents when typed inside `case` block
24719 cx.set_state(indoc! {"
24720 case \"$1\" in
24721 start)
24722 echo \"foo bar\"
24723 ;;
24724 ˇ
24725 "});
24726 cx.update_editor(|editor, window, cx| {
24727 editor.handle_input("*)", window, cx);
24728 });
24729 cx.assert_editor_state(indoc! {"
24730 case \"$1\" in
24731 start)
24732 echo \"foo bar\"
24733 ;;
24734 *)ˇ
24735 "});
24736
24737 // test `fi` outdents to correct level with nested if blocks
24738 cx.set_state(indoc! {"
24739 if [ \"$1\" = \"test\" ]; then
24740 echo \"outer if\"
24741 if [ \"$2\" = \"debug\" ]; then
24742 echo \"inner if\"
24743 ˇ
24744 "});
24745 cx.update_editor(|editor, window, cx| {
24746 editor.handle_input("fi", window, cx);
24747 });
24748 cx.assert_editor_state(indoc! {"
24749 if [ \"$1\" = \"test\" ]; then
24750 echo \"outer if\"
24751 if [ \"$2\" = \"debug\" ]; then
24752 echo \"inner if\"
24753 fiˇ
24754 "});
24755}
24756
24757#[gpui::test]
24758async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24759 init_test(cx, |_| {});
24760 update_test_language_settings(cx, |settings| {
24761 settings.defaults.extend_comment_on_newline = Some(false);
24762 });
24763 let mut cx = EditorTestContext::new(cx).await;
24764 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24765 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24766
24767 // test correct indent after newline on comment
24768 cx.set_state(indoc! {"
24769 # COMMENT:ˇ
24770 "});
24771 cx.update_editor(|editor, window, cx| {
24772 editor.newline(&Newline, window, cx);
24773 });
24774 cx.assert_editor_state(indoc! {"
24775 # COMMENT:
24776 ˇ
24777 "});
24778
24779 // test correct indent after newline after `then`
24780 cx.set_state(indoc! {"
24781
24782 if [ \"$1\" = \"test\" ]; thenˇ
24783 "});
24784 cx.update_editor(|editor, window, cx| {
24785 editor.newline(&Newline, window, cx);
24786 });
24787 cx.run_until_parked();
24788 cx.assert_editor_state(indoc! {"
24789
24790 if [ \"$1\" = \"test\" ]; then
24791 ˇ
24792 "});
24793
24794 // test correct indent after newline after `else`
24795 cx.set_state(indoc! {"
24796 if [ \"$1\" = \"test\" ]; then
24797 elseˇ
24798 "});
24799 cx.update_editor(|editor, window, cx| {
24800 editor.newline(&Newline, window, cx);
24801 });
24802 cx.run_until_parked();
24803 cx.assert_editor_state(indoc! {"
24804 if [ \"$1\" = \"test\" ]; then
24805 else
24806 ˇ
24807 "});
24808
24809 // test correct indent after newline after `elif`
24810 cx.set_state(indoc! {"
24811 if [ \"$1\" = \"test\" ]; then
24812 elifˇ
24813 "});
24814 cx.update_editor(|editor, window, cx| {
24815 editor.newline(&Newline, window, cx);
24816 });
24817 cx.run_until_parked();
24818 cx.assert_editor_state(indoc! {"
24819 if [ \"$1\" = \"test\" ]; then
24820 elif
24821 ˇ
24822 "});
24823
24824 // test correct indent after newline after `do`
24825 cx.set_state(indoc! {"
24826 for file in *.txt; doˇ
24827 "});
24828 cx.update_editor(|editor, window, cx| {
24829 editor.newline(&Newline, window, cx);
24830 });
24831 cx.run_until_parked();
24832 cx.assert_editor_state(indoc! {"
24833 for file in *.txt; do
24834 ˇ
24835 "});
24836
24837 // test correct indent after newline after case pattern
24838 cx.set_state(indoc! {"
24839 case \"$1\" in
24840 start)ˇ
24841 "});
24842 cx.update_editor(|editor, window, cx| {
24843 editor.newline(&Newline, window, cx);
24844 });
24845 cx.run_until_parked();
24846 cx.assert_editor_state(indoc! {"
24847 case \"$1\" in
24848 start)
24849 ˇ
24850 "});
24851
24852 // test correct indent after newline after case pattern
24853 cx.set_state(indoc! {"
24854 case \"$1\" in
24855 start)
24856 ;;
24857 *)ˇ
24858 "});
24859 cx.update_editor(|editor, window, cx| {
24860 editor.newline(&Newline, window, cx);
24861 });
24862 cx.run_until_parked();
24863 cx.assert_editor_state(indoc! {"
24864 case \"$1\" in
24865 start)
24866 ;;
24867 *)
24868 ˇ
24869 "});
24870
24871 // test correct indent after newline after function opening brace
24872 cx.set_state(indoc! {"
24873 function test() {ˇ}
24874 "});
24875 cx.update_editor(|editor, window, cx| {
24876 editor.newline(&Newline, window, cx);
24877 });
24878 cx.run_until_parked();
24879 cx.assert_editor_state(indoc! {"
24880 function test() {
24881 ˇ
24882 }
24883 "});
24884
24885 // test no extra indent after semicolon on same line
24886 cx.set_state(indoc! {"
24887 echo \"test\";ˇ
24888 "});
24889 cx.update_editor(|editor, window, cx| {
24890 editor.newline(&Newline, window, cx);
24891 });
24892 cx.run_until_parked();
24893 cx.assert_editor_state(indoc! {"
24894 echo \"test\";
24895 ˇ
24896 "});
24897}
24898
24899fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
24900 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
24901 point..point
24902}
24903
24904#[track_caller]
24905fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
24906 let (text, ranges) = marked_text_ranges(marked_text, true);
24907 assert_eq!(editor.text(cx), text);
24908 assert_eq!(
24909 editor.selections.ranges(cx),
24910 ranges,
24911 "Assert selections are {}",
24912 marked_text
24913 );
24914}
24915
24916pub fn handle_signature_help_request(
24917 cx: &mut EditorLspTestContext,
24918 mocked_response: lsp::SignatureHelp,
24919) -> impl Future<Output = ()> + use<> {
24920 let mut request =
24921 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
24922 let mocked_response = mocked_response.clone();
24923 async move { Ok(Some(mocked_response)) }
24924 });
24925
24926 async move {
24927 request.next().await;
24928 }
24929}
24930
24931#[track_caller]
24932pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
24933 cx.update_editor(|editor, _, _| {
24934 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
24935 let entries = menu.entries.borrow();
24936 let entries = entries
24937 .iter()
24938 .map(|entry| entry.string.as_str())
24939 .collect::<Vec<_>>();
24940 assert_eq!(entries, expected);
24941 } else {
24942 panic!("Expected completions menu");
24943 }
24944 });
24945}
24946
24947/// Handle completion request passing a marked string specifying where the completion
24948/// should be triggered from using '|' character, what range should be replaced, and what completions
24949/// should be returned using '<' and '>' to delimit the range.
24950///
24951/// Also see `handle_completion_request_with_insert_and_replace`.
24952#[track_caller]
24953pub fn handle_completion_request(
24954 marked_string: &str,
24955 completions: Vec<&'static str>,
24956 is_incomplete: bool,
24957 counter: Arc<AtomicUsize>,
24958 cx: &mut EditorLspTestContext,
24959) -> impl Future<Output = ()> {
24960 let complete_from_marker: TextRangeMarker = '|'.into();
24961 let replace_range_marker: TextRangeMarker = ('<', '>').into();
24962 let (_, mut marked_ranges) = marked_text_ranges_by(
24963 marked_string,
24964 vec![complete_from_marker.clone(), replace_range_marker.clone()],
24965 );
24966
24967 let complete_from_position =
24968 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
24969 let replace_range =
24970 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
24971
24972 let mut request =
24973 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
24974 let completions = completions.clone();
24975 counter.fetch_add(1, atomic::Ordering::Release);
24976 async move {
24977 assert_eq!(params.text_document_position.text_document.uri, url.clone());
24978 assert_eq!(
24979 params.text_document_position.position,
24980 complete_from_position
24981 );
24982 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
24983 is_incomplete,
24984 item_defaults: None,
24985 items: completions
24986 .iter()
24987 .map(|completion_text| lsp::CompletionItem {
24988 label: completion_text.to_string(),
24989 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
24990 range: replace_range,
24991 new_text: completion_text.to_string(),
24992 })),
24993 ..Default::default()
24994 })
24995 .collect(),
24996 })))
24997 }
24998 });
24999
25000 async move {
25001 request.next().await;
25002 }
25003}
25004
25005/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25006/// given instead, which also contains an `insert` range.
25007///
25008/// This function uses markers to define ranges:
25009/// - `|` marks the cursor position
25010/// - `<>` marks the replace range
25011/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25012pub fn handle_completion_request_with_insert_and_replace(
25013 cx: &mut EditorLspTestContext,
25014 marked_string: &str,
25015 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25016 counter: Arc<AtomicUsize>,
25017) -> impl Future<Output = ()> {
25018 let complete_from_marker: TextRangeMarker = '|'.into();
25019 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25020 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25021
25022 let (_, mut marked_ranges) = marked_text_ranges_by(
25023 marked_string,
25024 vec![
25025 complete_from_marker.clone(),
25026 replace_range_marker.clone(),
25027 insert_range_marker.clone(),
25028 ],
25029 );
25030
25031 let complete_from_position =
25032 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25033 let replace_range =
25034 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25035
25036 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25037 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25038 _ => lsp::Range {
25039 start: replace_range.start,
25040 end: complete_from_position,
25041 },
25042 };
25043
25044 let mut request =
25045 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25046 let completions = completions.clone();
25047 counter.fetch_add(1, atomic::Ordering::Release);
25048 async move {
25049 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25050 assert_eq!(
25051 params.text_document_position.position, complete_from_position,
25052 "marker `|` position doesn't match",
25053 );
25054 Ok(Some(lsp::CompletionResponse::Array(
25055 completions
25056 .iter()
25057 .map(|(label, new_text)| lsp::CompletionItem {
25058 label: label.to_string(),
25059 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25060 lsp::InsertReplaceEdit {
25061 insert: insert_range,
25062 replace: replace_range,
25063 new_text: new_text.to_string(),
25064 },
25065 )),
25066 ..Default::default()
25067 })
25068 .collect(),
25069 )))
25070 }
25071 });
25072
25073 async move {
25074 request.next().await;
25075 }
25076}
25077
25078fn handle_resolve_completion_request(
25079 cx: &mut EditorLspTestContext,
25080 edits: Option<Vec<(&'static str, &'static str)>>,
25081) -> impl Future<Output = ()> {
25082 let edits = edits.map(|edits| {
25083 edits
25084 .iter()
25085 .map(|(marked_string, new_text)| {
25086 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25087 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25088 lsp::TextEdit::new(replace_range, new_text.to_string())
25089 })
25090 .collect::<Vec<_>>()
25091 });
25092
25093 let mut request =
25094 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25095 let edits = edits.clone();
25096 async move {
25097 Ok(lsp::CompletionItem {
25098 additional_text_edits: edits,
25099 ..Default::default()
25100 })
25101 }
25102 });
25103
25104 async move {
25105 request.next().await;
25106 }
25107}
25108
25109pub(crate) fn update_test_language_settings(
25110 cx: &mut TestAppContext,
25111 f: impl Fn(&mut AllLanguageSettingsContent),
25112) {
25113 cx.update(|cx| {
25114 SettingsStore::update_global(cx, |store, cx| {
25115 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25116 });
25117 });
25118}
25119
25120pub(crate) fn update_test_project_settings(
25121 cx: &mut TestAppContext,
25122 f: impl Fn(&mut ProjectSettingsContent),
25123) {
25124 cx.update(|cx| {
25125 SettingsStore::update_global(cx, |store, cx| {
25126 store.update_user_settings(cx, |settings| f(&mut settings.project));
25127 });
25128 });
25129}
25130
25131pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25132 cx.update(|cx| {
25133 assets::Assets.load_test_fonts(cx);
25134 let store = SettingsStore::test(cx);
25135 cx.set_global(store);
25136 theme::init(theme::LoadThemes::JustBase, cx);
25137 release_channel::init(SemanticVersion::default(), cx);
25138 client::init_settings(cx);
25139 language::init(cx);
25140 Project::init_settings(cx);
25141 workspace::init_settings(cx);
25142 crate::init(cx);
25143 });
25144 zlog::init_test();
25145 update_test_language_settings(cx, f);
25146}
25147
25148#[track_caller]
25149fn assert_hunk_revert(
25150 not_reverted_text_with_selections: &str,
25151 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25152 expected_reverted_text_with_selections: &str,
25153 base_text: &str,
25154 cx: &mut EditorLspTestContext,
25155) {
25156 cx.set_state(not_reverted_text_with_selections);
25157 cx.set_head_text(base_text);
25158 cx.executor().run_until_parked();
25159
25160 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25161 let snapshot = editor.snapshot(window, cx);
25162 let reverted_hunk_statuses = snapshot
25163 .buffer_snapshot
25164 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
25165 .map(|hunk| hunk.status().kind)
25166 .collect::<Vec<_>>();
25167
25168 editor.git_restore(&Default::default(), window, cx);
25169 reverted_hunk_statuses
25170 });
25171 cx.executor().run_until_parked();
25172 cx.assert_editor_state(expected_reverted_text_with_selections);
25173 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25174}
25175
25176#[gpui::test(iterations = 10)]
25177async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25178 init_test(cx, |_| {});
25179
25180 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25181 let counter = diagnostic_requests.clone();
25182
25183 let fs = FakeFs::new(cx.executor());
25184 fs.insert_tree(
25185 path!("/a"),
25186 json!({
25187 "first.rs": "fn main() { let a = 5; }",
25188 "second.rs": "// Test file",
25189 }),
25190 )
25191 .await;
25192
25193 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25194 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25195 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25196
25197 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25198 language_registry.add(rust_lang());
25199 let mut fake_servers = language_registry.register_fake_lsp(
25200 "Rust",
25201 FakeLspAdapter {
25202 capabilities: lsp::ServerCapabilities {
25203 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25204 lsp::DiagnosticOptions {
25205 identifier: None,
25206 inter_file_dependencies: true,
25207 workspace_diagnostics: true,
25208 work_done_progress_options: Default::default(),
25209 },
25210 )),
25211 ..Default::default()
25212 },
25213 ..Default::default()
25214 },
25215 );
25216
25217 let editor = workspace
25218 .update(cx, |workspace, window, cx| {
25219 workspace.open_abs_path(
25220 PathBuf::from(path!("/a/first.rs")),
25221 OpenOptions::default(),
25222 window,
25223 cx,
25224 )
25225 })
25226 .unwrap()
25227 .await
25228 .unwrap()
25229 .downcast::<Editor>()
25230 .unwrap();
25231 let fake_server = fake_servers.next().await.unwrap();
25232 let server_id = fake_server.server.server_id();
25233 let mut first_request = fake_server
25234 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25235 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25236 let result_id = Some(new_result_id.to_string());
25237 assert_eq!(
25238 params.text_document.uri,
25239 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25240 );
25241 async move {
25242 Ok(lsp::DocumentDiagnosticReportResult::Report(
25243 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25244 related_documents: None,
25245 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25246 items: Vec::new(),
25247 result_id,
25248 },
25249 }),
25250 ))
25251 }
25252 });
25253
25254 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25255 project.update(cx, |project, cx| {
25256 let buffer_id = editor
25257 .read(cx)
25258 .buffer()
25259 .read(cx)
25260 .as_singleton()
25261 .expect("created a singleton buffer")
25262 .read(cx)
25263 .remote_id();
25264 let buffer_result_id = project
25265 .lsp_store()
25266 .read(cx)
25267 .result_id(server_id, buffer_id, cx);
25268 assert_eq!(expected, buffer_result_id);
25269 });
25270 };
25271
25272 ensure_result_id(None, cx);
25273 cx.executor().advance_clock(Duration::from_millis(60));
25274 cx.executor().run_until_parked();
25275 assert_eq!(
25276 diagnostic_requests.load(atomic::Ordering::Acquire),
25277 1,
25278 "Opening file should trigger diagnostic request"
25279 );
25280 first_request
25281 .next()
25282 .await
25283 .expect("should have sent the first diagnostics pull request");
25284 ensure_result_id(Some("1".to_string()), cx);
25285
25286 // Editing should trigger diagnostics
25287 editor.update_in(cx, |editor, window, cx| {
25288 editor.handle_input("2", window, cx)
25289 });
25290 cx.executor().advance_clock(Duration::from_millis(60));
25291 cx.executor().run_until_parked();
25292 assert_eq!(
25293 diagnostic_requests.load(atomic::Ordering::Acquire),
25294 2,
25295 "Editing should trigger diagnostic request"
25296 );
25297 ensure_result_id(Some("2".to_string()), cx);
25298
25299 // Moving cursor should not trigger diagnostic request
25300 editor.update_in(cx, |editor, window, cx| {
25301 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25302 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25303 });
25304 });
25305 cx.executor().advance_clock(Duration::from_millis(60));
25306 cx.executor().run_until_parked();
25307 assert_eq!(
25308 diagnostic_requests.load(atomic::Ordering::Acquire),
25309 2,
25310 "Cursor movement should not trigger diagnostic request"
25311 );
25312 ensure_result_id(Some("2".to_string()), cx);
25313 // Multiple rapid edits should be debounced
25314 for _ in 0..5 {
25315 editor.update_in(cx, |editor, window, cx| {
25316 editor.handle_input("x", window, cx)
25317 });
25318 }
25319 cx.executor().advance_clock(Duration::from_millis(60));
25320 cx.executor().run_until_parked();
25321
25322 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25323 assert!(
25324 final_requests <= 4,
25325 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25326 );
25327 ensure_result_id(Some(final_requests.to_string()), cx);
25328}
25329
25330#[gpui::test]
25331async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25332 // Regression test for issue #11671
25333 // Previously, adding a cursor after moving multiple cursors would reset
25334 // the cursor count instead of adding to the existing cursors.
25335 init_test(cx, |_| {});
25336 let mut cx = EditorTestContext::new(cx).await;
25337
25338 // Create a simple buffer with cursor at start
25339 cx.set_state(indoc! {"
25340 ˇaaaa
25341 bbbb
25342 cccc
25343 dddd
25344 eeee
25345 ffff
25346 gggg
25347 hhhh"});
25348
25349 // Add 2 cursors below (so we have 3 total)
25350 cx.update_editor(|editor, window, cx| {
25351 editor.add_selection_below(&Default::default(), window, cx);
25352 editor.add_selection_below(&Default::default(), window, cx);
25353 });
25354
25355 // Verify we have 3 cursors
25356 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25357 assert_eq!(
25358 initial_count, 3,
25359 "Should have 3 cursors after adding 2 below"
25360 );
25361
25362 // Move down one line
25363 cx.update_editor(|editor, window, cx| {
25364 editor.move_down(&MoveDown, window, cx);
25365 });
25366
25367 // Add another cursor below
25368 cx.update_editor(|editor, window, cx| {
25369 editor.add_selection_below(&Default::default(), window, cx);
25370 });
25371
25372 // Should now have 4 cursors (3 original + 1 new)
25373 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25374 assert_eq!(
25375 final_count, 4,
25376 "Should have 4 cursors after moving and adding another"
25377 );
25378}
25379
25380#[gpui::test(iterations = 10)]
25381async fn test_document_colors(cx: &mut TestAppContext) {
25382 let expected_color = Rgba {
25383 r: 0.33,
25384 g: 0.33,
25385 b: 0.33,
25386 a: 0.33,
25387 };
25388
25389 init_test(cx, |_| {});
25390
25391 let fs = FakeFs::new(cx.executor());
25392 fs.insert_tree(
25393 path!("/a"),
25394 json!({
25395 "first.rs": "fn main() { let a = 5; }",
25396 }),
25397 )
25398 .await;
25399
25400 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25401 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25402 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25403
25404 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25405 language_registry.add(rust_lang());
25406 let mut fake_servers = language_registry.register_fake_lsp(
25407 "Rust",
25408 FakeLspAdapter {
25409 capabilities: lsp::ServerCapabilities {
25410 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25411 ..lsp::ServerCapabilities::default()
25412 },
25413 name: "rust-analyzer",
25414 ..FakeLspAdapter::default()
25415 },
25416 );
25417 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25418 "Rust",
25419 FakeLspAdapter {
25420 capabilities: lsp::ServerCapabilities {
25421 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25422 ..lsp::ServerCapabilities::default()
25423 },
25424 name: "not-rust-analyzer",
25425 ..FakeLspAdapter::default()
25426 },
25427 );
25428
25429 let editor = workspace
25430 .update(cx, |workspace, window, cx| {
25431 workspace.open_abs_path(
25432 PathBuf::from(path!("/a/first.rs")),
25433 OpenOptions::default(),
25434 window,
25435 cx,
25436 )
25437 })
25438 .unwrap()
25439 .await
25440 .unwrap()
25441 .downcast::<Editor>()
25442 .unwrap();
25443 let fake_language_server = fake_servers.next().await.unwrap();
25444 let fake_language_server_without_capabilities =
25445 fake_servers_without_capabilities.next().await.unwrap();
25446 let requests_made = Arc::new(AtomicUsize::new(0));
25447 let closure_requests_made = Arc::clone(&requests_made);
25448 let mut color_request_handle = fake_language_server
25449 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25450 let requests_made = Arc::clone(&closure_requests_made);
25451 async move {
25452 assert_eq!(
25453 params.text_document.uri,
25454 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25455 );
25456 requests_made.fetch_add(1, atomic::Ordering::Release);
25457 Ok(vec![
25458 lsp::ColorInformation {
25459 range: lsp::Range {
25460 start: lsp::Position {
25461 line: 0,
25462 character: 0,
25463 },
25464 end: lsp::Position {
25465 line: 0,
25466 character: 1,
25467 },
25468 },
25469 color: lsp::Color {
25470 red: 0.33,
25471 green: 0.33,
25472 blue: 0.33,
25473 alpha: 0.33,
25474 },
25475 },
25476 lsp::ColorInformation {
25477 range: lsp::Range {
25478 start: lsp::Position {
25479 line: 0,
25480 character: 0,
25481 },
25482 end: lsp::Position {
25483 line: 0,
25484 character: 1,
25485 },
25486 },
25487 color: lsp::Color {
25488 red: 0.33,
25489 green: 0.33,
25490 blue: 0.33,
25491 alpha: 0.33,
25492 },
25493 },
25494 ])
25495 }
25496 });
25497
25498 let _handle = fake_language_server_without_capabilities
25499 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25500 panic!("Should not be called");
25501 });
25502 cx.executor().advance_clock(Duration::from_millis(100));
25503 color_request_handle.next().await.unwrap();
25504 cx.run_until_parked();
25505 assert_eq!(
25506 1,
25507 requests_made.load(atomic::Ordering::Acquire),
25508 "Should query for colors once per editor open"
25509 );
25510 editor.update_in(cx, |editor, _, cx| {
25511 assert_eq!(
25512 vec![expected_color],
25513 extract_color_inlays(editor, cx),
25514 "Should have an initial inlay"
25515 );
25516 });
25517
25518 // opening another file in a split should not influence the LSP query counter
25519 workspace
25520 .update(cx, |workspace, window, cx| {
25521 assert_eq!(
25522 workspace.panes().len(),
25523 1,
25524 "Should have one pane with one editor"
25525 );
25526 workspace.move_item_to_pane_in_direction(
25527 &MoveItemToPaneInDirection {
25528 direction: SplitDirection::Right,
25529 focus: false,
25530 clone: true,
25531 },
25532 window,
25533 cx,
25534 );
25535 })
25536 .unwrap();
25537 cx.run_until_parked();
25538 workspace
25539 .update(cx, |workspace, _, cx| {
25540 let panes = workspace.panes();
25541 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25542 for pane in panes {
25543 let editor = pane
25544 .read(cx)
25545 .active_item()
25546 .and_then(|item| item.downcast::<Editor>())
25547 .expect("Should have opened an editor in each split");
25548 let editor_file = editor
25549 .read(cx)
25550 .buffer()
25551 .read(cx)
25552 .as_singleton()
25553 .expect("test deals with singleton buffers")
25554 .read(cx)
25555 .file()
25556 .expect("test buffese should have a file")
25557 .path();
25558 assert_eq!(
25559 editor_file.as_ref(),
25560 Path::new("first.rs"),
25561 "Both editors should be opened for the same file"
25562 )
25563 }
25564 })
25565 .unwrap();
25566
25567 cx.executor().advance_clock(Duration::from_millis(500));
25568 let save = editor.update_in(cx, |editor, window, cx| {
25569 editor.move_to_end(&MoveToEnd, window, cx);
25570 editor.handle_input("dirty", window, cx);
25571 editor.save(
25572 SaveOptions {
25573 format: true,
25574 autosave: true,
25575 },
25576 project.clone(),
25577 window,
25578 cx,
25579 )
25580 });
25581 save.await.unwrap();
25582
25583 color_request_handle.next().await.unwrap();
25584 cx.run_until_parked();
25585 assert_eq!(
25586 3,
25587 requests_made.load(atomic::Ordering::Acquire),
25588 "Should query for colors once per save and once per formatting after save"
25589 );
25590
25591 drop(editor);
25592 let close = workspace
25593 .update(cx, |workspace, window, cx| {
25594 workspace.active_pane().update(cx, |pane, cx| {
25595 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25596 })
25597 })
25598 .unwrap();
25599 close.await.unwrap();
25600 let close = workspace
25601 .update(cx, |workspace, window, cx| {
25602 workspace.active_pane().update(cx, |pane, cx| {
25603 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25604 })
25605 })
25606 .unwrap();
25607 close.await.unwrap();
25608 assert_eq!(
25609 3,
25610 requests_made.load(atomic::Ordering::Acquire),
25611 "After saving and closing all editors, no extra requests should be made"
25612 );
25613 workspace
25614 .update(cx, |workspace, _, cx| {
25615 assert!(
25616 workspace.active_item(cx).is_none(),
25617 "Should close all editors"
25618 )
25619 })
25620 .unwrap();
25621
25622 workspace
25623 .update(cx, |workspace, window, cx| {
25624 workspace.active_pane().update(cx, |pane, cx| {
25625 pane.navigate_backward(&Default::default(), window, cx);
25626 })
25627 })
25628 .unwrap();
25629 cx.executor().advance_clock(Duration::from_millis(100));
25630 cx.run_until_parked();
25631 let editor = workspace
25632 .update(cx, |workspace, _, cx| {
25633 workspace
25634 .active_item(cx)
25635 .expect("Should have reopened the editor again after navigating back")
25636 .downcast::<Editor>()
25637 .expect("Should be an editor")
25638 })
25639 .unwrap();
25640 color_request_handle.next().await.unwrap();
25641 assert_eq!(
25642 3,
25643 requests_made.load(atomic::Ordering::Acquire),
25644 "Cache should be reused on buffer close and reopen"
25645 );
25646 editor.update(cx, |editor, cx| {
25647 assert_eq!(
25648 vec![expected_color],
25649 extract_color_inlays(editor, cx),
25650 "Should have an initial inlay"
25651 );
25652 });
25653}
25654
25655#[gpui::test]
25656async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25657 init_test(cx, |_| {});
25658 let (editor, cx) = cx.add_window_view(Editor::single_line);
25659 editor.update_in(cx, |editor, window, cx| {
25660 editor.set_text("oops\n\nwow\n", window, cx)
25661 });
25662 cx.run_until_parked();
25663 editor.update(cx, |editor, cx| {
25664 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25665 });
25666 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25667 cx.run_until_parked();
25668 editor.update(cx, |editor, cx| {
25669 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25670 });
25671}
25672
25673#[gpui::test]
25674async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25675 init_test(cx, |_| {});
25676
25677 cx.update(|cx| {
25678 register_project_item::<Editor>(cx);
25679 });
25680
25681 let fs = FakeFs::new(cx.executor());
25682 fs.insert_tree("/root1", json!({})).await;
25683 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25684 .await;
25685
25686 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25687 let (workspace, cx) =
25688 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25689
25690 let worktree_id = project.update(cx, |project, cx| {
25691 project.worktrees(cx).next().unwrap().read(cx).id()
25692 });
25693
25694 let handle = workspace
25695 .update_in(cx, |workspace, window, cx| {
25696 let project_path = (worktree_id, "one.pdf");
25697 workspace.open_path(project_path, None, true, window, cx)
25698 })
25699 .await
25700 .unwrap();
25701
25702 assert_eq!(
25703 handle.to_any().entity_type(),
25704 TypeId::of::<InvalidBufferView>()
25705 );
25706}
25707
25708#[gpui::test]
25709async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25710 init_test(cx, |_| {});
25711
25712 let language = Arc::new(Language::new(
25713 LanguageConfig::default(),
25714 Some(tree_sitter_rust::LANGUAGE.into()),
25715 ));
25716
25717 // Test hierarchical sibling navigation
25718 let text = r#"
25719 fn outer() {
25720 if condition {
25721 let a = 1;
25722 }
25723 let b = 2;
25724 }
25725
25726 fn another() {
25727 let c = 3;
25728 }
25729 "#;
25730
25731 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25732 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25733 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25734
25735 // Wait for parsing to complete
25736 editor
25737 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25738 .await;
25739
25740 editor.update_in(cx, |editor, window, cx| {
25741 // Start by selecting "let a = 1;" inside the if block
25742 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25743 s.select_display_ranges([
25744 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25745 ]);
25746 });
25747
25748 let initial_selection = editor.selections.display_ranges(cx);
25749 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25750
25751 // Test select next sibling - should move up levels to find the next sibling
25752 // Since "let a = 1;" has no siblings in the if block, it should move up
25753 // to find "let b = 2;" which is a sibling of the if block
25754 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25755 let next_selection = editor.selections.display_ranges(cx);
25756
25757 // Should have a selection and it should be different from the initial
25758 assert_eq!(
25759 next_selection.len(),
25760 1,
25761 "Should have one selection after next"
25762 );
25763 assert_ne!(
25764 next_selection[0], initial_selection[0],
25765 "Next sibling selection should be different"
25766 );
25767
25768 // Test hierarchical navigation by going to the end of the current function
25769 // and trying to navigate to the next function
25770 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25771 s.select_display_ranges([
25772 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25773 ]);
25774 });
25775
25776 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25777 let function_next_selection = editor.selections.display_ranges(cx);
25778
25779 // Should move to the next function
25780 assert_eq!(
25781 function_next_selection.len(),
25782 1,
25783 "Should have one selection after function next"
25784 );
25785
25786 // Test select previous sibling navigation
25787 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25788 let prev_selection = editor.selections.display_ranges(cx);
25789
25790 // Should have a selection and it should be different
25791 assert_eq!(
25792 prev_selection.len(),
25793 1,
25794 "Should have one selection after prev"
25795 );
25796 assert_ne!(
25797 prev_selection[0], function_next_selection[0],
25798 "Previous sibling selection should be different from next"
25799 );
25800 });
25801}
25802
25803#[gpui::test]
25804async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
25805 init_test(cx, |_| {});
25806
25807 let mut cx = EditorTestContext::new(cx).await;
25808 cx.set_state(
25809 "let ˇvariable = 42;
25810let another = variable + 1;
25811let result = variable * 2;",
25812 );
25813
25814 // Set up document highlights manually (simulating LSP response)
25815 cx.update_editor(|editor, _window, cx| {
25816 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
25817
25818 // Create highlights for "variable" occurrences
25819 let highlight_ranges = [
25820 Point::new(0, 4)..Point::new(0, 12), // First "variable"
25821 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
25822 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
25823 ];
25824
25825 let anchor_ranges: Vec<_> = highlight_ranges
25826 .iter()
25827 .map(|range| range.clone().to_anchors(&buffer_snapshot))
25828 .collect();
25829
25830 editor.highlight_background::<DocumentHighlightRead>(
25831 &anchor_ranges,
25832 |theme| theme.colors().editor_document_highlight_read_background,
25833 cx,
25834 );
25835 });
25836
25837 // Go to next highlight - should move to second "variable"
25838 cx.update_editor(|editor, window, cx| {
25839 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25840 });
25841 cx.assert_editor_state(
25842 "let variable = 42;
25843let another = ˇvariable + 1;
25844let result = variable * 2;",
25845 );
25846
25847 // Go to next highlight - should move to third "variable"
25848 cx.update_editor(|editor, window, cx| {
25849 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25850 });
25851 cx.assert_editor_state(
25852 "let variable = 42;
25853let another = variable + 1;
25854let result = ˇvariable * 2;",
25855 );
25856
25857 // Go to next highlight - should stay at third "variable" (no wrap-around)
25858 cx.update_editor(|editor, window, cx| {
25859 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25860 });
25861 cx.assert_editor_state(
25862 "let variable = 42;
25863let another = variable + 1;
25864let result = ˇvariable * 2;",
25865 );
25866
25867 // Now test going backwards from third position
25868 cx.update_editor(|editor, window, cx| {
25869 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25870 });
25871 cx.assert_editor_state(
25872 "let variable = 42;
25873let another = ˇvariable + 1;
25874let result = variable * 2;",
25875 );
25876
25877 // Go to previous highlight - should move to first "variable"
25878 cx.update_editor(|editor, window, cx| {
25879 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25880 });
25881 cx.assert_editor_state(
25882 "let ˇvariable = 42;
25883let another = variable + 1;
25884let result = variable * 2;",
25885 );
25886
25887 // Go to previous highlight - should stay on first "variable"
25888 cx.update_editor(|editor, window, cx| {
25889 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
25890 });
25891 cx.assert_editor_state(
25892 "let ˇvariable = 42;
25893let another = variable + 1;
25894let result = variable * 2;",
25895 );
25896}
25897
25898#[track_caller]
25899fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
25900 editor
25901 .all_inlays(cx)
25902 .into_iter()
25903 .filter_map(|inlay| inlay.get_color())
25904 .map(Rgba::from)
25905 .collect()
25906}