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 collections::HashMap;
17use futures::StreamExt;
18use gpui::{
19 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
20 VisualTestContext, WindowBounds, WindowOptions, div,
21};
22use indoc::indoc;
23use language::{
24 BracketPairConfig,
25 Capability::ReadWrite,
26 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
27 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
28 language_settings::{
29 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
30 SelectedFormatter,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use lsp::CompletionParams;
36use multi_buffer::{IndentGuide, PathKey};
37use parking_lot::Mutex;
38use pretty_assertions::{assert_eq, assert_ne};
39use project::{
40 FakeFs,
41 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
42 project_settings::LspSettings,
43};
44use serde_json::{self, json};
45use settings::{
46 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
47 ProjectSettingsContent,
48};
49use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
50use std::{
51 iter,
52 sync::atomic::{self, AtomicUsize},
53};
54use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
55use text::ToPoint as _;
56use unindent::Unindent;
57use util::{
58 assert_set_eq, path,
59 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
60 uri,
61};
62use workspace::{
63 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
64 OpenOptions, ViewId,
65 invalid_buffer_view::InvalidBufferView,
66 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
67 register_project_item,
68};
69
70#[gpui::test]
71fn test_edit_events(cx: &mut TestAppContext) {
72 init_test(cx, |_| {});
73
74 let buffer = cx.new(|cx| {
75 let mut buffer = language::Buffer::local("123456", cx);
76 buffer.set_group_interval(Duration::from_secs(1));
77 buffer
78 });
79
80 let events = Rc::new(RefCell::new(Vec::new()));
81 let editor1 = cx.add_window({
82 let events = events.clone();
83 |window, cx| {
84 let entity = cx.entity();
85 cx.subscribe_in(
86 &entity,
87 window,
88 move |_, _, event: &EditorEvent, _, _| match event {
89 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
90 EditorEvent::BufferEdited => {
91 events.borrow_mut().push(("editor1", "buffer edited"))
92 }
93 _ => {}
94 },
95 )
96 .detach();
97 Editor::for_buffer(buffer.clone(), None, window, cx)
98 }
99 });
100
101 let editor2 = cx.add_window({
102 let events = events.clone();
103 |window, cx| {
104 cx.subscribe_in(
105 &cx.entity(),
106 window,
107 move |_, _, event: &EditorEvent, _, _| match event {
108 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
109 EditorEvent::BufferEdited => {
110 events.borrow_mut().push(("editor2", "buffer edited"))
111 }
112 _ => {}
113 },
114 )
115 .detach();
116 Editor::for_buffer(buffer.clone(), None, window, cx)
117 }
118 });
119
120 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
121
122 // Mutating editor 1 will emit an `Edited` event only for that editor.
123 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
124 assert_eq!(
125 mem::take(&mut *events.borrow_mut()),
126 [
127 ("editor1", "edited"),
128 ("editor1", "buffer edited"),
129 ("editor2", "buffer edited"),
130 ]
131 );
132
133 // Mutating editor 2 will emit an `Edited` event only for that editor.
134 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor2", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Undoing on editor 1 will emit an `Edited` event only for that editor.
145 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor1", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Redoing on editor 1 will emit an `Edited` event only for that editor.
156 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor1", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Undoing on editor 2 will emit an `Edited` event only for that editor.
167 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor2", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // Redoing on editor 2 will emit an `Edited` event only for that editor.
178 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
179 assert_eq!(
180 mem::take(&mut *events.borrow_mut()),
181 [
182 ("editor2", "edited"),
183 ("editor1", "buffer edited"),
184 ("editor2", "buffer edited"),
185 ]
186 );
187
188 // No event is emitted when the mutation is a no-op.
189 _ = editor2.update(cx, |editor, window, cx| {
190 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
191 s.select_ranges([0..0])
192 });
193
194 editor.backspace(&Backspace, window, cx);
195 });
196 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
197}
198
199#[gpui::test]
200fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
201 init_test(cx, |_| {});
202
203 let mut now = Instant::now();
204 let group_interval = Duration::from_millis(1);
205 let buffer = cx.new(|cx| {
206 let mut buf = language::Buffer::local("123456", cx);
207 buf.set_group_interval(group_interval);
208 buf
209 });
210 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
211 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
212
213 _ = editor.update(cx, |editor, window, cx| {
214 editor.start_transaction_at(now, window, cx);
215 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
216 s.select_ranges([2..4])
217 });
218
219 editor.insert("cd", window, cx);
220 editor.end_transaction_at(now, cx);
221 assert_eq!(editor.text(cx), "12cd56");
222 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
223
224 editor.start_transaction_at(now, window, cx);
225 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
226 s.select_ranges([4..5])
227 });
228 editor.insert("e", window, cx);
229 editor.end_transaction_at(now, cx);
230 assert_eq!(editor.text(cx), "12cde6");
231 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
232
233 now += group_interval + Duration::from_millis(1);
234 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
235 s.select_ranges([2..2])
236 });
237
238 // Simulate an edit in another editor
239 buffer.update(cx, |buffer, cx| {
240 buffer.start_transaction_at(now, cx);
241 buffer.edit([(0..1, "a")], None, cx);
242 buffer.edit([(1..1, "b")], None, cx);
243 buffer.end_transaction_at(now, cx);
244 });
245
246 assert_eq!(editor.text(cx), "ab2cde6");
247 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
248
249 // Last transaction happened past the group interval in a different editor.
250 // Undo it individually and don't restore selections.
251 editor.undo(&Undo, window, cx);
252 assert_eq!(editor.text(cx), "12cde6");
253 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
254
255 // First two transactions happened within the group interval in this editor.
256 // Undo them together and restore selections.
257 editor.undo(&Undo, window, cx);
258 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
259 assert_eq!(editor.text(cx), "123456");
260 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
261
262 // Redo the first two transactions together.
263 editor.redo(&Redo, window, cx);
264 assert_eq!(editor.text(cx), "12cde6");
265 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
266
267 // Redo the last transaction on its own.
268 editor.redo(&Redo, window, cx);
269 assert_eq!(editor.text(cx), "ab2cde6");
270 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
271
272 // Test empty transactions.
273 editor.start_transaction_at(now, window, cx);
274 editor.end_transaction_at(now, cx);
275 editor.undo(&Undo, window, cx);
276 assert_eq!(editor.text(cx), "12cde6");
277 });
278}
279
280#[gpui::test]
281fn test_ime_composition(cx: &mut TestAppContext) {
282 init_test(cx, |_| {});
283
284 let buffer = cx.new(|cx| {
285 let mut buffer = language::Buffer::local("abcde", cx);
286 // Ensure automatic grouping doesn't occur.
287 buffer.set_group_interval(Duration::ZERO);
288 buffer
289 });
290
291 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
292 cx.add_window(|window, cx| {
293 let mut editor = build_editor(buffer.clone(), window, cx);
294
295 // Start a new IME composition.
296 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
297 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
298 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
299 assert_eq!(editor.text(cx), "äbcde");
300 assert_eq!(
301 editor.marked_text_ranges(cx),
302 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
303 );
304
305 // Finalize IME composition.
306 editor.replace_text_in_range(None, "ā", window, cx);
307 assert_eq!(editor.text(cx), "ābcde");
308 assert_eq!(editor.marked_text_ranges(cx), None);
309
310 // IME composition edits are grouped and are undone/redone at once.
311 editor.undo(&Default::default(), window, cx);
312 assert_eq!(editor.text(cx), "abcde");
313 assert_eq!(editor.marked_text_ranges(cx), None);
314 editor.redo(&Default::default(), window, cx);
315 assert_eq!(editor.text(cx), "ābcde");
316 assert_eq!(editor.marked_text_ranges(cx), None);
317
318 // Start a new IME composition.
319 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
320 assert_eq!(
321 editor.marked_text_ranges(cx),
322 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
323 );
324
325 // Undoing during an IME composition cancels it.
326 editor.undo(&Default::default(), window, cx);
327 assert_eq!(editor.text(cx), "ābcde");
328 assert_eq!(editor.marked_text_ranges(cx), None);
329
330 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
331 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
332 assert_eq!(editor.text(cx), "ābcdè");
333 assert_eq!(
334 editor.marked_text_ranges(cx),
335 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
336 );
337
338 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
339 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
340 assert_eq!(editor.text(cx), "ābcdę");
341 assert_eq!(editor.marked_text_ranges(cx), None);
342
343 // Start a new IME composition with multiple cursors.
344 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
345 s.select_ranges([
346 OffsetUtf16(1)..OffsetUtf16(1),
347 OffsetUtf16(3)..OffsetUtf16(3),
348 OffsetUtf16(5)..OffsetUtf16(5),
349 ])
350 });
351 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
352 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
353 assert_eq!(
354 editor.marked_text_ranges(cx),
355 Some(vec![
356 OffsetUtf16(0)..OffsetUtf16(3),
357 OffsetUtf16(4)..OffsetUtf16(7),
358 OffsetUtf16(8)..OffsetUtf16(11)
359 ])
360 );
361
362 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
363 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
364 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
365 assert_eq!(
366 editor.marked_text_ranges(cx),
367 Some(vec![
368 OffsetUtf16(1)..OffsetUtf16(2),
369 OffsetUtf16(5)..OffsetUtf16(6),
370 OffsetUtf16(9)..OffsetUtf16(10)
371 ])
372 );
373
374 // Finalize IME composition with multiple cursors.
375 editor.replace_text_in_range(Some(9..10), "2", window, cx);
376 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
377 assert_eq!(editor.marked_text_ranges(cx), None);
378
379 editor
380 });
381}
382
383#[gpui::test]
384fn test_selection_with_mouse(cx: &mut TestAppContext) {
385 init_test(cx, |_| {});
386
387 let editor = cx.add_window(|window, cx| {
388 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
389 build_editor(buffer, window, cx)
390 });
391
392 _ = editor.update(cx, |editor, window, cx| {
393 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
394 });
395 assert_eq!(
396 editor
397 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
398 .unwrap(),
399 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
400 );
401
402 _ = editor.update(cx, |editor, window, cx| {
403 editor.update_selection(
404 DisplayPoint::new(DisplayRow(3), 3),
405 0,
406 gpui::Point::<f32>::default(),
407 window,
408 cx,
409 );
410 });
411
412 assert_eq!(
413 editor
414 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
415 .unwrap(),
416 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
417 );
418
419 _ = editor.update(cx, |editor, window, cx| {
420 editor.update_selection(
421 DisplayPoint::new(DisplayRow(1), 1),
422 0,
423 gpui::Point::<f32>::default(),
424 window,
425 cx,
426 );
427 });
428
429 assert_eq!(
430 editor
431 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
432 .unwrap(),
433 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
434 );
435
436 _ = editor.update(cx, |editor, window, cx| {
437 editor.end_selection(window, cx);
438 editor.update_selection(
439 DisplayPoint::new(DisplayRow(3), 3),
440 0,
441 gpui::Point::<f32>::default(),
442 window,
443 cx,
444 );
445 });
446
447 assert_eq!(
448 editor
449 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
450 .unwrap(),
451 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
452 );
453
454 _ = editor.update(cx, |editor, window, cx| {
455 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
456 editor.update_selection(
457 DisplayPoint::new(DisplayRow(0), 0),
458 0,
459 gpui::Point::<f32>::default(),
460 window,
461 cx,
462 );
463 });
464
465 assert_eq!(
466 editor
467 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
468 .unwrap(),
469 [
470 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
471 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
472 ]
473 );
474
475 _ = editor.update(cx, |editor, window, cx| {
476 editor.end_selection(window, cx);
477 });
478
479 assert_eq!(
480 editor
481 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
482 .unwrap(),
483 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
484 );
485}
486
487#[gpui::test]
488fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
489 init_test(cx, |_| {});
490
491 let editor = cx.add_window(|window, cx| {
492 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
493 build_editor(buffer, window, cx)
494 });
495
496 _ = editor.update(cx, |editor, window, cx| {
497 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
498 });
499
500 _ = editor.update(cx, |editor, window, cx| {
501 editor.end_selection(window, cx);
502 });
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
506 });
507
508 _ = editor.update(cx, |editor, window, cx| {
509 editor.end_selection(window, cx);
510 });
511
512 assert_eq!(
513 editor
514 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
515 .unwrap(),
516 [
517 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
518 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
519 ]
520 );
521
522 _ = editor.update(cx, |editor, window, cx| {
523 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
524 });
525
526 _ = editor.update(cx, |editor, window, cx| {
527 editor.end_selection(window, cx);
528 });
529
530 assert_eq!(
531 editor
532 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
533 .unwrap(),
534 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
535 );
536}
537
538#[gpui::test]
539fn test_canceling_pending_selection(cx: &mut TestAppContext) {
540 init_test(cx, |_| {});
541
542 let editor = cx.add_window(|window, cx| {
543 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
544 build_editor(buffer, window, cx)
545 });
546
547 _ = editor.update(cx, |editor, window, cx| {
548 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
549 assert_eq!(
550 editor.selections.display_ranges(cx),
551 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
552 );
553 });
554
555 _ = editor.update(cx, |editor, window, cx| {
556 editor.update_selection(
557 DisplayPoint::new(DisplayRow(3), 3),
558 0,
559 gpui::Point::<f32>::default(),
560 window,
561 cx,
562 );
563 assert_eq!(
564 editor.selections.display_ranges(cx),
565 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
566 );
567 });
568
569 _ = editor.update(cx, |editor, window, cx| {
570 editor.cancel(&Cancel, window, cx);
571 editor.update_selection(
572 DisplayPoint::new(DisplayRow(1), 1),
573 0,
574 gpui::Point::<f32>::default(),
575 window,
576 cx,
577 );
578 assert_eq!(
579 editor.selections.display_ranges(cx),
580 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
581 );
582 });
583}
584
585#[gpui::test]
586fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
587 init_test(cx, |_| {});
588
589 let editor = cx.add_window(|window, cx| {
590 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
591 build_editor(buffer, window, cx)
592 });
593
594 _ = editor.update(cx, |editor, window, cx| {
595 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
596 assert_eq!(
597 editor.selections.display_ranges(cx),
598 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
599 );
600
601 editor.move_down(&Default::default(), window, cx);
602 assert_eq!(
603 editor.selections.display_ranges(cx),
604 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
605 );
606
607 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
608 assert_eq!(
609 editor.selections.display_ranges(cx),
610 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
611 );
612
613 editor.move_up(&Default::default(), window, cx);
614 assert_eq!(
615 editor.selections.display_ranges(cx),
616 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
617 );
618 });
619}
620
621#[gpui::test]
622fn test_clone(cx: &mut TestAppContext) {
623 init_test(cx, |_| {});
624
625 let (text, selection_ranges) = marked_text_ranges(
626 indoc! {"
627 one
628 two
629 threeˇ
630 four
631 fiveˇ
632 "},
633 true,
634 );
635
636 let editor = cx.add_window(|window, cx| {
637 let buffer = MultiBuffer::build_simple(&text, cx);
638 build_editor(buffer, window, cx)
639 });
640
641 _ = editor.update(cx, |editor, window, cx| {
642 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
643 s.select_ranges(selection_ranges.clone())
644 });
645 editor.fold_creases(
646 vec![
647 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
648 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
649 ],
650 true,
651 window,
652 cx,
653 );
654 });
655
656 let cloned_editor = editor
657 .update(cx, |editor, _, cx| {
658 cx.open_window(Default::default(), |window, cx| {
659 cx.new(|cx| editor.clone(window, cx))
660 })
661 })
662 .unwrap()
663 .unwrap();
664
665 let snapshot = editor
666 .update(cx, |e, window, cx| e.snapshot(window, cx))
667 .unwrap();
668 let cloned_snapshot = cloned_editor
669 .update(cx, |e, window, cx| e.snapshot(window, cx))
670 .unwrap();
671
672 assert_eq!(
673 cloned_editor
674 .update(cx, |e, _, cx| e.display_text(cx))
675 .unwrap(),
676 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
677 );
678 assert_eq!(
679 cloned_snapshot
680 .folds_in_range(0..text.len())
681 .collect::<Vec<_>>(),
682 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
683 );
684 assert_set_eq!(
685 cloned_editor
686 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
687 .unwrap(),
688 editor
689 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
690 .unwrap()
691 );
692 assert_set_eq!(
693 cloned_editor
694 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
695 .unwrap(),
696 editor
697 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
698 .unwrap()
699 );
700}
701
702#[gpui::test]
703async fn test_navigation_history(cx: &mut TestAppContext) {
704 init_test(cx, |_| {});
705
706 use workspace::item::Item;
707
708 let fs = FakeFs::new(cx.executor());
709 let project = Project::test(fs, [], cx).await;
710 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
711 let pane = workspace
712 .update(cx, |workspace, _, _| workspace.active_pane().clone())
713 .unwrap();
714
715 _ = workspace.update(cx, |_v, window, cx| {
716 cx.new(|cx| {
717 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
718 let mut editor = build_editor(buffer, window, cx);
719 let handle = cx.entity();
720 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
721
722 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
723 editor.nav_history.as_mut().unwrap().pop_backward(cx)
724 }
725
726 // Move the cursor a small distance.
727 // Nothing is added to the navigation history.
728 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
729 s.select_display_ranges([
730 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
731 ])
732 });
733 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
734 s.select_display_ranges([
735 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
736 ])
737 });
738 assert!(pop_history(&mut editor, cx).is_none());
739
740 // Move the cursor a large distance.
741 // The history can jump back to the previous position.
742 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
743 s.select_display_ranges([
744 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
745 ])
746 });
747 let nav_entry = pop_history(&mut editor, cx).unwrap();
748 editor.navigate(nav_entry.data.unwrap(), window, cx);
749 assert_eq!(nav_entry.item.id(), cx.entity_id());
750 assert_eq!(
751 editor.selections.display_ranges(cx),
752 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
753 );
754 assert!(pop_history(&mut editor, cx).is_none());
755
756 // Move the cursor a small distance via the mouse.
757 // Nothing is added to the navigation history.
758 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
759 editor.end_selection(window, cx);
760 assert_eq!(
761 editor.selections.display_ranges(cx),
762 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
763 );
764 assert!(pop_history(&mut editor, cx).is_none());
765
766 // Move the cursor a large distance via the mouse.
767 // The history can jump back to the previous position.
768 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
769 editor.end_selection(window, cx);
770 assert_eq!(
771 editor.selections.display_ranges(cx),
772 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
773 );
774 let nav_entry = pop_history(&mut editor, cx).unwrap();
775 editor.navigate(nav_entry.data.unwrap(), window, cx);
776 assert_eq!(nav_entry.item.id(), cx.entity_id());
777 assert_eq!(
778 editor.selections.display_ranges(cx),
779 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
780 );
781 assert!(pop_history(&mut editor, cx).is_none());
782
783 // Set scroll position to check later
784 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
785 let original_scroll_position = editor.scroll_manager.anchor();
786
787 // Jump to the end of the document and adjust scroll
788 editor.move_to_end(&MoveToEnd, window, cx);
789 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
790 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
791
792 let nav_entry = pop_history(&mut editor, cx).unwrap();
793 editor.navigate(nav_entry.data.unwrap(), window, cx);
794 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
795
796 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
797 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
798 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
799 let invalid_point = Point::new(9999, 0);
800 editor.navigate(
801 Box::new(NavigationData {
802 cursor_anchor: invalid_anchor,
803 cursor_position: invalid_point,
804 scroll_anchor: ScrollAnchor {
805 anchor: invalid_anchor,
806 offset: Default::default(),
807 },
808 scroll_top_row: invalid_point.row,
809 }),
810 window,
811 cx,
812 );
813 assert_eq!(
814 editor.selections.display_ranges(cx),
815 &[editor.max_point(cx)..editor.max_point(cx)]
816 );
817 assert_eq!(
818 editor.scroll_position(cx),
819 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
820 );
821
822 editor
823 })
824 });
825}
826
827#[gpui::test]
828fn test_cancel(cx: &mut TestAppContext) {
829 init_test(cx, |_| {});
830
831 let editor = cx.add_window(|window, cx| {
832 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
833 build_editor(buffer, window, cx)
834 });
835
836 _ = editor.update(cx, |editor, window, cx| {
837 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
838 editor.update_selection(
839 DisplayPoint::new(DisplayRow(1), 1),
840 0,
841 gpui::Point::<f32>::default(),
842 window,
843 cx,
844 );
845 editor.end_selection(window, cx);
846
847 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
848 editor.update_selection(
849 DisplayPoint::new(DisplayRow(0), 3),
850 0,
851 gpui::Point::<f32>::default(),
852 window,
853 cx,
854 );
855 editor.end_selection(window, cx);
856 assert_eq!(
857 editor.selections.display_ranges(cx),
858 [
859 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
860 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
861 ]
862 );
863 });
864
865 _ = editor.update(cx, |editor, window, cx| {
866 editor.cancel(&Cancel, window, cx);
867 assert_eq!(
868 editor.selections.display_ranges(cx),
869 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
870 );
871 });
872
873 _ = editor.update(cx, |editor, window, cx| {
874 editor.cancel(&Cancel, window, cx);
875 assert_eq!(
876 editor.selections.display_ranges(cx),
877 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
878 );
879 });
880}
881
882#[gpui::test]
883fn test_fold_action(cx: &mut TestAppContext) {
884 init_test(cx, |_| {});
885
886 let editor = cx.add_window(|window, cx| {
887 let buffer = MultiBuffer::build_simple(
888 &"
889 impl Foo {
890 // Hello!
891
892 fn a() {
893 1
894 }
895
896 fn b() {
897 2
898 }
899
900 fn c() {
901 3
902 }
903 }
904 "
905 .unindent(),
906 cx,
907 );
908 build_editor(buffer, window, cx)
909 });
910
911 _ = editor.update(cx, |editor, window, cx| {
912 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
913 s.select_display_ranges([
914 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
915 ]);
916 });
917 editor.fold(&Fold, window, cx);
918 assert_eq!(
919 editor.display_text(cx),
920 "
921 impl Foo {
922 // Hello!
923
924 fn a() {
925 1
926 }
927
928 fn b() {⋯
929 }
930
931 fn c() {⋯
932 }
933 }
934 "
935 .unindent(),
936 );
937
938 editor.fold(&Fold, window, cx);
939 assert_eq!(
940 editor.display_text(cx),
941 "
942 impl Foo {⋯
943 }
944 "
945 .unindent(),
946 );
947
948 editor.unfold_lines(&UnfoldLines, window, cx);
949 assert_eq!(
950 editor.display_text(cx),
951 "
952 impl Foo {
953 // Hello!
954
955 fn a() {
956 1
957 }
958
959 fn b() {⋯
960 }
961
962 fn c() {⋯
963 }
964 }
965 "
966 .unindent(),
967 );
968
969 editor.unfold_lines(&UnfoldLines, window, cx);
970 assert_eq!(
971 editor.display_text(cx),
972 editor.buffer.read(cx).read(cx).text()
973 );
974 });
975}
976
977#[gpui::test]
978fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
979 init_test(cx, |_| {});
980
981 let editor = cx.add_window(|window, cx| {
982 let buffer = MultiBuffer::build_simple(
983 &"
984 class Foo:
985 # Hello!
986
987 def a():
988 print(1)
989
990 def b():
991 print(2)
992
993 def c():
994 print(3)
995 "
996 .unindent(),
997 cx,
998 );
999 build_editor(buffer, window, cx)
1000 });
1001
1002 _ = editor.update(cx, |editor, window, cx| {
1003 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1004 s.select_display_ranges([
1005 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1006 ]);
1007 });
1008 editor.fold(&Fold, window, cx);
1009 assert_eq!(
1010 editor.display_text(cx),
1011 "
1012 class Foo:
1013 # Hello!
1014
1015 def a():
1016 print(1)
1017
1018 def b():⋯
1019
1020 def c():⋯
1021 "
1022 .unindent(),
1023 );
1024
1025 editor.fold(&Fold, window, cx);
1026 assert_eq!(
1027 editor.display_text(cx),
1028 "
1029 class Foo:⋯
1030 "
1031 .unindent(),
1032 );
1033
1034 editor.unfold_lines(&UnfoldLines, window, cx);
1035 assert_eq!(
1036 editor.display_text(cx),
1037 "
1038 class Foo:
1039 # Hello!
1040
1041 def a():
1042 print(1)
1043
1044 def b():⋯
1045
1046 def c():⋯
1047 "
1048 .unindent(),
1049 );
1050
1051 editor.unfold_lines(&UnfoldLines, window, cx);
1052 assert_eq!(
1053 editor.display_text(cx),
1054 editor.buffer.read(cx).read(cx).text()
1055 );
1056 });
1057}
1058
1059#[gpui::test]
1060fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1061 init_test(cx, |_| {});
1062
1063 let editor = cx.add_window(|window, cx| {
1064 let buffer = MultiBuffer::build_simple(
1065 &"
1066 class Foo:
1067 # Hello!
1068
1069 def a():
1070 print(1)
1071
1072 def b():
1073 print(2)
1074
1075
1076 def c():
1077 print(3)
1078
1079
1080 "
1081 .unindent(),
1082 cx,
1083 );
1084 build_editor(buffer, window, cx)
1085 });
1086
1087 _ = editor.update(cx, |editor, window, cx| {
1088 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1089 s.select_display_ranges([
1090 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1091 ]);
1092 });
1093 editor.fold(&Fold, window, cx);
1094 assert_eq!(
1095 editor.display_text(cx),
1096 "
1097 class Foo:
1098 # Hello!
1099
1100 def a():
1101 print(1)
1102
1103 def b():⋯
1104
1105
1106 def c():⋯
1107
1108
1109 "
1110 .unindent(),
1111 );
1112
1113 editor.fold(&Fold, window, cx);
1114 assert_eq!(
1115 editor.display_text(cx),
1116 "
1117 class Foo:⋯
1118
1119
1120 "
1121 .unindent(),
1122 );
1123
1124 editor.unfold_lines(&UnfoldLines, window, cx);
1125 assert_eq!(
1126 editor.display_text(cx),
1127 "
1128 class Foo:
1129 # Hello!
1130
1131 def a():
1132 print(1)
1133
1134 def b():⋯
1135
1136
1137 def c():⋯
1138
1139
1140 "
1141 .unindent(),
1142 );
1143
1144 editor.unfold_lines(&UnfoldLines, window, cx);
1145 assert_eq!(
1146 editor.display_text(cx),
1147 editor.buffer.read(cx).read(cx).text()
1148 );
1149 });
1150}
1151
1152#[gpui::test]
1153fn test_fold_at_level(cx: &mut TestAppContext) {
1154 init_test(cx, |_| {});
1155
1156 let editor = cx.add_window(|window, cx| {
1157 let buffer = MultiBuffer::build_simple(
1158 &"
1159 class Foo:
1160 # Hello!
1161
1162 def a():
1163 print(1)
1164
1165 def b():
1166 print(2)
1167
1168
1169 class Bar:
1170 # World!
1171
1172 def a():
1173 print(1)
1174
1175 def b():
1176 print(2)
1177
1178
1179 "
1180 .unindent(),
1181 cx,
1182 );
1183 build_editor(buffer, window, cx)
1184 });
1185
1186 _ = editor.update(cx, |editor, window, cx| {
1187 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1188 assert_eq!(
1189 editor.display_text(cx),
1190 "
1191 class Foo:
1192 # Hello!
1193
1194 def a():⋯
1195
1196 def b():⋯
1197
1198
1199 class Bar:
1200 # World!
1201
1202 def a():⋯
1203
1204 def b():⋯
1205
1206
1207 "
1208 .unindent(),
1209 );
1210
1211 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1212 assert_eq!(
1213 editor.display_text(cx),
1214 "
1215 class Foo:⋯
1216
1217
1218 class Bar:⋯
1219
1220
1221 "
1222 .unindent(),
1223 );
1224
1225 editor.unfold_all(&UnfoldAll, window, cx);
1226 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1227 assert_eq!(
1228 editor.display_text(cx),
1229 "
1230 class Foo:
1231 # Hello!
1232
1233 def a():
1234 print(1)
1235
1236 def b():
1237 print(2)
1238
1239
1240 class Bar:
1241 # World!
1242
1243 def a():
1244 print(1)
1245
1246 def b():
1247 print(2)
1248
1249
1250 "
1251 .unindent(),
1252 );
1253
1254 assert_eq!(
1255 editor.display_text(cx),
1256 editor.buffer.read(cx).read(cx).text()
1257 );
1258 });
1259}
1260
1261#[gpui::test]
1262fn test_move_cursor(cx: &mut TestAppContext) {
1263 init_test(cx, |_| {});
1264
1265 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1266 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1267
1268 buffer.update(cx, |buffer, cx| {
1269 buffer.edit(
1270 vec![
1271 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1272 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1273 ],
1274 None,
1275 cx,
1276 );
1277 });
1278 _ = editor.update(cx, |editor, window, cx| {
1279 assert_eq!(
1280 editor.selections.display_ranges(cx),
1281 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1282 );
1283
1284 editor.move_down(&MoveDown, window, cx);
1285 assert_eq!(
1286 editor.selections.display_ranges(cx),
1287 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1288 );
1289
1290 editor.move_right(&MoveRight, window, cx);
1291 assert_eq!(
1292 editor.selections.display_ranges(cx),
1293 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1294 );
1295
1296 editor.move_left(&MoveLeft, window, cx);
1297 assert_eq!(
1298 editor.selections.display_ranges(cx),
1299 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1300 );
1301
1302 editor.move_up(&MoveUp, window, cx);
1303 assert_eq!(
1304 editor.selections.display_ranges(cx),
1305 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1306 );
1307
1308 editor.move_to_end(&MoveToEnd, window, cx);
1309 assert_eq!(
1310 editor.selections.display_ranges(cx),
1311 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1312 );
1313
1314 editor.move_to_beginning(&MoveToBeginning, window, cx);
1315 assert_eq!(
1316 editor.selections.display_ranges(cx),
1317 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1318 );
1319
1320 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1321 s.select_display_ranges([
1322 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1323 ]);
1324 });
1325 editor.select_to_beginning(&SelectToBeginning, window, cx);
1326 assert_eq!(
1327 editor.selections.display_ranges(cx),
1328 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1329 );
1330
1331 editor.select_to_end(&SelectToEnd, window, cx);
1332 assert_eq!(
1333 editor.selections.display_ranges(cx),
1334 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1335 );
1336 });
1337}
1338
1339#[gpui::test]
1340fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1341 init_test(cx, |_| {});
1342
1343 let editor = cx.add_window(|window, cx| {
1344 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1345 build_editor(buffer, window, cx)
1346 });
1347
1348 assert_eq!('🟥'.len_utf8(), 4);
1349 assert_eq!('α'.len_utf8(), 2);
1350
1351 _ = editor.update(cx, |editor, window, cx| {
1352 editor.fold_creases(
1353 vec![
1354 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1355 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1356 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1357 ],
1358 true,
1359 window,
1360 cx,
1361 );
1362 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1363
1364 editor.move_right(&MoveRight, window, cx);
1365 assert_eq!(
1366 editor.selections.display_ranges(cx),
1367 &[empty_range(0, "🟥".len())]
1368 );
1369 editor.move_right(&MoveRight, window, cx);
1370 assert_eq!(
1371 editor.selections.display_ranges(cx),
1372 &[empty_range(0, "🟥🟧".len())]
1373 );
1374 editor.move_right(&MoveRight, window, cx);
1375 assert_eq!(
1376 editor.selections.display_ranges(cx),
1377 &[empty_range(0, "🟥🟧⋯".len())]
1378 );
1379
1380 editor.move_down(&MoveDown, window, cx);
1381 assert_eq!(
1382 editor.selections.display_ranges(cx),
1383 &[empty_range(1, "ab⋯e".len())]
1384 );
1385 editor.move_left(&MoveLeft, window, cx);
1386 assert_eq!(
1387 editor.selections.display_ranges(cx),
1388 &[empty_range(1, "ab⋯".len())]
1389 );
1390 editor.move_left(&MoveLeft, window, cx);
1391 assert_eq!(
1392 editor.selections.display_ranges(cx),
1393 &[empty_range(1, "ab".len())]
1394 );
1395 editor.move_left(&MoveLeft, window, cx);
1396 assert_eq!(
1397 editor.selections.display_ranges(cx),
1398 &[empty_range(1, "a".len())]
1399 );
1400
1401 editor.move_down(&MoveDown, window, cx);
1402 assert_eq!(
1403 editor.selections.display_ranges(cx),
1404 &[empty_range(2, "α".len())]
1405 );
1406 editor.move_right(&MoveRight, window, cx);
1407 assert_eq!(
1408 editor.selections.display_ranges(cx),
1409 &[empty_range(2, "αβ".len())]
1410 );
1411 editor.move_right(&MoveRight, window, cx);
1412 assert_eq!(
1413 editor.selections.display_ranges(cx),
1414 &[empty_range(2, "αβ⋯".len())]
1415 );
1416 editor.move_right(&MoveRight, window, cx);
1417 assert_eq!(
1418 editor.selections.display_ranges(cx),
1419 &[empty_range(2, "αβ⋯ε".len())]
1420 );
1421
1422 editor.move_up(&MoveUp, window, cx);
1423 assert_eq!(
1424 editor.selections.display_ranges(cx),
1425 &[empty_range(1, "ab⋯e".len())]
1426 );
1427 editor.move_down(&MoveDown, window, cx);
1428 assert_eq!(
1429 editor.selections.display_ranges(cx),
1430 &[empty_range(2, "αβ⋯ε".len())]
1431 );
1432 editor.move_up(&MoveUp, window, cx);
1433 assert_eq!(
1434 editor.selections.display_ranges(cx),
1435 &[empty_range(1, "ab⋯e".len())]
1436 );
1437
1438 editor.move_up(&MoveUp, window, cx);
1439 assert_eq!(
1440 editor.selections.display_ranges(cx),
1441 &[empty_range(0, "🟥🟧".len())]
1442 );
1443 editor.move_left(&MoveLeft, window, cx);
1444 assert_eq!(
1445 editor.selections.display_ranges(cx),
1446 &[empty_range(0, "🟥".len())]
1447 );
1448 editor.move_left(&MoveLeft, window, cx);
1449 assert_eq!(
1450 editor.selections.display_ranges(cx),
1451 &[empty_range(0, "".len())]
1452 );
1453 });
1454}
1455
1456#[gpui::test]
1457fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1458 init_test(cx, |_| {});
1459
1460 let editor = cx.add_window(|window, cx| {
1461 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1462 build_editor(buffer, window, cx)
1463 });
1464 _ = editor.update(cx, |editor, window, cx| {
1465 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1466 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1467 });
1468
1469 // moving above start of document should move selection to start of document,
1470 // but the next move down should still be at the original goal_x
1471 editor.move_up(&MoveUp, window, cx);
1472 assert_eq!(
1473 editor.selections.display_ranges(cx),
1474 &[empty_range(0, "".len())]
1475 );
1476
1477 editor.move_down(&MoveDown, window, cx);
1478 assert_eq!(
1479 editor.selections.display_ranges(cx),
1480 &[empty_range(1, "abcd".len())]
1481 );
1482
1483 editor.move_down(&MoveDown, window, cx);
1484 assert_eq!(
1485 editor.selections.display_ranges(cx),
1486 &[empty_range(2, "αβγ".len())]
1487 );
1488
1489 editor.move_down(&MoveDown, window, cx);
1490 assert_eq!(
1491 editor.selections.display_ranges(cx),
1492 &[empty_range(3, "abcd".len())]
1493 );
1494
1495 editor.move_down(&MoveDown, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1499 );
1500
1501 // moving past end of document should not change goal_x
1502 editor.move_down(&MoveDown, window, cx);
1503 assert_eq!(
1504 editor.selections.display_ranges(cx),
1505 &[empty_range(5, "".len())]
1506 );
1507
1508 editor.move_down(&MoveDown, window, cx);
1509 assert_eq!(
1510 editor.selections.display_ranges(cx),
1511 &[empty_range(5, "".len())]
1512 );
1513
1514 editor.move_up(&MoveUp, window, cx);
1515 assert_eq!(
1516 editor.selections.display_ranges(cx),
1517 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1518 );
1519
1520 editor.move_up(&MoveUp, window, cx);
1521 assert_eq!(
1522 editor.selections.display_ranges(cx),
1523 &[empty_range(3, "abcd".len())]
1524 );
1525
1526 editor.move_up(&MoveUp, window, cx);
1527 assert_eq!(
1528 editor.selections.display_ranges(cx),
1529 &[empty_range(2, "αβγ".len())]
1530 );
1531 });
1532}
1533
1534#[gpui::test]
1535fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1536 init_test(cx, |_| {});
1537 let move_to_beg = MoveToBeginningOfLine {
1538 stop_at_soft_wraps: true,
1539 stop_at_indent: true,
1540 };
1541
1542 let delete_to_beg = DeleteToBeginningOfLine {
1543 stop_at_indent: false,
1544 };
1545
1546 let move_to_end = MoveToEndOfLine {
1547 stop_at_soft_wraps: true,
1548 };
1549
1550 let editor = cx.add_window(|window, cx| {
1551 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1552 build_editor(buffer, window, cx)
1553 });
1554 _ = editor.update(cx, |editor, window, cx| {
1555 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1556 s.select_display_ranges([
1557 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1558 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1559 ]);
1560 });
1561 });
1562
1563 _ = editor.update(cx, |editor, window, cx| {
1564 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1565 assert_eq!(
1566 editor.selections.display_ranges(cx),
1567 &[
1568 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1569 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1570 ]
1571 );
1572 });
1573
1574 _ = editor.update(cx, |editor, window, cx| {
1575 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1576 assert_eq!(
1577 editor.selections.display_ranges(cx),
1578 &[
1579 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1580 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1581 ]
1582 );
1583 });
1584
1585 _ = editor.update(cx, |editor, window, cx| {
1586 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1587 assert_eq!(
1588 editor.selections.display_ranges(cx),
1589 &[
1590 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1591 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1592 ]
1593 );
1594 });
1595
1596 _ = editor.update(cx, |editor, window, cx| {
1597 editor.move_to_end_of_line(&move_to_end, window, cx);
1598 assert_eq!(
1599 editor.selections.display_ranges(cx),
1600 &[
1601 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1602 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1603 ]
1604 );
1605 });
1606
1607 // Moving to the end of line again is a no-op.
1608 _ = editor.update(cx, |editor, window, cx| {
1609 editor.move_to_end_of_line(&move_to_end, window, cx);
1610 assert_eq!(
1611 editor.selections.display_ranges(cx),
1612 &[
1613 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1614 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1615 ]
1616 );
1617 });
1618
1619 _ = editor.update(cx, |editor, window, cx| {
1620 editor.move_left(&MoveLeft, window, cx);
1621 editor.select_to_beginning_of_line(
1622 &SelectToBeginningOfLine {
1623 stop_at_soft_wraps: true,
1624 stop_at_indent: true,
1625 },
1626 window,
1627 cx,
1628 );
1629 assert_eq!(
1630 editor.selections.display_ranges(cx),
1631 &[
1632 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1633 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1634 ]
1635 );
1636 });
1637
1638 _ = editor.update(cx, |editor, window, cx| {
1639 editor.select_to_beginning_of_line(
1640 &SelectToBeginningOfLine {
1641 stop_at_soft_wraps: true,
1642 stop_at_indent: true,
1643 },
1644 window,
1645 cx,
1646 );
1647 assert_eq!(
1648 editor.selections.display_ranges(cx),
1649 &[
1650 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1651 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1652 ]
1653 );
1654 });
1655
1656 _ = editor.update(cx, |editor, window, cx| {
1657 editor.select_to_beginning_of_line(
1658 &SelectToBeginningOfLine {
1659 stop_at_soft_wraps: true,
1660 stop_at_indent: true,
1661 },
1662 window,
1663 cx,
1664 );
1665 assert_eq!(
1666 editor.selections.display_ranges(cx),
1667 &[
1668 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1669 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1670 ]
1671 );
1672 });
1673
1674 _ = editor.update(cx, |editor, window, cx| {
1675 editor.select_to_end_of_line(
1676 &SelectToEndOfLine {
1677 stop_at_soft_wraps: true,
1678 },
1679 window,
1680 cx,
1681 );
1682 assert_eq!(
1683 editor.selections.display_ranges(cx),
1684 &[
1685 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1686 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1687 ]
1688 );
1689 });
1690
1691 _ = editor.update(cx, |editor, window, cx| {
1692 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1693 assert_eq!(editor.display_text(cx), "ab\n de");
1694 assert_eq!(
1695 editor.selections.display_ranges(cx),
1696 &[
1697 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1698 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1699 ]
1700 );
1701 });
1702
1703 _ = editor.update(cx, |editor, window, cx| {
1704 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1705 assert_eq!(editor.display_text(cx), "\n");
1706 assert_eq!(
1707 editor.selections.display_ranges(cx),
1708 &[
1709 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1710 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1711 ]
1712 );
1713 });
1714}
1715
1716#[gpui::test]
1717fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1718 init_test(cx, |_| {});
1719 let move_to_beg = MoveToBeginningOfLine {
1720 stop_at_soft_wraps: false,
1721 stop_at_indent: false,
1722 };
1723
1724 let move_to_end = MoveToEndOfLine {
1725 stop_at_soft_wraps: false,
1726 };
1727
1728 let editor = cx.add_window(|window, cx| {
1729 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1730 build_editor(buffer, window, cx)
1731 });
1732
1733 _ = editor.update(cx, |editor, window, cx| {
1734 editor.set_wrap_width(Some(140.0.into()), cx);
1735
1736 // We expect the following lines after wrapping
1737 // ```
1738 // thequickbrownfox
1739 // jumpedoverthelazydo
1740 // gs
1741 // ```
1742 // The final `gs` was soft-wrapped onto a new line.
1743 assert_eq!(
1744 "thequickbrownfox\njumpedoverthelaz\nydogs",
1745 editor.display_text(cx),
1746 );
1747
1748 // First, let's assert behavior on the first line, that was not soft-wrapped.
1749 // Start the cursor at the `k` on the first line
1750 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1751 s.select_display_ranges([
1752 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1753 ]);
1754 });
1755
1756 // Moving to the beginning of the line should put us at the beginning of the line.
1757 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1758 assert_eq!(
1759 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1760 editor.selections.display_ranges(cx)
1761 );
1762
1763 // Moving to the end of the line should put us at the end of the line.
1764 editor.move_to_end_of_line(&move_to_end, window, cx);
1765 assert_eq!(
1766 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1767 editor.selections.display_ranges(cx)
1768 );
1769
1770 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1771 // Start the cursor at the last line (`y` that was wrapped to a new line)
1772 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1773 s.select_display_ranges([
1774 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1775 ]);
1776 });
1777
1778 // Moving to the beginning of the line should put us at the start of the second line of
1779 // display text, i.e., the `j`.
1780 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1781 assert_eq!(
1782 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1783 editor.selections.display_ranges(cx)
1784 );
1785
1786 // Moving to the beginning of the line again should be a no-op.
1787 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1788 assert_eq!(
1789 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1790 editor.selections.display_ranges(cx)
1791 );
1792
1793 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1794 // next display line.
1795 editor.move_to_end_of_line(&move_to_end, window, cx);
1796 assert_eq!(
1797 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1798 editor.selections.display_ranges(cx)
1799 );
1800
1801 // Moving to the end of the line again should be a no-op.
1802 editor.move_to_end_of_line(&move_to_end, window, cx);
1803 assert_eq!(
1804 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1805 editor.selections.display_ranges(cx)
1806 );
1807 });
1808}
1809
1810#[gpui::test]
1811fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1812 init_test(cx, |_| {});
1813
1814 let move_to_beg = MoveToBeginningOfLine {
1815 stop_at_soft_wraps: true,
1816 stop_at_indent: true,
1817 };
1818
1819 let select_to_beg = SelectToBeginningOfLine {
1820 stop_at_soft_wraps: true,
1821 stop_at_indent: true,
1822 };
1823
1824 let delete_to_beg = DeleteToBeginningOfLine {
1825 stop_at_indent: true,
1826 };
1827
1828 let move_to_end = MoveToEndOfLine {
1829 stop_at_soft_wraps: false,
1830 };
1831
1832 let editor = cx.add_window(|window, cx| {
1833 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1834 build_editor(buffer, window, cx)
1835 });
1836
1837 _ = editor.update(cx, |editor, window, cx| {
1838 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1839 s.select_display_ranges([
1840 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1841 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1842 ]);
1843 });
1844
1845 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1846 // and the second cursor at the first non-whitespace character in the line.
1847 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1848 assert_eq!(
1849 editor.selections.display_ranges(cx),
1850 &[
1851 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1852 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1853 ]
1854 );
1855
1856 // Moving to the beginning of the line again should be a no-op for the first cursor,
1857 // and should move the second cursor to the beginning of the line.
1858 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1859 assert_eq!(
1860 editor.selections.display_ranges(cx),
1861 &[
1862 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1863 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1864 ]
1865 );
1866
1867 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1868 // and should move the second cursor back to the first non-whitespace character in the line.
1869 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1870 assert_eq!(
1871 editor.selections.display_ranges(cx),
1872 &[
1873 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1874 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1875 ]
1876 );
1877
1878 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1879 // and to the first non-whitespace character in the line for the second cursor.
1880 editor.move_to_end_of_line(&move_to_end, window, cx);
1881 editor.move_left(&MoveLeft, window, cx);
1882 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1883 assert_eq!(
1884 editor.selections.display_ranges(cx),
1885 &[
1886 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1887 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1888 ]
1889 );
1890
1891 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1892 // and should select to the beginning of the line for the second cursor.
1893 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1894 assert_eq!(
1895 editor.selections.display_ranges(cx),
1896 &[
1897 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1898 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1899 ]
1900 );
1901
1902 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1903 // and should delete to the first non-whitespace character in the line for the second cursor.
1904 editor.move_to_end_of_line(&move_to_end, window, cx);
1905 editor.move_left(&MoveLeft, window, cx);
1906 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1907 assert_eq!(editor.text(cx), "c\n f");
1908 });
1909}
1910
1911#[gpui::test]
1912fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
1913 init_test(cx, |_| {});
1914
1915 let move_to_beg = MoveToBeginningOfLine {
1916 stop_at_soft_wraps: true,
1917 stop_at_indent: true,
1918 };
1919
1920 let editor = cx.add_window(|window, cx| {
1921 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
1922 build_editor(buffer, window, cx)
1923 });
1924
1925 _ = editor.update(cx, |editor, window, cx| {
1926 // test cursor between line_start and indent_start
1927 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1928 s.select_display_ranges([
1929 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
1930 ]);
1931 });
1932
1933 // cursor should move to line_start
1934 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1935 assert_eq!(
1936 editor.selections.display_ranges(cx),
1937 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1938 );
1939
1940 // cursor should move to indent_start
1941 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1942 assert_eq!(
1943 editor.selections.display_ranges(cx),
1944 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
1945 );
1946
1947 // cursor should move to back to line_start
1948 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1949 assert_eq!(
1950 editor.selections.display_ranges(cx),
1951 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1952 );
1953 });
1954}
1955
1956#[gpui::test]
1957fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1958 init_test(cx, |_| {});
1959
1960 let editor = cx.add_window(|window, cx| {
1961 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1962 build_editor(buffer, window, cx)
1963 });
1964 _ = editor.update(cx, |editor, window, cx| {
1965 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1966 s.select_display_ranges([
1967 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1968 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1969 ])
1970 });
1971 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1972 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1973
1974 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1975 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1976
1977 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1978 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1979
1980 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1981 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1982
1983 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1984 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
1985
1986 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1987 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1988
1989 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1990 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1991
1992 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1993 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1994
1995 editor.move_right(&MoveRight, window, cx);
1996 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1997 assert_selection_ranges(
1998 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
1999 editor,
2000 cx,
2001 );
2002
2003 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2004 assert_selection_ranges(
2005 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2006 editor,
2007 cx,
2008 );
2009
2010 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2011 assert_selection_ranges(
2012 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2013 editor,
2014 cx,
2015 );
2016 });
2017}
2018
2019#[gpui::test]
2020fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2021 init_test(cx, |_| {});
2022
2023 let editor = cx.add_window(|window, cx| {
2024 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2025 build_editor(buffer, window, cx)
2026 });
2027
2028 _ = editor.update(cx, |editor, window, cx| {
2029 editor.set_wrap_width(Some(140.0.into()), cx);
2030 assert_eq!(
2031 editor.display_text(cx),
2032 "use one::{\n two::three::\n four::five\n};"
2033 );
2034
2035 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2036 s.select_display_ranges([
2037 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2038 ]);
2039 });
2040
2041 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2042 assert_eq!(
2043 editor.selections.display_ranges(cx),
2044 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2045 );
2046
2047 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2048 assert_eq!(
2049 editor.selections.display_ranges(cx),
2050 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2051 );
2052
2053 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2054 assert_eq!(
2055 editor.selections.display_ranges(cx),
2056 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2057 );
2058
2059 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2060 assert_eq!(
2061 editor.selections.display_ranges(cx),
2062 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2063 );
2064
2065 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2066 assert_eq!(
2067 editor.selections.display_ranges(cx),
2068 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2069 );
2070
2071 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2072 assert_eq!(
2073 editor.selections.display_ranges(cx),
2074 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2075 );
2076 });
2077}
2078
2079#[gpui::test]
2080async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2081 init_test(cx, |_| {});
2082 let mut cx = EditorTestContext::new(cx).await;
2083
2084 let line_height = cx.editor(|editor, window, _| {
2085 editor
2086 .style()
2087 .unwrap()
2088 .text
2089 .line_height_in_pixels(window.rem_size())
2090 });
2091 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2092
2093 cx.set_state(
2094 &r#"ˇone
2095 two
2096
2097 three
2098 fourˇ
2099 five
2100
2101 six"#
2102 .unindent(),
2103 );
2104
2105 cx.update_editor(|editor, window, cx| {
2106 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2107 });
2108 cx.assert_editor_state(
2109 &r#"one
2110 two
2111 ˇ
2112 three
2113 four
2114 five
2115 ˇ
2116 six"#
2117 .unindent(),
2118 );
2119
2120 cx.update_editor(|editor, window, cx| {
2121 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2122 });
2123 cx.assert_editor_state(
2124 &r#"one
2125 two
2126
2127 three
2128 four
2129 five
2130 ˇ
2131 sixˇ"#
2132 .unindent(),
2133 );
2134
2135 cx.update_editor(|editor, window, cx| {
2136 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2137 });
2138 cx.assert_editor_state(
2139 &r#"one
2140 two
2141
2142 three
2143 four
2144 five
2145
2146 sixˇ"#
2147 .unindent(),
2148 );
2149
2150 cx.update_editor(|editor, window, cx| {
2151 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2152 });
2153 cx.assert_editor_state(
2154 &r#"one
2155 two
2156
2157 three
2158 four
2159 five
2160 ˇ
2161 six"#
2162 .unindent(),
2163 );
2164
2165 cx.update_editor(|editor, window, cx| {
2166 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2167 });
2168 cx.assert_editor_state(
2169 &r#"one
2170 two
2171 ˇ
2172 three
2173 four
2174 five
2175
2176 six"#
2177 .unindent(),
2178 );
2179
2180 cx.update_editor(|editor, window, cx| {
2181 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2182 });
2183 cx.assert_editor_state(
2184 &r#"ˇone
2185 two
2186
2187 three
2188 four
2189 five
2190
2191 six"#
2192 .unindent(),
2193 );
2194}
2195
2196#[gpui::test]
2197async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2198 init_test(cx, |_| {});
2199 let mut cx = EditorTestContext::new(cx).await;
2200 let line_height = cx.editor(|editor, window, _| {
2201 editor
2202 .style()
2203 .unwrap()
2204 .text
2205 .line_height_in_pixels(window.rem_size())
2206 });
2207 let window = cx.window;
2208 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2209
2210 cx.set_state(
2211 r#"ˇone
2212 two
2213 three
2214 four
2215 five
2216 six
2217 seven
2218 eight
2219 nine
2220 ten
2221 "#,
2222 );
2223
2224 cx.update_editor(|editor, window, cx| {
2225 assert_eq!(
2226 editor.snapshot(window, cx).scroll_position(),
2227 gpui::Point::new(0., 0.)
2228 );
2229 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2230 assert_eq!(
2231 editor.snapshot(window, cx).scroll_position(),
2232 gpui::Point::new(0., 3.)
2233 );
2234 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2235 assert_eq!(
2236 editor.snapshot(window, cx).scroll_position(),
2237 gpui::Point::new(0., 6.)
2238 );
2239 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2240 assert_eq!(
2241 editor.snapshot(window, cx).scroll_position(),
2242 gpui::Point::new(0., 3.)
2243 );
2244
2245 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2246 assert_eq!(
2247 editor.snapshot(window, cx).scroll_position(),
2248 gpui::Point::new(0., 1.)
2249 );
2250 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2251 assert_eq!(
2252 editor.snapshot(window, cx).scroll_position(),
2253 gpui::Point::new(0., 3.)
2254 );
2255 });
2256}
2257
2258#[gpui::test]
2259async fn test_autoscroll(cx: &mut TestAppContext) {
2260 init_test(cx, |_| {});
2261 let mut cx = EditorTestContext::new(cx).await;
2262
2263 let line_height = cx.update_editor(|editor, window, cx| {
2264 editor.set_vertical_scroll_margin(2, cx);
2265 editor
2266 .style()
2267 .unwrap()
2268 .text
2269 .line_height_in_pixels(window.rem_size())
2270 });
2271 let window = cx.window;
2272 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2273
2274 cx.set_state(
2275 r#"ˇone
2276 two
2277 three
2278 four
2279 five
2280 six
2281 seven
2282 eight
2283 nine
2284 ten
2285 "#,
2286 );
2287 cx.update_editor(|editor, window, cx| {
2288 assert_eq!(
2289 editor.snapshot(window, cx).scroll_position(),
2290 gpui::Point::new(0., 0.0)
2291 );
2292 });
2293
2294 // Add a cursor below the visible area. Since both cursors cannot fit
2295 // on screen, the editor autoscrolls to reveal the newest cursor, and
2296 // allows the vertical scroll margin below that cursor.
2297 cx.update_editor(|editor, window, cx| {
2298 editor.change_selections(Default::default(), window, cx, |selections| {
2299 selections.select_ranges([
2300 Point::new(0, 0)..Point::new(0, 0),
2301 Point::new(6, 0)..Point::new(6, 0),
2302 ]);
2303 })
2304 });
2305 cx.update_editor(|editor, window, cx| {
2306 assert_eq!(
2307 editor.snapshot(window, cx).scroll_position(),
2308 gpui::Point::new(0., 3.0)
2309 );
2310 });
2311
2312 // Move down. The editor cursor scrolls down to track the newest cursor.
2313 cx.update_editor(|editor, window, cx| {
2314 editor.move_down(&Default::default(), window, cx);
2315 });
2316 cx.update_editor(|editor, window, cx| {
2317 assert_eq!(
2318 editor.snapshot(window, cx).scroll_position(),
2319 gpui::Point::new(0., 4.0)
2320 );
2321 });
2322
2323 // Add a cursor above the visible area. Since both cursors fit on screen,
2324 // the editor scrolls to show both.
2325 cx.update_editor(|editor, window, cx| {
2326 editor.change_selections(Default::default(), window, cx, |selections| {
2327 selections.select_ranges([
2328 Point::new(1, 0)..Point::new(1, 0),
2329 Point::new(6, 0)..Point::new(6, 0),
2330 ]);
2331 })
2332 });
2333 cx.update_editor(|editor, window, cx| {
2334 assert_eq!(
2335 editor.snapshot(window, cx).scroll_position(),
2336 gpui::Point::new(0., 1.0)
2337 );
2338 });
2339}
2340
2341#[gpui::test]
2342async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2343 init_test(cx, |_| {});
2344 let mut cx = EditorTestContext::new(cx).await;
2345
2346 let line_height = cx.editor(|editor, window, _cx| {
2347 editor
2348 .style()
2349 .unwrap()
2350 .text
2351 .line_height_in_pixels(window.rem_size())
2352 });
2353 let window = cx.window;
2354 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2355 cx.set_state(
2356 &r#"
2357 ˇone
2358 two
2359 threeˇ
2360 four
2361 five
2362 six
2363 seven
2364 eight
2365 nine
2366 ten
2367 "#
2368 .unindent(),
2369 );
2370
2371 cx.update_editor(|editor, window, cx| {
2372 editor.move_page_down(&MovePageDown::default(), window, cx)
2373 });
2374 cx.assert_editor_state(
2375 &r#"
2376 one
2377 two
2378 three
2379 ˇfour
2380 five
2381 sixˇ
2382 seven
2383 eight
2384 nine
2385 ten
2386 "#
2387 .unindent(),
2388 );
2389
2390 cx.update_editor(|editor, window, cx| {
2391 editor.move_page_down(&MovePageDown::default(), window, cx)
2392 });
2393 cx.assert_editor_state(
2394 &r#"
2395 one
2396 two
2397 three
2398 four
2399 five
2400 six
2401 ˇseven
2402 eight
2403 nineˇ
2404 ten
2405 "#
2406 .unindent(),
2407 );
2408
2409 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2410 cx.assert_editor_state(
2411 &r#"
2412 one
2413 two
2414 three
2415 ˇfour
2416 five
2417 sixˇ
2418 seven
2419 eight
2420 nine
2421 ten
2422 "#
2423 .unindent(),
2424 );
2425
2426 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2427 cx.assert_editor_state(
2428 &r#"
2429 ˇone
2430 two
2431 threeˇ
2432 four
2433 five
2434 six
2435 seven
2436 eight
2437 nine
2438 ten
2439 "#
2440 .unindent(),
2441 );
2442
2443 // Test select collapsing
2444 cx.update_editor(|editor, window, cx| {
2445 editor.move_page_down(&MovePageDown::default(), window, cx);
2446 editor.move_page_down(&MovePageDown::default(), window, cx);
2447 editor.move_page_down(&MovePageDown::default(), window, cx);
2448 });
2449 cx.assert_editor_state(
2450 &r#"
2451 one
2452 two
2453 three
2454 four
2455 five
2456 six
2457 seven
2458 eight
2459 nine
2460 ˇten
2461 ˇ"#
2462 .unindent(),
2463 );
2464}
2465
2466#[gpui::test]
2467async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2468 init_test(cx, |_| {});
2469 let mut cx = EditorTestContext::new(cx).await;
2470 cx.set_state("one «two threeˇ» four");
2471 cx.update_editor(|editor, window, cx| {
2472 editor.delete_to_beginning_of_line(
2473 &DeleteToBeginningOfLine {
2474 stop_at_indent: false,
2475 },
2476 window,
2477 cx,
2478 );
2479 assert_eq!(editor.text(cx), " four");
2480 });
2481}
2482
2483#[gpui::test]
2484async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2485 init_test(cx, |_| {});
2486
2487 let mut cx = EditorTestContext::new(cx).await;
2488
2489 // For an empty selection, the preceding word fragment is deleted.
2490 // For non-empty selections, only selected characters are deleted.
2491 cx.set_state("onˇe two t«hreˇ»e four");
2492 cx.update_editor(|editor, window, cx| {
2493 editor.delete_to_previous_word_start(
2494 &DeleteToPreviousWordStart {
2495 ignore_newlines: false,
2496 ignore_brackets: false,
2497 },
2498 window,
2499 cx,
2500 );
2501 });
2502 cx.assert_editor_state("ˇe two tˇe four");
2503
2504 cx.set_state("e tˇwo te «fˇ»our");
2505 cx.update_editor(|editor, window, cx| {
2506 editor.delete_to_next_word_end(
2507 &DeleteToNextWordEnd {
2508 ignore_newlines: false,
2509 ignore_brackets: false,
2510 },
2511 window,
2512 cx,
2513 );
2514 });
2515 cx.assert_editor_state("e tˇ te ˇour");
2516}
2517
2518#[gpui::test]
2519async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2520 init_test(cx, |_| {});
2521
2522 let mut cx = EditorTestContext::new(cx).await;
2523
2524 cx.set_state("here is some text ˇwith a space");
2525 cx.update_editor(|editor, window, cx| {
2526 editor.delete_to_previous_word_start(
2527 &DeleteToPreviousWordStart {
2528 ignore_newlines: false,
2529 ignore_brackets: true,
2530 },
2531 window,
2532 cx,
2533 );
2534 });
2535 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2536 cx.assert_editor_state("here is some textˇwith a space");
2537
2538 cx.set_state("here is some text ˇwith a space");
2539 cx.update_editor(|editor, window, cx| {
2540 editor.delete_to_previous_word_start(
2541 &DeleteToPreviousWordStart {
2542 ignore_newlines: false,
2543 ignore_brackets: false,
2544 },
2545 window,
2546 cx,
2547 );
2548 });
2549 cx.assert_editor_state("here is some textˇwith a space");
2550
2551 cx.set_state("here is some textˇ with a space");
2552 cx.update_editor(|editor, window, cx| {
2553 editor.delete_to_next_word_end(
2554 &DeleteToNextWordEnd {
2555 ignore_newlines: false,
2556 ignore_brackets: true,
2557 },
2558 window,
2559 cx,
2560 );
2561 });
2562 // Same happens in the other direction.
2563 cx.assert_editor_state("here is some textˇwith a space");
2564
2565 cx.set_state("here is some textˇ with a space");
2566 cx.update_editor(|editor, window, cx| {
2567 editor.delete_to_next_word_end(
2568 &DeleteToNextWordEnd {
2569 ignore_newlines: false,
2570 ignore_brackets: false,
2571 },
2572 window,
2573 cx,
2574 );
2575 });
2576 cx.assert_editor_state("here is some textˇwith a space");
2577
2578 cx.set_state("here is some textˇ with a space");
2579 cx.update_editor(|editor, window, cx| {
2580 editor.delete_to_next_word_end(
2581 &DeleteToNextWordEnd {
2582 ignore_newlines: true,
2583 ignore_brackets: false,
2584 },
2585 window,
2586 cx,
2587 );
2588 });
2589 cx.assert_editor_state("here is some textˇwith a space");
2590 cx.update_editor(|editor, window, cx| {
2591 editor.delete_to_previous_word_start(
2592 &DeleteToPreviousWordStart {
2593 ignore_newlines: true,
2594 ignore_brackets: false,
2595 },
2596 window,
2597 cx,
2598 );
2599 });
2600 cx.assert_editor_state("here is some ˇwith a space");
2601 cx.update_editor(|editor, window, cx| {
2602 editor.delete_to_previous_word_start(
2603 &DeleteToPreviousWordStart {
2604 ignore_newlines: true,
2605 ignore_brackets: false,
2606 },
2607 window,
2608 cx,
2609 );
2610 });
2611 // Single whitespaces are removed with the word behind them.
2612 cx.assert_editor_state("here is ˇwith a space");
2613 cx.update_editor(|editor, window, cx| {
2614 editor.delete_to_previous_word_start(
2615 &DeleteToPreviousWordStart {
2616 ignore_newlines: true,
2617 ignore_brackets: false,
2618 },
2619 window,
2620 cx,
2621 );
2622 });
2623 cx.assert_editor_state("here ˇwith a space");
2624 cx.update_editor(|editor, window, cx| {
2625 editor.delete_to_previous_word_start(
2626 &DeleteToPreviousWordStart {
2627 ignore_newlines: true,
2628 ignore_brackets: false,
2629 },
2630 window,
2631 cx,
2632 );
2633 });
2634 cx.assert_editor_state("ˇwith a space");
2635 cx.update_editor(|editor, window, cx| {
2636 editor.delete_to_previous_word_start(
2637 &DeleteToPreviousWordStart {
2638 ignore_newlines: true,
2639 ignore_brackets: false,
2640 },
2641 window,
2642 cx,
2643 );
2644 });
2645 cx.assert_editor_state("ˇwith a space");
2646 cx.update_editor(|editor, window, cx| {
2647 editor.delete_to_next_word_end(
2648 &DeleteToNextWordEnd {
2649 ignore_newlines: true,
2650 ignore_brackets: false,
2651 },
2652 window,
2653 cx,
2654 );
2655 });
2656 // Same happens in the other direction.
2657 cx.assert_editor_state("ˇ a space");
2658 cx.update_editor(|editor, window, cx| {
2659 editor.delete_to_next_word_end(
2660 &DeleteToNextWordEnd {
2661 ignore_newlines: true,
2662 ignore_brackets: false,
2663 },
2664 window,
2665 cx,
2666 );
2667 });
2668 cx.assert_editor_state("ˇ space");
2669 cx.update_editor(|editor, window, cx| {
2670 editor.delete_to_next_word_end(
2671 &DeleteToNextWordEnd {
2672 ignore_newlines: true,
2673 ignore_brackets: false,
2674 },
2675 window,
2676 cx,
2677 );
2678 });
2679 cx.assert_editor_state("ˇ");
2680 cx.update_editor(|editor, window, cx| {
2681 editor.delete_to_next_word_end(
2682 &DeleteToNextWordEnd {
2683 ignore_newlines: true,
2684 ignore_brackets: false,
2685 },
2686 window,
2687 cx,
2688 );
2689 });
2690 cx.assert_editor_state("ˇ");
2691 cx.update_editor(|editor, window, cx| {
2692 editor.delete_to_previous_word_start(
2693 &DeleteToPreviousWordStart {
2694 ignore_newlines: true,
2695 ignore_brackets: false,
2696 },
2697 window,
2698 cx,
2699 );
2700 });
2701 cx.assert_editor_state("ˇ");
2702}
2703
2704#[gpui::test]
2705async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2706 init_test(cx, |_| {});
2707
2708 let language = Arc::new(
2709 Language::new(
2710 LanguageConfig {
2711 brackets: BracketPairConfig {
2712 pairs: vec![
2713 BracketPair {
2714 start: "\"".to_string(),
2715 end: "\"".to_string(),
2716 close: true,
2717 surround: true,
2718 newline: false,
2719 },
2720 BracketPair {
2721 start: "(".to_string(),
2722 end: ")".to_string(),
2723 close: true,
2724 surround: true,
2725 newline: true,
2726 },
2727 ],
2728 ..BracketPairConfig::default()
2729 },
2730 ..LanguageConfig::default()
2731 },
2732 Some(tree_sitter_rust::LANGUAGE.into()),
2733 )
2734 .with_brackets_query(
2735 r#"
2736 ("(" @open ")" @close)
2737 ("\"" @open "\"" @close)
2738 "#,
2739 )
2740 .unwrap(),
2741 );
2742
2743 let mut cx = EditorTestContext::new(cx).await;
2744 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2745
2746 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2747 cx.update_editor(|editor, window, cx| {
2748 editor.delete_to_previous_word_start(
2749 &DeleteToPreviousWordStart {
2750 ignore_newlines: true,
2751 ignore_brackets: false,
2752 },
2753 window,
2754 cx,
2755 );
2756 });
2757 // Deletion stops before brackets if asked to not ignore them.
2758 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2759 cx.update_editor(|editor, window, cx| {
2760 editor.delete_to_previous_word_start(
2761 &DeleteToPreviousWordStart {
2762 ignore_newlines: true,
2763 ignore_brackets: false,
2764 },
2765 window,
2766 cx,
2767 );
2768 });
2769 // Deletion has to remove a single bracket and then stop again.
2770 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2771
2772 cx.update_editor(|editor, window, cx| {
2773 editor.delete_to_previous_word_start(
2774 &DeleteToPreviousWordStart {
2775 ignore_newlines: true,
2776 ignore_brackets: false,
2777 },
2778 window,
2779 cx,
2780 );
2781 });
2782 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2783
2784 cx.update_editor(|editor, window, cx| {
2785 editor.delete_to_previous_word_start(
2786 &DeleteToPreviousWordStart {
2787 ignore_newlines: true,
2788 ignore_brackets: false,
2789 },
2790 window,
2791 cx,
2792 );
2793 });
2794 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2795
2796 cx.update_editor(|editor, window, cx| {
2797 editor.delete_to_previous_word_start(
2798 &DeleteToPreviousWordStart {
2799 ignore_newlines: true,
2800 ignore_brackets: false,
2801 },
2802 window,
2803 cx,
2804 );
2805 });
2806 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2807
2808 cx.update_editor(|editor, window, cx| {
2809 editor.delete_to_next_word_end(
2810 &DeleteToNextWordEnd {
2811 ignore_newlines: true,
2812 ignore_brackets: false,
2813 },
2814 window,
2815 cx,
2816 );
2817 });
2818 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2819 cx.assert_editor_state(r#"ˇ");"#);
2820
2821 cx.update_editor(|editor, window, cx| {
2822 editor.delete_to_next_word_end(
2823 &DeleteToNextWordEnd {
2824 ignore_newlines: true,
2825 ignore_brackets: false,
2826 },
2827 window,
2828 cx,
2829 );
2830 });
2831 cx.assert_editor_state(r#"ˇ"#);
2832
2833 cx.update_editor(|editor, window, cx| {
2834 editor.delete_to_next_word_end(
2835 &DeleteToNextWordEnd {
2836 ignore_newlines: true,
2837 ignore_brackets: false,
2838 },
2839 window,
2840 cx,
2841 );
2842 });
2843 cx.assert_editor_state(r#"ˇ"#);
2844
2845 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2846 cx.update_editor(|editor, window, cx| {
2847 editor.delete_to_previous_word_start(
2848 &DeleteToPreviousWordStart {
2849 ignore_newlines: true,
2850 ignore_brackets: true,
2851 },
2852 window,
2853 cx,
2854 );
2855 });
2856 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2857}
2858
2859#[gpui::test]
2860fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2861 init_test(cx, |_| {});
2862
2863 let editor = cx.add_window(|window, cx| {
2864 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2865 build_editor(buffer, window, cx)
2866 });
2867 let del_to_prev_word_start = DeleteToPreviousWordStart {
2868 ignore_newlines: false,
2869 ignore_brackets: false,
2870 };
2871 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2872 ignore_newlines: true,
2873 ignore_brackets: false,
2874 };
2875
2876 _ = editor.update(cx, |editor, window, cx| {
2877 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2878 s.select_display_ranges([
2879 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2880 ])
2881 });
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\nthree\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\nthree");
2886 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2887 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2888 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2889 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2890 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2891 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2892 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2893 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2894 });
2895}
2896
2897#[gpui::test]
2898fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2899 init_test(cx, |_| {});
2900
2901 let editor = cx.add_window(|window, cx| {
2902 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2903 build_editor(buffer, window, cx)
2904 });
2905 let del_to_next_word_end = DeleteToNextWordEnd {
2906 ignore_newlines: false,
2907 ignore_brackets: false,
2908 };
2909 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2910 ignore_newlines: true,
2911 ignore_brackets: false,
2912 };
2913
2914 _ = editor.update(cx, |editor, window, cx| {
2915 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2916 s.select_display_ranges([
2917 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2918 ])
2919 });
2920 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2921 assert_eq!(
2922 editor.buffer.read(cx).read(cx).text(),
2923 "one\n two\nthree\n four"
2924 );
2925 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2926 assert_eq!(
2927 editor.buffer.read(cx).read(cx).text(),
2928 "\n two\nthree\n four"
2929 );
2930 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2931 assert_eq!(
2932 editor.buffer.read(cx).read(cx).text(),
2933 "two\nthree\n four"
2934 );
2935 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2936 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n 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(), "\n four");
2939 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2940 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
2941 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2942 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2943 });
2944}
2945
2946#[gpui::test]
2947fn test_newline(cx: &mut TestAppContext) {
2948 init_test(cx, |_| {});
2949
2950 let editor = cx.add_window(|window, cx| {
2951 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2952 build_editor(buffer, window, cx)
2953 });
2954
2955 _ = editor.update(cx, |editor, window, cx| {
2956 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2957 s.select_display_ranges([
2958 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2959 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2960 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2961 ])
2962 });
2963
2964 editor.newline(&Newline, window, cx);
2965 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2966 });
2967}
2968
2969#[gpui::test]
2970fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2971 init_test(cx, |_| {});
2972
2973 let editor = cx.add_window(|window, cx| {
2974 let buffer = MultiBuffer::build_simple(
2975 "
2976 a
2977 b(
2978 X
2979 )
2980 c(
2981 X
2982 )
2983 "
2984 .unindent()
2985 .as_str(),
2986 cx,
2987 );
2988 let mut editor = build_editor(buffer, window, cx);
2989 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2990 s.select_ranges([
2991 Point::new(2, 4)..Point::new(2, 5),
2992 Point::new(5, 4)..Point::new(5, 5),
2993 ])
2994 });
2995 editor
2996 });
2997
2998 _ = editor.update(cx, |editor, window, cx| {
2999 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3000 editor.buffer.update(cx, |buffer, cx| {
3001 buffer.edit(
3002 [
3003 (Point::new(1, 2)..Point::new(3, 0), ""),
3004 (Point::new(4, 2)..Point::new(6, 0), ""),
3005 ],
3006 None,
3007 cx,
3008 );
3009 assert_eq!(
3010 buffer.read(cx).text(),
3011 "
3012 a
3013 b()
3014 c()
3015 "
3016 .unindent()
3017 );
3018 });
3019 assert_eq!(
3020 editor.selections.ranges(cx),
3021 &[
3022 Point::new(1, 2)..Point::new(1, 2),
3023 Point::new(2, 2)..Point::new(2, 2),
3024 ],
3025 );
3026
3027 editor.newline(&Newline, window, cx);
3028 assert_eq!(
3029 editor.text(cx),
3030 "
3031 a
3032 b(
3033 )
3034 c(
3035 )
3036 "
3037 .unindent()
3038 );
3039
3040 // The selections are moved after the inserted newlines
3041 assert_eq!(
3042 editor.selections.ranges(cx),
3043 &[
3044 Point::new(2, 0)..Point::new(2, 0),
3045 Point::new(4, 0)..Point::new(4, 0),
3046 ],
3047 );
3048 });
3049}
3050
3051#[gpui::test]
3052async fn test_newline_above(cx: &mut TestAppContext) {
3053 init_test(cx, |settings| {
3054 settings.defaults.tab_size = NonZeroU32::new(4)
3055 });
3056
3057 let language = Arc::new(
3058 Language::new(
3059 LanguageConfig::default(),
3060 Some(tree_sitter_rust::LANGUAGE.into()),
3061 )
3062 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3063 .unwrap(),
3064 );
3065
3066 let mut cx = EditorTestContext::new(cx).await;
3067 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3068 cx.set_state(indoc! {"
3069 const a: ˇA = (
3070 (ˇ
3071 «const_functionˇ»(ˇ),
3072 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3073 )ˇ
3074 ˇ);ˇ
3075 "});
3076
3077 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3078 cx.assert_editor_state(indoc! {"
3079 ˇ
3080 const a: A = (
3081 ˇ
3082 (
3083 ˇ
3084 ˇ
3085 const_function(),
3086 ˇ
3087 ˇ
3088 ˇ
3089 ˇ
3090 something_else,
3091 ˇ
3092 )
3093 ˇ
3094 ˇ
3095 );
3096 "});
3097}
3098
3099#[gpui::test]
3100async fn test_newline_below(cx: &mut TestAppContext) {
3101 init_test(cx, |settings| {
3102 settings.defaults.tab_size = NonZeroU32::new(4)
3103 });
3104
3105 let language = Arc::new(
3106 Language::new(
3107 LanguageConfig::default(),
3108 Some(tree_sitter_rust::LANGUAGE.into()),
3109 )
3110 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3111 .unwrap(),
3112 );
3113
3114 let mut cx = EditorTestContext::new(cx).await;
3115 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3116 cx.set_state(indoc! {"
3117 const a: ˇA = (
3118 (ˇ
3119 «const_functionˇ»(ˇ),
3120 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3121 )ˇ
3122 ˇ);ˇ
3123 "});
3124
3125 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3126 cx.assert_editor_state(indoc! {"
3127 const a: A = (
3128 ˇ
3129 (
3130 ˇ
3131 const_function(),
3132 ˇ
3133 ˇ
3134 something_else,
3135 ˇ
3136 ˇ
3137 ˇ
3138 ˇ
3139 )
3140 ˇ
3141 );
3142 ˇ
3143 ˇ
3144 "});
3145}
3146
3147#[gpui::test]
3148async fn test_newline_comments(cx: &mut TestAppContext) {
3149 init_test(cx, |settings| {
3150 settings.defaults.tab_size = NonZeroU32::new(4)
3151 });
3152
3153 let language = Arc::new(Language::new(
3154 LanguageConfig {
3155 line_comments: vec!["// ".into()],
3156 ..LanguageConfig::default()
3157 },
3158 None,
3159 ));
3160 {
3161 let mut cx = EditorTestContext::new(cx).await;
3162 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3163 cx.set_state(indoc! {"
3164 // Fooˇ
3165 "});
3166
3167 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3168 cx.assert_editor_state(indoc! {"
3169 // Foo
3170 // ˇ
3171 "});
3172 // Ensure that we add comment prefix when existing line contains space
3173 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3174 cx.assert_editor_state(
3175 indoc! {"
3176 // Foo
3177 //s
3178 // ˇ
3179 "}
3180 .replace("s", " ") // s is used as space placeholder to prevent format on save
3181 .as_str(),
3182 );
3183 // Ensure that we add comment prefix when existing line does not contain space
3184 cx.set_state(indoc! {"
3185 // Foo
3186 //ˇ
3187 "});
3188 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3189 cx.assert_editor_state(indoc! {"
3190 // Foo
3191 //
3192 // ˇ
3193 "});
3194 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3195 cx.set_state(indoc! {"
3196 ˇ// Foo
3197 "});
3198 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3199 cx.assert_editor_state(indoc! {"
3200
3201 ˇ// Foo
3202 "});
3203 }
3204 // Ensure that comment continuations can be disabled.
3205 update_test_language_settings(cx, |settings| {
3206 settings.defaults.extend_comment_on_newline = Some(false);
3207 });
3208 let mut cx = EditorTestContext::new(cx).await;
3209 cx.set_state(indoc! {"
3210 // Fooˇ
3211 "});
3212 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3213 cx.assert_editor_state(indoc! {"
3214 // Foo
3215 ˇ
3216 "});
3217}
3218
3219#[gpui::test]
3220async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3221 init_test(cx, |settings| {
3222 settings.defaults.tab_size = NonZeroU32::new(4)
3223 });
3224
3225 let language = Arc::new(Language::new(
3226 LanguageConfig {
3227 line_comments: vec!["// ".into(), "/// ".into()],
3228 ..LanguageConfig::default()
3229 },
3230 None,
3231 ));
3232 {
3233 let mut cx = EditorTestContext::new(cx).await;
3234 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3235 cx.set_state(indoc! {"
3236 //ˇ
3237 "});
3238 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3239 cx.assert_editor_state(indoc! {"
3240 //
3241 // ˇ
3242 "});
3243
3244 cx.set_state(indoc! {"
3245 ///ˇ
3246 "});
3247 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3248 cx.assert_editor_state(indoc! {"
3249 ///
3250 /// ˇ
3251 "});
3252 }
3253}
3254
3255#[gpui::test]
3256async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3257 init_test(cx, |settings| {
3258 settings.defaults.tab_size = NonZeroU32::new(4)
3259 });
3260
3261 let language = Arc::new(
3262 Language::new(
3263 LanguageConfig {
3264 documentation_comment: Some(language::BlockCommentConfig {
3265 start: "/**".into(),
3266 end: "*/".into(),
3267 prefix: "* ".into(),
3268 tab_size: 1,
3269 }),
3270
3271 ..LanguageConfig::default()
3272 },
3273 Some(tree_sitter_rust::LANGUAGE.into()),
3274 )
3275 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3276 .unwrap(),
3277 );
3278
3279 {
3280 let mut cx = EditorTestContext::new(cx).await;
3281 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3282 cx.set_state(indoc! {"
3283 /**ˇ
3284 "});
3285
3286 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3287 cx.assert_editor_state(indoc! {"
3288 /**
3289 * ˇ
3290 "});
3291 // Ensure that if cursor is before the comment start,
3292 // we do not actually insert a comment prefix.
3293 cx.set_state(indoc! {"
3294 ˇ/**
3295 "});
3296 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3297 cx.assert_editor_state(indoc! {"
3298
3299 ˇ/**
3300 "});
3301 // Ensure that if cursor is between it doesn't add comment prefix.
3302 cx.set_state(indoc! {"
3303 /*ˇ*
3304 "});
3305 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3306 cx.assert_editor_state(indoc! {"
3307 /*
3308 ˇ*
3309 "});
3310 // Ensure that if suffix exists on same line after cursor it adds new line.
3311 cx.set_state(indoc! {"
3312 /**ˇ*/
3313 "});
3314 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3315 cx.assert_editor_state(indoc! {"
3316 /**
3317 * ˇ
3318 */
3319 "});
3320 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3321 cx.set_state(indoc! {"
3322 /**ˇ */
3323 "});
3324 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3325 cx.assert_editor_state(indoc! {"
3326 /**
3327 * ˇ
3328 */
3329 "});
3330 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3331 cx.set_state(indoc! {"
3332 /** ˇ*/
3333 "});
3334 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3335 cx.assert_editor_state(
3336 indoc! {"
3337 /**s
3338 * ˇ
3339 */
3340 "}
3341 .replace("s", " ") // s is used as space placeholder to prevent format on save
3342 .as_str(),
3343 );
3344 // Ensure that delimiter space is preserved when newline on already
3345 // spaced delimiter.
3346 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3347 cx.assert_editor_state(
3348 indoc! {"
3349 /**s
3350 *s
3351 * ˇ
3352 */
3353 "}
3354 .replace("s", " ") // s is used as space placeholder to prevent format on save
3355 .as_str(),
3356 );
3357 // Ensure that delimiter space is preserved when space is not
3358 // on existing delimiter.
3359 cx.set_state(indoc! {"
3360 /**
3361 *ˇ
3362 */
3363 "});
3364 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3365 cx.assert_editor_state(indoc! {"
3366 /**
3367 *
3368 * ˇ
3369 */
3370 "});
3371 // Ensure that if suffix exists on same line after cursor it
3372 // doesn't add extra new line if prefix is not on same line.
3373 cx.set_state(indoc! {"
3374 /**
3375 ˇ*/
3376 "});
3377 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3378 cx.assert_editor_state(indoc! {"
3379 /**
3380
3381 ˇ*/
3382 "});
3383 // Ensure that it detects suffix after existing prefix.
3384 cx.set_state(indoc! {"
3385 /**ˇ/
3386 "});
3387 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3388 cx.assert_editor_state(indoc! {"
3389 /**
3390 ˇ/
3391 "});
3392 // Ensure that if suffix exists on same line before
3393 // cursor it does not add comment prefix.
3394 cx.set_state(indoc! {"
3395 /** */ˇ
3396 "});
3397 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3398 cx.assert_editor_state(indoc! {"
3399 /** */
3400 ˇ
3401 "});
3402 // Ensure that if suffix exists on same line before
3403 // cursor it does not add comment prefix.
3404 cx.set_state(indoc! {"
3405 /**
3406 *
3407 */ˇ
3408 "});
3409 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3410 cx.assert_editor_state(indoc! {"
3411 /**
3412 *
3413 */
3414 ˇ
3415 "});
3416
3417 // Ensure that inline comment followed by code
3418 // doesn't add comment prefix on newline
3419 cx.set_state(indoc! {"
3420 /** */ textˇ
3421 "});
3422 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3423 cx.assert_editor_state(indoc! {"
3424 /** */ text
3425 ˇ
3426 "});
3427
3428 // Ensure that text after comment end tag
3429 // doesn't add comment prefix on newline
3430 cx.set_state(indoc! {"
3431 /**
3432 *
3433 */ˇtext
3434 "});
3435 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3436 cx.assert_editor_state(indoc! {"
3437 /**
3438 *
3439 */
3440 ˇtext
3441 "});
3442
3443 // Ensure if not comment block it doesn't
3444 // add comment prefix on newline
3445 cx.set_state(indoc! {"
3446 * textˇ
3447 "});
3448 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3449 cx.assert_editor_state(indoc! {"
3450 * text
3451 ˇ
3452 "});
3453 }
3454 // Ensure that comment continuations can be disabled.
3455 update_test_language_settings(cx, |settings| {
3456 settings.defaults.extend_comment_on_newline = Some(false);
3457 });
3458 let mut cx = EditorTestContext::new(cx).await;
3459 cx.set_state(indoc! {"
3460 /**ˇ
3461 "});
3462 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3463 cx.assert_editor_state(indoc! {"
3464 /**
3465 ˇ
3466 "});
3467}
3468
3469#[gpui::test]
3470async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3471 init_test(cx, |settings| {
3472 settings.defaults.tab_size = NonZeroU32::new(4)
3473 });
3474
3475 let lua_language = Arc::new(Language::new(
3476 LanguageConfig {
3477 line_comments: vec!["--".into()],
3478 block_comment: Some(language::BlockCommentConfig {
3479 start: "--[[".into(),
3480 prefix: "".into(),
3481 end: "]]".into(),
3482 tab_size: 0,
3483 }),
3484 ..LanguageConfig::default()
3485 },
3486 None,
3487 ));
3488
3489 let mut cx = EditorTestContext::new(cx).await;
3490 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3491
3492 // Line with line comment should extend
3493 cx.set_state(indoc! {"
3494 --ˇ
3495 "});
3496 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3497 cx.assert_editor_state(indoc! {"
3498 --
3499 --ˇ
3500 "});
3501
3502 // Line with block comment that matches line comment should not extend
3503 cx.set_state(indoc! {"
3504 --[[ˇ
3505 "});
3506 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3507 cx.assert_editor_state(indoc! {"
3508 --[[
3509 ˇ
3510 "});
3511}
3512
3513#[gpui::test]
3514fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3515 init_test(cx, |_| {});
3516
3517 let editor = cx.add_window(|window, cx| {
3518 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3519 let mut editor = build_editor(buffer, window, cx);
3520 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3521 s.select_ranges([3..4, 11..12, 19..20])
3522 });
3523 editor
3524 });
3525
3526 _ = editor.update(cx, |editor, window, cx| {
3527 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3528 editor.buffer.update(cx, |buffer, cx| {
3529 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3530 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3531 });
3532 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3533
3534 editor.insert("Z", window, cx);
3535 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3536
3537 // The selections are moved after the inserted characters
3538 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3539 });
3540}
3541
3542#[gpui::test]
3543async fn test_tab(cx: &mut TestAppContext) {
3544 init_test(cx, |settings| {
3545 settings.defaults.tab_size = NonZeroU32::new(3)
3546 });
3547
3548 let mut cx = EditorTestContext::new(cx).await;
3549 cx.set_state(indoc! {"
3550 ˇabˇc
3551 ˇ🏀ˇ🏀ˇefg
3552 dˇ
3553 "});
3554 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3555 cx.assert_editor_state(indoc! {"
3556 ˇab ˇc
3557 ˇ🏀 ˇ🏀 ˇefg
3558 d ˇ
3559 "});
3560
3561 cx.set_state(indoc! {"
3562 a
3563 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3564 "});
3565 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3566 cx.assert_editor_state(indoc! {"
3567 a
3568 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3569 "});
3570}
3571
3572#[gpui::test]
3573async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3574 init_test(cx, |_| {});
3575
3576 let mut cx = EditorTestContext::new(cx).await;
3577 let language = Arc::new(
3578 Language::new(
3579 LanguageConfig::default(),
3580 Some(tree_sitter_rust::LANGUAGE.into()),
3581 )
3582 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3583 .unwrap(),
3584 );
3585 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3586
3587 // test when all cursors are not at suggested indent
3588 // then simply move to their suggested indent location
3589 cx.set_state(indoc! {"
3590 const a: B = (
3591 c(
3592 ˇ
3593 ˇ )
3594 );
3595 "});
3596 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3597 cx.assert_editor_state(indoc! {"
3598 const a: B = (
3599 c(
3600 ˇ
3601 ˇ)
3602 );
3603 "});
3604
3605 // test cursor already at suggested indent not moving when
3606 // other cursors are yet to reach their suggested indents
3607 cx.set_state(indoc! {"
3608 ˇ
3609 const a: B = (
3610 c(
3611 d(
3612 ˇ
3613 )
3614 ˇ
3615 ˇ )
3616 );
3617 "});
3618 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3619 cx.assert_editor_state(indoc! {"
3620 ˇ
3621 const a: B = (
3622 c(
3623 d(
3624 ˇ
3625 )
3626 ˇ
3627 ˇ)
3628 );
3629 "});
3630 // test when all cursors are at suggested indent then tab is inserted
3631 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3632 cx.assert_editor_state(indoc! {"
3633 ˇ
3634 const a: B = (
3635 c(
3636 d(
3637 ˇ
3638 )
3639 ˇ
3640 ˇ)
3641 );
3642 "});
3643
3644 // test when current indent is less than suggested indent,
3645 // we adjust line to match suggested indent and move cursor to it
3646 //
3647 // when no other cursor is at word boundary, all of them should move
3648 cx.set_state(indoc! {"
3649 const a: B = (
3650 c(
3651 d(
3652 ˇ
3653 ˇ )
3654 ˇ )
3655 );
3656 "});
3657 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3658 cx.assert_editor_state(indoc! {"
3659 const a: B = (
3660 c(
3661 d(
3662 ˇ
3663 ˇ)
3664 ˇ)
3665 );
3666 "});
3667
3668 // test when current indent is less than suggested indent,
3669 // we adjust line to match suggested indent and move cursor to it
3670 //
3671 // when some other cursor is at word boundary, it should not move
3672 cx.set_state(indoc! {"
3673 const a: B = (
3674 c(
3675 d(
3676 ˇ
3677 ˇ )
3678 ˇ)
3679 );
3680 "});
3681 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3682 cx.assert_editor_state(indoc! {"
3683 const a: B = (
3684 c(
3685 d(
3686 ˇ
3687 ˇ)
3688 ˇ)
3689 );
3690 "});
3691
3692 // test when current indent is more than suggested indent,
3693 // we just move cursor to current indent instead of suggested indent
3694 //
3695 // when no other cursor is at word boundary, all of them should move
3696 cx.set_state(indoc! {"
3697 const a: B = (
3698 c(
3699 d(
3700 ˇ
3701 ˇ )
3702 ˇ )
3703 );
3704 "});
3705 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3706 cx.assert_editor_state(indoc! {"
3707 const a: B = (
3708 c(
3709 d(
3710 ˇ
3711 ˇ)
3712 ˇ)
3713 );
3714 "});
3715 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3716 cx.assert_editor_state(indoc! {"
3717 const a: B = (
3718 c(
3719 d(
3720 ˇ
3721 ˇ)
3722 ˇ)
3723 );
3724 "});
3725
3726 // test when current indent is more than suggested indent,
3727 // we just move cursor to current indent instead of suggested indent
3728 //
3729 // when some other cursor is at word boundary, it doesn't move
3730 cx.set_state(indoc! {"
3731 const a: B = (
3732 c(
3733 d(
3734 ˇ
3735 ˇ )
3736 ˇ)
3737 );
3738 "});
3739 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3740 cx.assert_editor_state(indoc! {"
3741 const a: B = (
3742 c(
3743 d(
3744 ˇ
3745 ˇ)
3746 ˇ)
3747 );
3748 "});
3749
3750 // handle auto-indent when there are multiple cursors on the same line
3751 cx.set_state(indoc! {"
3752 const a: B = (
3753 c(
3754 ˇ ˇ
3755 ˇ )
3756 );
3757 "});
3758 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3759 cx.assert_editor_state(indoc! {"
3760 const a: B = (
3761 c(
3762 ˇ
3763 ˇ)
3764 );
3765 "});
3766}
3767
3768#[gpui::test]
3769async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3770 init_test(cx, |settings| {
3771 settings.defaults.tab_size = NonZeroU32::new(3)
3772 });
3773
3774 let mut cx = EditorTestContext::new(cx).await;
3775 cx.set_state(indoc! {"
3776 ˇ
3777 \t ˇ
3778 \t ˇ
3779 \t ˇ
3780 \t \t\t \t \t\t \t\t \t \t ˇ
3781 "});
3782
3783 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3784 cx.assert_editor_state(indoc! {"
3785 ˇ
3786 \t ˇ
3787 \t ˇ
3788 \t ˇ
3789 \t \t\t \t \t\t \t\t \t \t ˇ
3790 "});
3791}
3792
3793#[gpui::test]
3794async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3795 init_test(cx, |settings| {
3796 settings.defaults.tab_size = NonZeroU32::new(4)
3797 });
3798
3799 let language = Arc::new(
3800 Language::new(
3801 LanguageConfig::default(),
3802 Some(tree_sitter_rust::LANGUAGE.into()),
3803 )
3804 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3805 .unwrap(),
3806 );
3807
3808 let mut cx = EditorTestContext::new(cx).await;
3809 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3810 cx.set_state(indoc! {"
3811 fn a() {
3812 if b {
3813 \t ˇc
3814 }
3815 }
3816 "});
3817
3818 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3819 cx.assert_editor_state(indoc! {"
3820 fn a() {
3821 if b {
3822 ˇc
3823 }
3824 }
3825 "});
3826}
3827
3828#[gpui::test]
3829async fn test_indent_outdent(cx: &mut TestAppContext) {
3830 init_test(cx, |settings| {
3831 settings.defaults.tab_size = NonZeroU32::new(4);
3832 });
3833
3834 let mut cx = EditorTestContext::new(cx).await;
3835
3836 cx.set_state(indoc! {"
3837 «oneˇ» «twoˇ»
3838 three
3839 four
3840 "});
3841 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3842 cx.assert_editor_state(indoc! {"
3843 «oneˇ» «twoˇ»
3844 three
3845 four
3846 "});
3847
3848 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3849 cx.assert_editor_state(indoc! {"
3850 «oneˇ» «twoˇ»
3851 three
3852 four
3853 "});
3854
3855 // select across line ending
3856 cx.set_state(indoc! {"
3857 one two
3858 t«hree
3859 ˇ» four
3860 "});
3861 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3862 cx.assert_editor_state(indoc! {"
3863 one two
3864 t«hree
3865 ˇ» four
3866 "});
3867
3868 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3869 cx.assert_editor_state(indoc! {"
3870 one two
3871 t«hree
3872 ˇ» four
3873 "});
3874
3875 // Ensure that indenting/outdenting works when the cursor is at column 0.
3876 cx.set_state(indoc! {"
3877 one two
3878 ˇthree
3879 four
3880 "});
3881 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3882 cx.assert_editor_state(indoc! {"
3883 one two
3884 ˇthree
3885 four
3886 "});
3887
3888 cx.set_state(indoc! {"
3889 one two
3890 ˇ three
3891 four
3892 "});
3893 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3894 cx.assert_editor_state(indoc! {"
3895 one two
3896 ˇthree
3897 four
3898 "});
3899}
3900
3901#[gpui::test]
3902async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3903 // This is a regression test for issue #33761
3904 init_test(cx, |_| {});
3905
3906 let mut cx = EditorTestContext::new(cx).await;
3907 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3908 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3909
3910 cx.set_state(
3911 r#"ˇ# ingress:
3912ˇ# api:
3913ˇ# enabled: false
3914ˇ# pathType: Prefix
3915ˇ# console:
3916ˇ# enabled: false
3917ˇ# pathType: Prefix
3918"#,
3919 );
3920
3921 // Press tab to indent all lines
3922 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3923
3924 cx.assert_editor_state(
3925 r#" ˇ# ingress:
3926 ˇ# api:
3927 ˇ# enabled: false
3928 ˇ# pathType: Prefix
3929 ˇ# console:
3930 ˇ# enabled: false
3931 ˇ# pathType: Prefix
3932"#,
3933 );
3934}
3935
3936#[gpui::test]
3937async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
3938 // This is a test to make sure our fix for issue #33761 didn't break anything
3939 init_test(cx, |_| {});
3940
3941 let mut cx = EditorTestContext::new(cx).await;
3942 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3943 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3944
3945 cx.set_state(
3946 r#"ˇingress:
3947ˇ api:
3948ˇ enabled: false
3949ˇ pathType: Prefix
3950"#,
3951 );
3952
3953 // Press tab to indent all lines
3954 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3955
3956 cx.assert_editor_state(
3957 r#"ˇingress:
3958 ˇapi:
3959 ˇenabled: false
3960 ˇpathType: Prefix
3961"#,
3962 );
3963}
3964
3965#[gpui::test]
3966async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3967 init_test(cx, |settings| {
3968 settings.defaults.hard_tabs = Some(true);
3969 });
3970
3971 let mut cx = EditorTestContext::new(cx).await;
3972
3973 // select two ranges on one line
3974 cx.set_state(indoc! {"
3975 «oneˇ» «twoˇ»
3976 three
3977 four
3978 "});
3979 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3980 cx.assert_editor_state(indoc! {"
3981 \t«oneˇ» «twoˇ»
3982 three
3983 four
3984 "});
3985 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3986 cx.assert_editor_state(indoc! {"
3987 \t\t«oneˇ» «twoˇ»
3988 three
3989 four
3990 "});
3991 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3992 cx.assert_editor_state(indoc! {"
3993 \t«oneˇ» «twoˇ»
3994 three
3995 four
3996 "});
3997 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3998 cx.assert_editor_state(indoc! {"
3999 «oneˇ» «twoˇ»
4000 three
4001 four
4002 "});
4003
4004 // select across a line ending
4005 cx.set_state(indoc! {"
4006 one two
4007 t«hree
4008 ˇ»four
4009 "});
4010 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4011 cx.assert_editor_state(indoc! {"
4012 one two
4013 \tt«hree
4014 ˇ»four
4015 "});
4016 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4017 cx.assert_editor_state(indoc! {"
4018 one two
4019 \t\tt«hree
4020 ˇ»four
4021 "});
4022 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4023 cx.assert_editor_state(indoc! {"
4024 one two
4025 \tt«hree
4026 ˇ»four
4027 "});
4028 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4029 cx.assert_editor_state(indoc! {"
4030 one two
4031 t«hree
4032 ˇ»four
4033 "});
4034
4035 // Ensure that indenting/outdenting works when the cursor is at column 0.
4036 cx.set_state(indoc! {"
4037 one two
4038 ˇthree
4039 four
4040 "});
4041 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4042 cx.assert_editor_state(indoc! {"
4043 one two
4044 ˇthree
4045 four
4046 "});
4047 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4048 cx.assert_editor_state(indoc! {"
4049 one two
4050 \tˇthree
4051 four
4052 "});
4053 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4054 cx.assert_editor_state(indoc! {"
4055 one two
4056 ˇthree
4057 four
4058 "});
4059}
4060
4061#[gpui::test]
4062fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4063 init_test(cx, |settings| {
4064 settings.languages.0.extend([
4065 (
4066 "TOML".into(),
4067 LanguageSettingsContent {
4068 tab_size: NonZeroU32::new(2),
4069 ..Default::default()
4070 },
4071 ),
4072 (
4073 "Rust".into(),
4074 LanguageSettingsContent {
4075 tab_size: NonZeroU32::new(4),
4076 ..Default::default()
4077 },
4078 ),
4079 ]);
4080 });
4081
4082 let toml_language = Arc::new(Language::new(
4083 LanguageConfig {
4084 name: "TOML".into(),
4085 ..Default::default()
4086 },
4087 None,
4088 ));
4089 let rust_language = Arc::new(Language::new(
4090 LanguageConfig {
4091 name: "Rust".into(),
4092 ..Default::default()
4093 },
4094 None,
4095 ));
4096
4097 let toml_buffer =
4098 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4099 let rust_buffer =
4100 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4101 let multibuffer = cx.new(|cx| {
4102 let mut multibuffer = MultiBuffer::new(ReadWrite);
4103 multibuffer.push_excerpts(
4104 toml_buffer.clone(),
4105 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4106 cx,
4107 );
4108 multibuffer.push_excerpts(
4109 rust_buffer.clone(),
4110 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4111 cx,
4112 );
4113 multibuffer
4114 });
4115
4116 cx.add_window(|window, cx| {
4117 let mut editor = build_editor(multibuffer, window, cx);
4118
4119 assert_eq!(
4120 editor.text(cx),
4121 indoc! {"
4122 a = 1
4123 b = 2
4124
4125 const c: usize = 3;
4126 "}
4127 );
4128
4129 select_ranges(
4130 &mut editor,
4131 indoc! {"
4132 «aˇ» = 1
4133 b = 2
4134
4135 «const c:ˇ» usize = 3;
4136 "},
4137 window,
4138 cx,
4139 );
4140
4141 editor.tab(&Tab, window, cx);
4142 assert_text_with_selections(
4143 &mut editor,
4144 indoc! {"
4145 «aˇ» = 1
4146 b = 2
4147
4148 «const c:ˇ» usize = 3;
4149 "},
4150 cx,
4151 );
4152 editor.backtab(&Backtab, window, cx);
4153 assert_text_with_selections(
4154 &mut editor,
4155 indoc! {"
4156 «aˇ» = 1
4157 b = 2
4158
4159 «const c:ˇ» usize = 3;
4160 "},
4161 cx,
4162 );
4163
4164 editor
4165 });
4166}
4167
4168#[gpui::test]
4169async fn test_backspace(cx: &mut TestAppContext) {
4170 init_test(cx, |_| {});
4171
4172 let mut cx = EditorTestContext::new(cx).await;
4173
4174 // Basic backspace
4175 cx.set_state(indoc! {"
4176 onˇe two three
4177 fou«rˇ» five six
4178 seven «ˇeight nine
4179 »ten
4180 "});
4181 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4182 cx.assert_editor_state(indoc! {"
4183 oˇe two three
4184 fouˇ five six
4185 seven ˇten
4186 "});
4187
4188 // Test backspace inside and around indents
4189 cx.set_state(indoc! {"
4190 zero
4191 ˇone
4192 ˇtwo
4193 ˇ ˇ ˇ three
4194 ˇ ˇ four
4195 "});
4196 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4197 cx.assert_editor_state(indoc! {"
4198 zero
4199 ˇone
4200 ˇtwo
4201 ˇ threeˇ four
4202 "});
4203}
4204
4205#[gpui::test]
4206async fn test_delete(cx: &mut TestAppContext) {
4207 init_test(cx, |_| {});
4208
4209 let mut cx = EditorTestContext::new(cx).await;
4210 cx.set_state(indoc! {"
4211 onˇe two three
4212 fou«rˇ» five six
4213 seven «ˇeight nine
4214 »ten
4215 "});
4216 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4217 cx.assert_editor_state(indoc! {"
4218 onˇ two three
4219 fouˇ five six
4220 seven ˇten
4221 "});
4222}
4223
4224#[gpui::test]
4225fn test_delete_line(cx: &mut TestAppContext) {
4226 init_test(cx, |_| {});
4227
4228 let editor = cx.add_window(|window, cx| {
4229 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4230 build_editor(buffer, window, cx)
4231 });
4232 _ = editor.update(cx, |editor, window, cx| {
4233 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4234 s.select_display_ranges([
4235 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4236 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4237 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4238 ])
4239 });
4240 editor.delete_line(&DeleteLine, window, cx);
4241 assert_eq!(editor.display_text(cx), "ghi");
4242 assert_eq!(
4243 editor.selections.display_ranges(cx),
4244 vec![
4245 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4246 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
4247 ]
4248 );
4249 });
4250
4251 let editor = cx.add_window(|window, cx| {
4252 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4253 build_editor(buffer, window, cx)
4254 });
4255 _ = editor.update(cx, |editor, window, cx| {
4256 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4257 s.select_display_ranges([
4258 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4259 ])
4260 });
4261 editor.delete_line(&DeleteLine, window, cx);
4262 assert_eq!(editor.display_text(cx), "ghi\n");
4263 assert_eq!(
4264 editor.selections.display_ranges(cx),
4265 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4266 );
4267 });
4268}
4269
4270#[gpui::test]
4271fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4272 init_test(cx, |_| {});
4273
4274 cx.add_window(|window, cx| {
4275 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4276 let mut editor = build_editor(buffer.clone(), window, cx);
4277 let buffer = buffer.read(cx).as_singleton().unwrap();
4278
4279 assert_eq!(
4280 editor.selections.ranges::<Point>(cx),
4281 &[Point::new(0, 0)..Point::new(0, 0)]
4282 );
4283
4284 // When on single line, replace newline at end by space
4285 editor.join_lines(&JoinLines, window, cx);
4286 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4287 assert_eq!(
4288 editor.selections.ranges::<Point>(cx),
4289 &[Point::new(0, 3)..Point::new(0, 3)]
4290 );
4291
4292 // When multiple lines are selected, remove newlines that are spanned by the selection
4293 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4294 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4295 });
4296 editor.join_lines(&JoinLines, window, cx);
4297 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4298 assert_eq!(
4299 editor.selections.ranges::<Point>(cx),
4300 &[Point::new(0, 11)..Point::new(0, 11)]
4301 );
4302
4303 // Undo should be transactional
4304 editor.undo(&Undo, window, cx);
4305 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4306 assert_eq!(
4307 editor.selections.ranges::<Point>(cx),
4308 &[Point::new(0, 5)..Point::new(2, 2)]
4309 );
4310
4311 // When joining an empty line don't insert a space
4312 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4313 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4314 });
4315 editor.join_lines(&JoinLines, window, cx);
4316 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4317 assert_eq!(
4318 editor.selections.ranges::<Point>(cx),
4319 [Point::new(2, 3)..Point::new(2, 3)]
4320 );
4321
4322 // We can remove trailing newlines
4323 editor.join_lines(&JoinLines, window, cx);
4324 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4325 assert_eq!(
4326 editor.selections.ranges::<Point>(cx),
4327 [Point::new(2, 3)..Point::new(2, 3)]
4328 );
4329
4330 // We don't blow up on the last line
4331 editor.join_lines(&JoinLines, window, cx);
4332 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4333 assert_eq!(
4334 editor.selections.ranges::<Point>(cx),
4335 [Point::new(2, 3)..Point::new(2, 3)]
4336 );
4337
4338 // reset to test indentation
4339 editor.buffer.update(cx, |buffer, cx| {
4340 buffer.edit(
4341 [
4342 (Point::new(1, 0)..Point::new(1, 2), " "),
4343 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4344 ],
4345 None,
4346 cx,
4347 )
4348 });
4349
4350 // We remove any leading spaces
4351 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4352 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4353 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4354 });
4355 editor.join_lines(&JoinLines, window, cx);
4356 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4357
4358 // We don't insert a space for a line containing only spaces
4359 editor.join_lines(&JoinLines, window, cx);
4360 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4361
4362 // We ignore any leading tabs
4363 editor.join_lines(&JoinLines, window, cx);
4364 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4365
4366 editor
4367 });
4368}
4369
4370#[gpui::test]
4371fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4372 init_test(cx, |_| {});
4373
4374 cx.add_window(|window, cx| {
4375 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4376 let mut editor = build_editor(buffer.clone(), window, cx);
4377 let buffer = buffer.read(cx).as_singleton().unwrap();
4378
4379 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4380 s.select_ranges([
4381 Point::new(0, 2)..Point::new(1, 1),
4382 Point::new(1, 2)..Point::new(1, 2),
4383 Point::new(3, 1)..Point::new(3, 2),
4384 ])
4385 });
4386
4387 editor.join_lines(&JoinLines, window, cx);
4388 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4389
4390 assert_eq!(
4391 editor.selections.ranges::<Point>(cx),
4392 [
4393 Point::new(0, 7)..Point::new(0, 7),
4394 Point::new(1, 3)..Point::new(1, 3)
4395 ]
4396 );
4397 editor
4398 });
4399}
4400
4401#[gpui::test]
4402async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4403 init_test(cx, |_| {});
4404
4405 let mut cx = EditorTestContext::new(cx).await;
4406
4407 let diff_base = r#"
4408 Line 0
4409 Line 1
4410 Line 2
4411 Line 3
4412 "#
4413 .unindent();
4414
4415 cx.set_state(
4416 &r#"
4417 ˇLine 0
4418 Line 1
4419 Line 2
4420 Line 3
4421 "#
4422 .unindent(),
4423 );
4424
4425 cx.set_head_text(&diff_base);
4426 executor.run_until_parked();
4427
4428 // Join lines
4429 cx.update_editor(|editor, window, cx| {
4430 editor.join_lines(&JoinLines, window, cx);
4431 });
4432 executor.run_until_parked();
4433
4434 cx.assert_editor_state(
4435 &r#"
4436 Line 0ˇ Line 1
4437 Line 2
4438 Line 3
4439 "#
4440 .unindent(),
4441 );
4442 // Join again
4443 cx.update_editor(|editor, window, cx| {
4444 editor.join_lines(&JoinLines, window, cx);
4445 });
4446 executor.run_until_parked();
4447
4448 cx.assert_editor_state(
4449 &r#"
4450 Line 0 Line 1ˇ Line 2
4451 Line 3
4452 "#
4453 .unindent(),
4454 );
4455}
4456
4457#[gpui::test]
4458async fn test_custom_newlines_cause_no_false_positive_diffs(
4459 executor: BackgroundExecutor,
4460 cx: &mut TestAppContext,
4461) {
4462 init_test(cx, |_| {});
4463 let mut cx = EditorTestContext::new(cx).await;
4464 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4465 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4466 executor.run_until_parked();
4467
4468 cx.update_editor(|editor, window, cx| {
4469 let snapshot = editor.snapshot(window, cx);
4470 assert_eq!(
4471 snapshot
4472 .buffer_snapshot
4473 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
4474 .collect::<Vec<_>>(),
4475 Vec::new(),
4476 "Should not have any diffs for files with custom newlines"
4477 );
4478 });
4479}
4480
4481#[gpui::test]
4482async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4483 init_test(cx, |_| {});
4484
4485 let mut cx = EditorTestContext::new(cx).await;
4486
4487 // Test sort_lines_case_insensitive()
4488 cx.set_state(indoc! {"
4489 «z
4490 y
4491 x
4492 Z
4493 Y
4494 Xˇ»
4495 "});
4496 cx.update_editor(|e, window, cx| {
4497 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4498 });
4499 cx.assert_editor_state(indoc! {"
4500 «x
4501 X
4502 y
4503 Y
4504 z
4505 Zˇ»
4506 "});
4507
4508 // Test sort_lines_by_length()
4509 //
4510 // Demonstrates:
4511 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4512 // - sort is stable
4513 cx.set_state(indoc! {"
4514 «123
4515 æ
4516 12
4517 ∞
4518 1
4519 æˇ»
4520 "});
4521 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4522 cx.assert_editor_state(indoc! {"
4523 «æ
4524 ∞
4525 1
4526 æ
4527 12
4528 123ˇ»
4529 "});
4530
4531 // Test reverse_lines()
4532 cx.set_state(indoc! {"
4533 «5
4534 4
4535 3
4536 2
4537 1ˇ»
4538 "});
4539 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4540 cx.assert_editor_state(indoc! {"
4541 «1
4542 2
4543 3
4544 4
4545 5ˇ»
4546 "});
4547
4548 // Skip testing shuffle_line()
4549
4550 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4551 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4552
4553 // Don't manipulate when cursor is on single line, but expand the selection
4554 cx.set_state(indoc! {"
4555 ddˇdd
4556 ccc
4557 bb
4558 a
4559 "});
4560 cx.update_editor(|e, window, cx| {
4561 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4562 });
4563 cx.assert_editor_state(indoc! {"
4564 «ddddˇ»
4565 ccc
4566 bb
4567 a
4568 "});
4569
4570 // Basic manipulate case
4571 // Start selection moves to column 0
4572 // End of selection shrinks to fit shorter line
4573 cx.set_state(indoc! {"
4574 dd«d
4575 ccc
4576 bb
4577 aaaaaˇ»
4578 "});
4579 cx.update_editor(|e, window, cx| {
4580 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4581 });
4582 cx.assert_editor_state(indoc! {"
4583 «aaaaa
4584 bb
4585 ccc
4586 dddˇ»
4587 "});
4588
4589 // Manipulate case with newlines
4590 cx.set_state(indoc! {"
4591 dd«d
4592 ccc
4593
4594 bb
4595 aaaaa
4596
4597 ˇ»
4598 "});
4599 cx.update_editor(|e, window, cx| {
4600 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4601 });
4602 cx.assert_editor_state(indoc! {"
4603 «
4604
4605 aaaaa
4606 bb
4607 ccc
4608 dddˇ»
4609
4610 "});
4611
4612 // Adding new line
4613 cx.set_state(indoc! {"
4614 aa«a
4615 bbˇ»b
4616 "});
4617 cx.update_editor(|e, window, cx| {
4618 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4619 });
4620 cx.assert_editor_state(indoc! {"
4621 «aaa
4622 bbb
4623 added_lineˇ»
4624 "});
4625
4626 // Removing line
4627 cx.set_state(indoc! {"
4628 aa«a
4629 bbbˇ»
4630 "});
4631 cx.update_editor(|e, window, cx| {
4632 e.manipulate_immutable_lines(window, cx, |lines| {
4633 lines.pop();
4634 })
4635 });
4636 cx.assert_editor_state(indoc! {"
4637 «aaaˇ»
4638 "});
4639
4640 // Removing all lines
4641 cx.set_state(indoc! {"
4642 aa«a
4643 bbbˇ»
4644 "});
4645 cx.update_editor(|e, window, cx| {
4646 e.manipulate_immutable_lines(window, cx, |lines| {
4647 lines.drain(..);
4648 })
4649 });
4650 cx.assert_editor_state(indoc! {"
4651 ˇ
4652 "});
4653}
4654
4655#[gpui::test]
4656async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4657 init_test(cx, |_| {});
4658
4659 let mut cx = EditorTestContext::new(cx).await;
4660
4661 // Consider continuous selection as single selection
4662 cx.set_state(indoc! {"
4663 Aaa«aa
4664 cˇ»c«c
4665 bb
4666 aaaˇ»aa
4667 "});
4668 cx.update_editor(|e, window, cx| {
4669 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4670 });
4671 cx.assert_editor_state(indoc! {"
4672 «Aaaaa
4673 ccc
4674 bb
4675 aaaaaˇ»
4676 "});
4677
4678 cx.set_state(indoc! {"
4679 Aaa«aa
4680 cˇ»c«c
4681 bb
4682 aaaˇ»aa
4683 "});
4684 cx.update_editor(|e, window, cx| {
4685 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4686 });
4687 cx.assert_editor_state(indoc! {"
4688 «Aaaaa
4689 ccc
4690 bbˇ»
4691 "});
4692
4693 // Consider non continuous selection as distinct dedup operations
4694 cx.set_state(indoc! {"
4695 «aaaaa
4696 bb
4697 aaaaa
4698 aaaaaˇ»
4699
4700 aaa«aaˇ»
4701 "});
4702 cx.update_editor(|e, window, cx| {
4703 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4704 });
4705 cx.assert_editor_state(indoc! {"
4706 «aaaaa
4707 bbˇ»
4708
4709 «aaaaaˇ»
4710 "});
4711}
4712
4713#[gpui::test]
4714async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4715 init_test(cx, |_| {});
4716
4717 let mut cx = EditorTestContext::new(cx).await;
4718
4719 cx.set_state(indoc! {"
4720 «Aaa
4721 aAa
4722 Aaaˇ»
4723 "});
4724 cx.update_editor(|e, window, cx| {
4725 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4726 });
4727 cx.assert_editor_state(indoc! {"
4728 «Aaa
4729 aAaˇ»
4730 "});
4731
4732 cx.set_state(indoc! {"
4733 «Aaa
4734 aAa
4735 aaAˇ»
4736 "});
4737 cx.update_editor(|e, window, cx| {
4738 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4739 });
4740 cx.assert_editor_state(indoc! {"
4741 «Aaaˇ»
4742 "});
4743}
4744
4745#[gpui::test]
4746async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4747 init_test(cx, |_| {});
4748
4749 let mut cx = EditorTestContext::new(cx).await;
4750
4751 let js_language = Arc::new(Language::new(
4752 LanguageConfig {
4753 name: "JavaScript".into(),
4754 wrap_characters: Some(language::WrapCharactersConfig {
4755 start_prefix: "<".into(),
4756 start_suffix: ">".into(),
4757 end_prefix: "</".into(),
4758 end_suffix: ">".into(),
4759 }),
4760 ..LanguageConfig::default()
4761 },
4762 None,
4763 ));
4764
4765 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4766
4767 cx.set_state(indoc! {"
4768 «testˇ»
4769 "});
4770 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4771 cx.assert_editor_state(indoc! {"
4772 <«ˇ»>test</«ˇ»>
4773 "});
4774
4775 cx.set_state(indoc! {"
4776 «test
4777 testˇ»
4778 "});
4779 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4780 cx.assert_editor_state(indoc! {"
4781 <«ˇ»>test
4782 test</«ˇ»>
4783 "});
4784
4785 cx.set_state(indoc! {"
4786 teˇst
4787 "});
4788 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4789 cx.assert_editor_state(indoc! {"
4790 te<«ˇ»></«ˇ»>st
4791 "});
4792}
4793
4794#[gpui::test]
4795async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
4796 init_test(cx, |_| {});
4797
4798 let mut cx = EditorTestContext::new(cx).await;
4799
4800 let js_language = Arc::new(Language::new(
4801 LanguageConfig {
4802 name: "JavaScript".into(),
4803 wrap_characters: Some(language::WrapCharactersConfig {
4804 start_prefix: "<".into(),
4805 start_suffix: ">".into(),
4806 end_prefix: "</".into(),
4807 end_suffix: ">".into(),
4808 }),
4809 ..LanguageConfig::default()
4810 },
4811 None,
4812 ));
4813
4814 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4815
4816 cx.set_state(indoc! {"
4817 «testˇ»
4818 «testˇ» «testˇ»
4819 «testˇ»
4820 "});
4821 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4822 cx.assert_editor_state(indoc! {"
4823 <«ˇ»>test</«ˇ»>
4824 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
4825 <«ˇ»>test</«ˇ»>
4826 "});
4827
4828 cx.set_state(indoc! {"
4829 «test
4830 testˇ»
4831 «test
4832 testˇ»
4833 "});
4834 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4835 cx.assert_editor_state(indoc! {"
4836 <«ˇ»>test
4837 test</«ˇ»>
4838 <«ˇ»>test
4839 test</«ˇ»>
4840 "});
4841}
4842
4843#[gpui::test]
4844async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
4845 init_test(cx, |_| {});
4846
4847 let mut cx = EditorTestContext::new(cx).await;
4848
4849 let plaintext_language = Arc::new(Language::new(
4850 LanguageConfig {
4851 name: "Plain Text".into(),
4852 ..LanguageConfig::default()
4853 },
4854 None,
4855 ));
4856
4857 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
4858
4859 cx.set_state(indoc! {"
4860 «testˇ»
4861 "});
4862 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4863 cx.assert_editor_state(indoc! {"
4864 «testˇ»
4865 "});
4866}
4867
4868#[gpui::test]
4869async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
4870 init_test(cx, |_| {});
4871
4872 let mut cx = EditorTestContext::new(cx).await;
4873
4874 // Manipulate with multiple selections on a single line
4875 cx.set_state(indoc! {"
4876 dd«dd
4877 cˇ»c«c
4878 bb
4879 aaaˇ»aa
4880 "});
4881 cx.update_editor(|e, window, cx| {
4882 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4883 });
4884 cx.assert_editor_state(indoc! {"
4885 «aaaaa
4886 bb
4887 ccc
4888 ddddˇ»
4889 "});
4890
4891 // Manipulate with multiple disjoin selections
4892 cx.set_state(indoc! {"
4893 5«
4894 4
4895 3
4896 2
4897 1ˇ»
4898
4899 dd«dd
4900 ccc
4901 bb
4902 aaaˇ»aa
4903 "});
4904 cx.update_editor(|e, window, cx| {
4905 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4906 });
4907 cx.assert_editor_state(indoc! {"
4908 «1
4909 2
4910 3
4911 4
4912 5ˇ»
4913
4914 «aaaaa
4915 bb
4916 ccc
4917 ddddˇ»
4918 "});
4919
4920 // Adding lines on each selection
4921 cx.set_state(indoc! {"
4922 2«
4923 1ˇ»
4924
4925 bb«bb
4926 aaaˇ»aa
4927 "});
4928 cx.update_editor(|e, window, cx| {
4929 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
4930 });
4931 cx.assert_editor_state(indoc! {"
4932 «2
4933 1
4934 added lineˇ»
4935
4936 «bbbb
4937 aaaaa
4938 added lineˇ»
4939 "});
4940
4941 // Removing lines on each selection
4942 cx.set_state(indoc! {"
4943 2«
4944 1ˇ»
4945
4946 bb«bb
4947 aaaˇ»aa
4948 "});
4949 cx.update_editor(|e, window, cx| {
4950 e.manipulate_immutable_lines(window, cx, |lines| {
4951 lines.pop();
4952 })
4953 });
4954 cx.assert_editor_state(indoc! {"
4955 «2ˇ»
4956
4957 «bbbbˇ»
4958 "});
4959}
4960
4961#[gpui::test]
4962async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
4963 init_test(cx, |settings| {
4964 settings.defaults.tab_size = NonZeroU32::new(3)
4965 });
4966
4967 let mut cx = EditorTestContext::new(cx).await;
4968
4969 // MULTI SELECTION
4970 // Ln.1 "«" tests empty lines
4971 // Ln.9 tests just leading whitespace
4972 cx.set_state(indoc! {"
4973 «
4974 abc // No indentationˇ»
4975 «\tabc // 1 tabˇ»
4976 \t\tabc « ˇ» // 2 tabs
4977 \t ab«c // Tab followed by space
4978 \tabc // Space followed by tab (3 spaces should be the result)
4979 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
4980 abˇ»ˇc ˇ ˇ // Already space indented«
4981 \t
4982 \tabc\tdef // Only the leading tab is manipulatedˇ»
4983 "});
4984 cx.update_editor(|e, window, cx| {
4985 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
4986 });
4987 cx.assert_editor_state(
4988 indoc! {"
4989 «
4990 abc // No indentation
4991 abc // 1 tab
4992 abc // 2 tabs
4993 abc // Tab followed by space
4994 abc // Space followed by tab (3 spaces should be the result)
4995 abc // Mixed indentation (tab conversion depends on the column)
4996 abc // Already space indented
4997 ·
4998 abc\tdef // Only the leading tab is manipulatedˇ»
4999 "}
5000 .replace("·", "")
5001 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5002 );
5003
5004 // Test on just a few lines, the others should remain unchanged
5005 // Only lines (3, 5, 10, 11) should change
5006 cx.set_state(
5007 indoc! {"
5008 ·
5009 abc // No indentation
5010 \tabcˇ // 1 tab
5011 \t\tabc // 2 tabs
5012 \t abcˇ // Tab followed by space
5013 \tabc // Space followed by tab (3 spaces should be the result)
5014 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5015 abc // Already space indented
5016 «\t
5017 \tabc\tdef // Only the leading tab is manipulatedˇ»
5018 "}
5019 .replace("·", "")
5020 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5021 );
5022 cx.update_editor(|e, window, cx| {
5023 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5024 });
5025 cx.assert_editor_state(
5026 indoc! {"
5027 ·
5028 abc // No indentation
5029 « abc // 1 tabˇ»
5030 \t\tabc // 2 tabs
5031 « abc // Tab followed by spaceˇ»
5032 \tabc // Space followed by tab (3 spaces should be the result)
5033 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5034 abc // Already space indented
5035 « ·
5036 abc\tdef // Only the leading tab is manipulatedˇ»
5037 "}
5038 .replace("·", "")
5039 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5040 );
5041
5042 // SINGLE SELECTION
5043 // Ln.1 "«" tests empty lines
5044 // Ln.9 tests just leading whitespace
5045 cx.set_state(indoc! {"
5046 «
5047 abc // No indentation
5048 \tabc // 1 tab
5049 \t\tabc // 2 tabs
5050 \t abc // Tab followed by space
5051 \tabc // Space followed by tab (3 spaces should be the result)
5052 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5053 abc // Already space indented
5054 \t
5055 \tabc\tdef // Only the leading tab is manipulatedˇ»
5056 "});
5057 cx.update_editor(|e, window, cx| {
5058 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5059 });
5060 cx.assert_editor_state(
5061 indoc! {"
5062 «
5063 abc // No indentation
5064 abc // 1 tab
5065 abc // 2 tabs
5066 abc // Tab followed by space
5067 abc // Space followed by tab (3 spaces should be the result)
5068 abc // Mixed indentation (tab conversion depends on the column)
5069 abc // Already space indented
5070 ·
5071 abc\tdef // Only the leading tab is manipulatedˇ»
5072 "}
5073 .replace("·", "")
5074 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5075 );
5076}
5077
5078#[gpui::test]
5079async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5080 init_test(cx, |settings| {
5081 settings.defaults.tab_size = NonZeroU32::new(3)
5082 });
5083
5084 let mut cx = EditorTestContext::new(cx).await;
5085
5086 // MULTI SELECTION
5087 // Ln.1 "«" tests empty lines
5088 // Ln.11 tests just leading whitespace
5089 cx.set_state(indoc! {"
5090 «
5091 abˇ»ˇc // No indentation
5092 abc ˇ ˇ // 1 space (< 3 so dont convert)
5093 abc « // 2 spaces (< 3 so dont convert)
5094 abc // 3 spaces (convert)
5095 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5096 «\tˇ»\t«\tˇ»abc // Already tab indented
5097 «\t abc // Tab followed by space
5098 \tabc // Space followed by tab (should be consumed due to tab)
5099 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5100 \tˇ» «\t
5101 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5102 "});
5103 cx.update_editor(|e, window, cx| {
5104 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5105 });
5106 cx.assert_editor_state(indoc! {"
5107 «
5108 abc // No indentation
5109 abc // 1 space (< 3 so dont convert)
5110 abc // 2 spaces (< 3 so dont convert)
5111 \tabc // 3 spaces (convert)
5112 \t abc // 5 spaces (1 tab + 2 spaces)
5113 \t\t\tabc // Already tab indented
5114 \t abc // Tab followed by space
5115 \tabc // Space followed by tab (should be consumed due to tab)
5116 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5117 \t\t\t
5118 \tabc \t // Only the leading spaces should be convertedˇ»
5119 "});
5120
5121 // Test on just a few lines, the other should remain unchanged
5122 // Only lines (4, 8, 11, 12) should change
5123 cx.set_state(
5124 indoc! {"
5125 ·
5126 abc // No indentation
5127 abc // 1 space (< 3 so dont convert)
5128 abc // 2 spaces (< 3 so dont convert)
5129 « abc // 3 spaces (convert)ˇ»
5130 abc // 5 spaces (1 tab + 2 spaces)
5131 \t\t\tabc // Already tab indented
5132 \t abc // Tab followed by space
5133 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5134 \t\t \tabc // Mixed indentation
5135 \t \t \t \tabc // Mixed indentation
5136 \t \tˇ
5137 « abc \t // Only the leading spaces should be convertedˇ»
5138 "}
5139 .replace("·", "")
5140 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5141 );
5142 cx.update_editor(|e, window, cx| {
5143 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5144 });
5145 cx.assert_editor_state(
5146 indoc! {"
5147 ·
5148 abc // No indentation
5149 abc // 1 space (< 3 so dont convert)
5150 abc // 2 spaces (< 3 so dont convert)
5151 «\tabc // 3 spaces (convert)ˇ»
5152 abc // 5 spaces (1 tab + 2 spaces)
5153 \t\t\tabc // Already tab indented
5154 \t abc // Tab followed by space
5155 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5156 \t\t \tabc // Mixed indentation
5157 \t \t \t \tabc // Mixed indentation
5158 «\t\t\t
5159 \tabc \t // Only the leading spaces should be convertedˇ»
5160 "}
5161 .replace("·", "")
5162 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5163 );
5164
5165 // SINGLE SELECTION
5166 // Ln.1 "«" tests empty lines
5167 // Ln.11 tests just leading whitespace
5168 cx.set_state(indoc! {"
5169 «
5170 abc // No indentation
5171 abc // 1 space (< 3 so dont convert)
5172 abc // 2 spaces (< 3 so dont convert)
5173 abc // 3 spaces (convert)
5174 abc // 5 spaces (1 tab + 2 spaces)
5175 \t\t\tabc // Already tab indented
5176 \t abc // Tab followed by space
5177 \tabc // Space followed by tab (should be consumed due to tab)
5178 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5179 \t \t
5180 abc \t // Only the leading spaces should be convertedˇ»
5181 "});
5182 cx.update_editor(|e, window, cx| {
5183 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5184 });
5185 cx.assert_editor_state(indoc! {"
5186 «
5187 abc // No indentation
5188 abc // 1 space (< 3 so dont convert)
5189 abc // 2 spaces (< 3 so dont convert)
5190 \tabc // 3 spaces (convert)
5191 \t abc // 5 spaces (1 tab + 2 spaces)
5192 \t\t\tabc // Already tab indented
5193 \t abc // Tab followed by space
5194 \tabc // Space followed by tab (should be consumed due to tab)
5195 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5196 \t\t\t
5197 \tabc \t // Only the leading spaces should be convertedˇ»
5198 "});
5199}
5200
5201#[gpui::test]
5202async fn test_toggle_case(cx: &mut TestAppContext) {
5203 init_test(cx, |_| {});
5204
5205 let mut cx = EditorTestContext::new(cx).await;
5206
5207 // If all lower case -> upper case
5208 cx.set_state(indoc! {"
5209 «hello worldˇ»
5210 "});
5211 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5212 cx.assert_editor_state(indoc! {"
5213 «HELLO WORLDˇ»
5214 "});
5215
5216 // If all upper case -> lower case
5217 cx.set_state(indoc! {"
5218 «HELLO WORLDˇ»
5219 "});
5220 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5221 cx.assert_editor_state(indoc! {"
5222 «hello worldˇ»
5223 "});
5224
5225 // If any upper case characters are identified -> lower case
5226 // This matches JetBrains IDEs
5227 cx.set_state(indoc! {"
5228 «hEllo worldˇ»
5229 "});
5230 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5231 cx.assert_editor_state(indoc! {"
5232 «hello worldˇ»
5233 "});
5234}
5235
5236#[gpui::test]
5237async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5238 init_test(cx, |_| {});
5239
5240 let mut cx = EditorTestContext::new(cx).await;
5241
5242 cx.set_state(indoc! {"
5243 «implement-windows-supportˇ»
5244 "});
5245 cx.update_editor(|e, window, cx| {
5246 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5247 });
5248 cx.assert_editor_state(indoc! {"
5249 «Implement windows supportˇ»
5250 "});
5251}
5252
5253#[gpui::test]
5254async fn test_manipulate_text(cx: &mut TestAppContext) {
5255 init_test(cx, |_| {});
5256
5257 let mut cx = EditorTestContext::new(cx).await;
5258
5259 // Test convert_to_upper_case()
5260 cx.set_state(indoc! {"
5261 «hello worldˇ»
5262 "});
5263 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5264 cx.assert_editor_state(indoc! {"
5265 «HELLO WORLDˇ»
5266 "});
5267
5268 // Test convert_to_lower_case()
5269 cx.set_state(indoc! {"
5270 «HELLO WORLDˇ»
5271 "});
5272 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5273 cx.assert_editor_state(indoc! {"
5274 «hello worldˇ»
5275 "});
5276
5277 // Test multiple line, single selection case
5278 cx.set_state(indoc! {"
5279 «The quick brown
5280 fox jumps over
5281 the lazy dogˇ»
5282 "});
5283 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5284 cx.assert_editor_state(indoc! {"
5285 «The Quick Brown
5286 Fox Jumps Over
5287 The Lazy Dogˇ»
5288 "});
5289
5290 // Test multiple line, single selection case
5291 cx.set_state(indoc! {"
5292 «The quick brown
5293 fox jumps over
5294 the lazy dogˇ»
5295 "});
5296 cx.update_editor(|e, window, cx| {
5297 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5298 });
5299 cx.assert_editor_state(indoc! {"
5300 «TheQuickBrown
5301 FoxJumpsOver
5302 TheLazyDogˇ»
5303 "});
5304
5305 // From here on out, test more complex cases of manipulate_text()
5306
5307 // Test no selection case - should affect words cursors are in
5308 // Cursor at beginning, middle, and end of word
5309 cx.set_state(indoc! {"
5310 ˇhello big beauˇtiful worldˇ
5311 "});
5312 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5313 cx.assert_editor_state(indoc! {"
5314 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5315 "});
5316
5317 // Test multiple selections on a single line and across multiple lines
5318 cx.set_state(indoc! {"
5319 «Theˇ» quick «brown
5320 foxˇ» jumps «overˇ»
5321 the «lazyˇ» dog
5322 "});
5323 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5324 cx.assert_editor_state(indoc! {"
5325 «THEˇ» quick «BROWN
5326 FOXˇ» jumps «OVERˇ»
5327 the «LAZYˇ» dog
5328 "});
5329
5330 // Test case where text length grows
5331 cx.set_state(indoc! {"
5332 «tschüߡ»
5333 "});
5334 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5335 cx.assert_editor_state(indoc! {"
5336 «TSCHÜSSˇ»
5337 "});
5338
5339 // Test to make sure we don't crash when text shrinks
5340 cx.set_state(indoc! {"
5341 aaa_bbbˇ
5342 "});
5343 cx.update_editor(|e, window, cx| {
5344 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5345 });
5346 cx.assert_editor_state(indoc! {"
5347 «aaaBbbˇ»
5348 "});
5349
5350 // Test to make sure we all aware of the fact that each word can grow and shrink
5351 // Final selections should be aware of this fact
5352 cx.set_state(indoc! {"
5353 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5354 "});
5355 cx.update_editor(|e, window, cx| {
5356 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5357 });
5358 cx.assert_editor_state(indoc! {"
5359 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5360 "});
5361
5362 cx.set_state(indoc! {"
5363 «hElLo, WoRld!ˇ»
5364 "});
5365 cx.update_editor(|e, window, cx| {
5366 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5367 });
5368 cx.assert_editor_state(indoc! {"
5369 «HeLlO, wOrLD!ˇ»
5370 "});
5371
5372 // Test selections with `line_mode = true`.
5373 cx.update_editor(|editor, _window, _cx| editor.selections.line_mode = true);
5374 cx.set_state(indoc! {"
5375 «The quick brown
5376 fox jumps over
5377 tˇ»he lazy dog
5378 "});
5379 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5380 cx.assert_editor_state(indoc! {"
5381 «THE QUICK BROWN
5382 FOX JUMPS OVER
5383 THE LAZY DOGˇ»
5384 "});
5385}
5386
5387#[gpui::test]
5388fn test_duplicate_line(cx: &mut TestAppContext) {
5389 init_test(cx, |_| {});
5390
5391 let editor = cx.add_window(|window, cx| {
5392 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5393 build_editor(buffer, window, cx)
5394 });
5395 _ = editor.update(cx, |editor, window, cx| {
5396 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5397 s.select_display_ranges([
5398 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5399 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5400 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5401 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5402 ])
5403 });
5404 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5405 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5406 assert_eq!(
5407 editor.selections.display_ranges(cx),
5408 vec![
5409 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5410 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5411 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5412 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5413 ]
5414 );
5415 });
5416
5417 let editor = cx.add_window(|window, cx| {
5418 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5419 build_editor(buffer, window, cx)
5420 });
5421 _ = editor.update(cx, |editor, window, cx| {
5422 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5423 s.select_display_ranges([
5424 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5425 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5426 ])
5427 });
5428 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5429 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5430 assert_eq!(
5431 editor.selections.display_ranges(cx),
5432 vec![
5433 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5434 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5435 ]
5436 );
5437 });
5438
5439 // With `move_upwards` the selections stay in place, except for
5440 // the lines inserted above them
5441 let editor = cx.add_window(|window, cx| {
5442 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5443 build_editor(buffer, window, cx)
5444 });
5445 _ = editor.update(cx, |editor, window, cx| {
5446 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5447 s.select_display_ranges([
5448 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5449 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5450 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5451 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5452 ])
5453 });
5454 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5455 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5456 assert_eq!(
5457 editor.selections.display_ranges(cx),
5458 vec![
5459 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5460 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5461 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5462 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5463 ]
5464 );
5465 });
5466
5467 let editor = cx.add_window(|window, cx| {
5468 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5469 build_editor(buffer, window, cx)
5470 });
5471 _ = editor.update(cx, |editor, window, cx| {
5472 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5473 s.select_display_ranges([
5474 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5475 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5476 ])
5477 });
5478 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5479 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5480 assert_eq!(
5481 editor.selections.display_ranges(cx),
5482 vec![
5483 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5484 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5485 ]
5486 );
5487 });
5488
5489 let editor = cx.add_window(|window, cx| {
5490 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5491 build_editor(buffer, window, cx)
5492 });
5493 _ = editor.update(cx, |editor, window, cx| {
5494 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5495 s.select_display_ranges([
5496 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5497 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5498 ])
5499 });
5500 editor.duplicate_selection(&DuplicateSelection, window, cx);
5501 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5502 assert_eq!(
5503 editor.selections.display_ranges(cx),
5504 vec![
5505 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5506 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5507 ]
5508 );
5509 });
5510}
5511
5512#[gpui::test]
5513fn test_move_line_up_down(cx: &mut TestAppContext) {
5514 init_test(cx, |_| {});
5515
5516 let editor = cx.add_window(|window, cx| {
5517 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5518 build_editor(buffer, window, cx)
5519 });
5520 _ = editor.update(cx, |editor, window, cx| {
5521 editor.fold_creases(
5522 vec![
5523 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5524 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5525 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5526 ],
5527 true,
5528 window,
5529 cx,
5530 );
5531 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5532 s.select_display_ranges([
5533 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5534 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5535 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5536 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5537 ])
5538 });
5539 assert_eq!(
5540 editor.display_text(cx),
5541 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5542 );
5543
5544 editor.move_line_up(&MoveLineUp, window, cx);
5545 assert_eq!(
5546 editor.display_text(cx),
5547 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5548 );
5549 assert_eq!(
5550 editor.selections.display_ranges(cx),
5551 vec![
5552 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5553 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5554 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5555 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5556 ]
5557 );
5558 });
5559
5560 _ = editor.update(cx, |editor, window, cx| {
5561 editor.move_line_down(&MoveLineDown, window, cx);
5562 assert_eq!(
5563 editor.display_text(cx),
5564 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5565 );
5566 assert_eq!(
5567 editor.selections.display_ranges(cx),
5568 vec![
5569 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5570 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5571 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5572 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5573 ]
5574 );
5575 });
5576
5577 _ = editor.update(cx, |editor, window, cx| {
5578 editor.move_line_down(&MoveLineDown, window, cx);
5579 assert_eq!(
5580 editor.display_text(cx),
5581 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5582 );
5583 assert_eq!(
5584 editor.selections.display_ranges(cx),
5585 vec![
5586 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5587 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5588 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5589 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5590 ]
5591 );
5592 });
5593
5594 _ = editor.update(cx, |editor, window, cx| {
5595 editor.move_line_up(&MoveLineUp, window, cx);
5596 assert_eq!(
5597 editor.display_text(cx),
5598 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5599 );
5600 assert_eq!(
5601 editor.selections.display_ranges(cx),
5602 vec![
5603 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5604 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5605 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5606 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5607 ]
5608 );
5609 });
5610}
5611
5612#[gpui::test]
5613fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5614 init_test(cx, |_| {});
5615 let editor = cx.add_window(|window, cx| {
5616 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5617 build_editor(buffer, window, cx)
5618 });
5619 _ = editor.update(cx, |editor, window, cx| {
5620 editor.fold_creases(
5621 vec![Crease::simple(
5622 Point::new(6, 4)..Point::new(7, 4),
5623 FoldPlaceholder::test(),
5624 )],
5625 true,
5626 window,
5627 cx,
5628 );
5629 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5630 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5631 });
5632 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5633 editor.move_line_up(&MoveLineUp, window, cx);
5634 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5635 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5636 });
5637}
5638
5639#[gpui::test]
5640fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5641 init_test(cx, |_| {});
5642
5643 let editor = cx.add_window(|window, cx| {
5644 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5645 build_editor(buffer, window, cx)
5646 });
5647 _ = editor.update(cx, |editor, window, cx| {
5648 let snapshot = editor.buffer.read(cx).snapshot(cx);
5649 editor.insert_blocks(
5650 [BlockProperties {
5651 style: BlockStyle::Fixed,
5652 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5653 height: Some(1),
5654 render: Arc::new(|_| div().into_any()),
5655 priority: 0,
5656 }],
5657 Some(Autoscroll::fit()),
5658 cx,
5659 );
5660 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5661 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5662 });
5663 editor.move_line_down(&MoveLineDown, window, cx);
5664 });
5665}
5666
5667#[gpui::test]
5668async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5669 init_test(cx, |_| {});
5670
5671 let mut cx = EditorTestContext::new(cx).await;
5672 cx.set_state(
5673 &"
5674 ˇzero
5675 one
5676 two
5677 three
5678 four
5679 five
5680 "
5681 .unindent(),
5682 );
5683
5684 // Create a four-line block that replaces three lines of text.
5685 cx.update_editor(|editor, window, cx| {
5686 let snapshot = editor.snapshot(window, cx);
5687 let snapshot = &snapshot.buffer_snapshot;
5688 let placement = BlockPlacement::Replace(
5689 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5690 );
5691 editor.insert_blocks(
5692 [BlockProperties {
5693 placement,
5694 height: Some(4),
5695 style: BlockStyle::Sticky,
5696 render: Arc::new(|_| gpui::div().into_any_element()),
5697 priority: 0,
5698 }],
5699 None,
5700 cx,
5701 );
5702 });
5703
5704 // Move down so that the cursor touches the block.
5705 cx.update_editor(|editor, window, cx| {
5706 editor.move_down(&Default::default(), window, cx);
5707 });
5708 cx.assert_editor_state(
5709 &"
5710 zero
5711 «one
5712 two
5713 threeˇ»
5714 four
5715 five
5716 "
5717 .unindent(),
5718 );
5719
5720 // Move down past the block.
5721 cx.update_editor(|editor, window, cx| {
5722 editor.move_down(&Default::default(), window, cx);
5723 });
5724 cx.assert_editor_state(
5725 &"
5726 zero
5727 one
5728 two
5729 three
5730 ˇfour
5731 five
5732 "
5733 .unindent(),
5734 );
5735}
5736
5737#[gpui::test]
5738fn test_transpose(cx: &mut TestAppContext) {
5739 init_test(cx, |_| {});
5740
5741 _ = cx.add_window(|window, cx| {
5742 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5743 editor.set_style(EditorStyle::default(), window, cx);
5744 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5745 s.select_ranges([1..1])
5746 });
5747 editor.transpose(&Default::default(), window, cx);
5748 assert_eq!(editor.text(cx), "bac");
5749 assert_eq!(editor.selections.ranges(cx), [2..2]);
5750
5751 editor.transpose(&Default::default(), window, cx);
5752 assert_eq!(editor.text(cx), "bca");
5753 assert_eq!(editor.selections.ranges(cx), [3..3]);
5754
5755 editor.transpose(&Default::default(), window, cx);
5756 assert_eq!(editor.text(cx), "bac");
5757 assert_eq!(editor.selections.ranges(cx), [3..3]);
5758
5759 editor
5760 });
5761
5762 _ = cx.add_window(|window, cx| {
5763 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5764 editor.set_style(EditorStyle::default(), window, cx);
5765 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5766 s.select_ranges([3..3])
5767 });
5768 editor.transpose(&Default::default(), window, cx);
5769 assert_eq!(editor.text(cx), "acb\nde");
5770 assert_eq!(editor.selections.ranges(cx), [3..3]);
5771
5772 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5773 s.select_ranges([4..4])
5774 });
5775 editor.transpose(&Default::default(), window, cx);
5776 assert_eq!(editor.text(cx), "acbd\ne");
5777 assert_eq!(editor.selections.ranges(cx), [5..5]);
5778
5779 editor.transpose(&Default::default(), window, cx);
5780 assert_eq!(editor.text(cx), "acbde\n");
5781 assert_eq!(editor.selections.ranges(cx), [6..6]);
5782
5783 editor.transpose(&Default::default(), window, cx);
5784 assert_eq!(editor.text(cx), "acbd\ne");
5785 assert_eq!(editor.selections.ranges(cx), [6..6]);
5786
5787 editor
5788 });
5789
5790 _ = cx.add_window(|window, cx| {
5791 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5792 editor.set_style(EditorStyle::default(), window, cx);
5793 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5794 s.select_ranges([1..1, 2..2, 4..4])
5795 });
5796 editor.transpose(&Default::default(), window, cx);
5797 assert_eq!(editor.text(cx), "bacd\ne");
5798 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
5799
5800 editor.transpose(&Default::default(), window, cx);
5801 assert_eq!(editor.text(cx), "bcade\n");
5802 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
5803
5804 editor.transpose(&Default::default(), window, cx);
5805 assert_eq!(editor.text(cx), "bcda\ne");
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), "bcade\n");
5810 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
5811
5812 editor.transpose(&Default::default(), window, cx);
5813 assert_eq!(editor.text(cx), "bcaed\n");
5814 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
5815
5816 editor
5817 });
5818
5819 _ = cx.add_window(|window, cx| {
5820 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
5821 editor.set_style(EditorStyle::default(), window, cx);
5822 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5823 s.select_ranges([4..4])
5824 });
5825 editor.transpose(&Default::default(), window, cx);
5826 assert_eq!(editor.text(cx), "🏀🍐✋");
5827 assert_eq!(editor.selections.ranges(cx), [8..8]);
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.transpose(&Default::default(), window, cx);
5834 assert_eq!(editor.text(cx), "🏀🍐✋");
5835 assert_eq!(editor.selections.ranges(cx), [11..11]);
5836
5837 editor
5838 });
5839}
5840
5841#[gpui::test]
5842async fn test_rewrap(cx: &mut TestAppContext) {
5843 init_test(cx, |settings| {
5844 settings.languages.0.extend([
5845 (
5846 "Markdown".into(),
5847 LanguageSettingsContent {
5848 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5849 preferred_line_length: Some(40),
5850 ..Default::default()
5851 },
5852 ),
5853 (
5854 "Plain Text".into(),
5855 LanguageSettingsContent {
5856 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
5857 preferred_line_length: Some(40),
5858 ..Default::default()
5859 },
5860 ),
5861 (
5862 "C++".into(),
5863 LanguageSettingsContent {
5864 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5865 preferred_line_length: Some(40),
5866 ..Default::default()
5867 },
5868 ),
5869 (
5870 "Python".into(),
5871 LanguageSettingsContent {
5872 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5873 preferred_line_length: Some(40),
5874 ..Default::default()
5875 },
5876 ),
5877 (
5878 "Rust".into(),
5879 LanguageSettingsContent {
5880 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
5881 preferred_line_length: Some(40),
5882 ..Default::default()
5883 },
5884 ),
5885 ])
5886 });
5887
5888 let mut cx = EditorTestContext::new(cx).await;
5889
5890 let cpp_language = Arc::new(Language::new(
5891 LanguageConfig {
5892 name: "C++".into(),
5893 line_comments: vec!["// ".into()],
5894 ..LanguageConfig::default()
5895 },
5896 None,
5897 ));
5898 let python_language = Arc::new(Language::new(
5899 LanguageConfig {
5900 name: "Python".into(),
5901 line_comments: vec!["# ".into()],
5902 ..LanguageConfig::default()
5903 },
5904 None,
5905 ));
5906 let markdown_language = Arc::new(Language::new(
5907 LanguageConfig {
5908 name: "Markdown".into(),
5909 rewrap_prefixes: vec![
5910 regex::Regex::new("\\d+\\.\\s+").unwrap(),
5911 regex::Regex::new("[-*+]\\s+").unwrap(),
5912 ],
5913 ..LanguageConfig::default()
5914 },
5915 None,
5916 ));
5917 let rust_language = Arc::new(
5918 Language::new(
5919 LanguageConfig {
5920 name: "Rust".into(),
5921 line_comments: vec!["// ".into(), "/// ".into()],
5922 ..LanguageConfig::default()
5923 },
5924 Some(tree_sitter_rust::LANGUAGE.into()),
5925 )
5926 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
5927 .unwrap(),
5928 );
5929
5930 let plaintext_language = Arc::new(Language::new(
5931 LanguageConfig {
5932 name: "Plain Text".into(),
5933 ..LanguageConfig::default()
5934 },
5935 None,
5936 ));
5937
5938 // Test basic rewrapping of a long line with a cursor
5939 assert_rewrap(
5940 indoc! {"
5941 // ˇThis is a long comment that needs to be wrapped.
5942 "},
5943 indoc! {"
5944 // ˇThis is a long comment that needs to
5945 // be wrapped.
5946 "},
5947 cpp_language.clone(),
5948 &mut cx,
5949 );
5950
5951 // Test rewrapping a full selection
5952 assert_rewrap(
5953 indoc! {"
5954 «// This selected long comment needs to be wrapped.ˇ»"
5955 },
5956 indoc! {"
5957 «// This selected long comment needs to
5958 // be wrapped.ˇ»"
5959 },
5960 cpp_language.clone(),
5961 &mut cx,
5962 );
5963
5964 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
5965 assert_rewrap(
5966 indoc! {"
5967 // ˇThis is the first line.
5968 // Thisˇ is the second line.
5969 // This is the thirdˇ line, all part of one paragraph.
5970 "},
5971 indoc! {"
5972 // ˇThis is the first line. Thisˇ is the
5973 // second line. This is the thirdˇ line,
5974 // all part of one paragraph.
5975 "},
5976 cpp_language.clone(),
5977 &mut cx,
5978 );
5979
5980 // Test multiple cursors in different paragraphs trigger separate rewraps
5981 assert_rewrap(
5982 indoc! {"
5983 // ˇThis is the first paragraph, first line.
5984 // ˇThis is the first paragraph, second line.
5985
5986 // ˇThis is the second paragraph, first line.
5987 // ˇThis is the second paragraph, second line.
5988 "},
5989 indoc! {"
5990 // ˇThis is the first paragraph, first
5991 // line. ˇThis is the first paragraph,
5992 // second line.
5993
5994 // ˇThis is the second paragraph, first
5995 // line. ˇThis is the second paragraph,
5996 // second line.
5997 "},
5998 cpp_language.clone(),
5999 &mut cx,
6000 );
6001
6002 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6003 assert_rewrap(
6004 indoc! {"
6005 «// A regular long long comment to be wrapped.
6006 /// A documentation long comment to be wrapped.ˇ»
6007 "},
6008 indoc! {"
6009 «// A regular long long comment to be
6010 // wrapped.
6011 /// A documentation long comment to be
6012 /// wrapped.ˇ»
6013 "},
6014 rust_language.clone(),
6015 &mut cx,
6016 );
6017
6018 // Test that change in indentation level trigger seperate rewraps
6019 assert_rewrap(
6020 indoc! {"
6021 fn foo() {
6022 «// This is a long comment at the base indent.
6023 // This is a long comment at the next indent.ˇ»
6024 }
6025 "},
6026 indoc! {"
6027 fn foo() {
6028 «// This is a long comment at the
6029 // base indent.
6030 // This is a long comment at the
6031 // next indent.ˇ»
6032 }
6033 "},
6034 rust_language.clone(),
6035 &mut cx,
6036 );
6037
6038 // Test that different comment prefix characters (e.g., '#') are handled correctly
6039 assert_rewrap(
6040 indoc! {"
6041 # ˇThis is a long comment using a pound sign.
6042 "},
6043 indoc! {"
6044 # ˇThis is a long comment using a pound
6045 # sign.
6046 "},
6047 python_language,
6048 &mut cx,
6049 );
6050
6051 // Test rewrapping only affects comments, not code even when selected
6052 assert_rewrap(
6053 indoc! {"
6054 «/// This doc comment is long and should be wrapped.
6055 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6056 "},
6057 indoc! {"
6058 «/// This doc comment is long and should
6059 /// be wrapped.
6060 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6061 "},
6062 rust_language.clone(),
6063 &mut cx,
6064 );
6065
6066 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6067 assert_rewrap(
6068 indoc! {"
6069 # Header
6070
6071 A long long long line of markdown text to wrap.ˇ
6072 "},
6073 indoc! {"
6074 # Header
6075
6076 A long long long line of markdown text
6077 to wrap.ˇ
6078 "},
6079 markdown_language.clone(),
6080 &mut cx,
6081 );
6082
6083 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6084 assert_rewrap(
6085 indoc! {"
6086 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6087 2. This is a numbered list item that is very long and needs to be wrapped properly.
6088 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6089 "},
6090 indoc! {"
6091 «1. This is a numbered list item that is
6092 very long and needs to be wrapped
6093 properly.
6094 2. This is a numbered list item that is
6095 very long and needs to be wrapped
6096 properly.
6097 - This is an unordered list item that is
6098 also very long and should not merge
6099 with the numbered item.ˇ»
6100 "},
6101 markdown_language.clone(),
6102 &mut cx,
6103 );
6104
6105 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6106 assert_rewrap(
6107 indoc! {"
6108 «1. This is a numbered list item that is
6109 very long and needs to be wrapped
6110 properly.
6111 2. This is a numbered list item that is
6112 very long and needs to be wrapped
6113 properly.
6114 - This is an unordered list item that is
6115 also very long and should not merge with
6116 the numbered item.ˇ»
6117 "},
6118 indoc! {"
6119 «1. This is a numbered list item that is
6120 very long and needs to be wrapped
6121 properly.
6122 2. This is a numbered list item that is
6123 very long and needs to be wrapped
6124 properly.
6125 - This is an unordered list item that is
6126 also very long and should not merge
6127 with the numbered item.ˇ»
6128 "},
6129 markdown_language.clone(),
6130 &mut cx,
6131 );
6132
6133 // Test that rewrapping maintain indents even when they already exists.
6134 assert_rewrap(
6135 indoc! {"
6136 «1. This is a numbered list
6137 item that is very long and needs to be wrapped properly.
6138 2. This is a numbered list
6139 item that is very long and needs to be wrapped properly.
6140 - This is an unordered list item that is also very long and
6141 should not merge with the numbered item.ˇ»
6142 "},
6143 indoc! {"
6144 «1. This is a numbered list item that is
6145 very long and needs to be wrapped
6146 properly.
6147 2. This is a numbered list item that is
6148 very long and needs to be wrapped
6149 properly.
6150 - This is an unordered list item that is
6151 also very long and should not merge
6152 with the numbered item.ˇ»
6153 "},
6154 markdown_language,
6155 &mut cx,
6156 );
6157
6158 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6159 assert_rewrap(
6160 indoc! {"
6161 ˇThis is a very long line of plain text that will be wrapped.
6162 "},
6163 indoc! {"
6164 ˇThis is a very long line of plain text
6165 that will be wrapped.
6166 "},
6167 plaintext_language.clone(),
6168 &mut cx,
6169 );
6170
6171 // Test that non-commented code acts as a paragraph boundary within a selection
6172 assert_rewrap(
6173 indoc! {"
6174 «// This is the first long comment block to be wrapped.
6175 fn my_func(a: u32);
6176 // This is the second long comment block to be wrapped.ˇ»
6177 "},
6178 indoc! {"
6179 «// This is the first long comment block
6180 // to be wrapped.
6181 fn my_func(a: u32);
6182 // This is the second long comment block
6183 // to be wrapped.ˇ»
6184 "},
6185 rust_language,
6186 &mut cx,
6187 );
6188
6189 // Test rewrapping multiple selections, including ones with blank lines or tabs
6190 assert_rewrap(
6191 indoc! {"
6192 «ˇThis is a very long line that will be wrapped.
6193
6194 This is another paragraph in the same selection.»
6195
6196 «\tThis is a very long indented line that will be wrapped.ˇ»
6197 "},
6198 indoc! {"
6199 «ˇThis is a very long line that will be
6200 wrapped.
6201
6202 This is another paragraph in the same
6203 selection.»
6204
6205 «\tThis is a very long indented line
6206 \tthat will be wrapped.ˇ»
6207 "},
6208 plaintext_language,
6209 &mut cx,
6210 );
6211
6212 // Test that an empty comment line acts as a paragraph boundary
6213 assert_rewrap(
6214 indoc! {"
6215 // ˇThis is a long comment that will be wrapped.
6216 //
6217 // And this is another long comment that will also be wrapped.ˇ
6218 "},
6219 indoc! {"
6220 // ˇThis is a long comment that will be
6221 // wrapped.
6222 //
6223 // And this is another long comment that
6224 // will also be wrapped.ˇ
6225 "},
6226 cpp_language,
6227 &mut cx,
6228 );
6229
6230 #[track_caller]
6231 fn assert_rewrap(
6232 unwrapped_text: &str,
6233 wrapped_text: &str,
6234 language: Arc<Language>,
6235 cx: &mut EditorTestContext,
6236 ) {
6237 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6238 cx.set_state(unwrapped_text);
6239 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6240 cx.assert_editor_state(wrapped_text);
6241 }
6242}
6243
6244#[gpui::test]
6245async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6246 init_test(cx, |settings| {
6247 settings.languages.0.extend([(
6248 "Rust".into(),
6249 LanguageSettingsContent {
6250 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6251 preferred_line_length: Some(40),
6252 ..Default::default()
6253 },
6254 )])
6255 });
6256
6257 let mut cx = EditorTestContext::new(cx).await;
6258
6259 let rust_lang = Arc::new(
6260 Language::new(
6261 LanguageConfig {
6262 name: "Rust".into(),
6263 line_comments: vec!["// ".into()],
6264 block_comment: Some(BlockCommentConfig {
6265 start: "/*".into(),
6266 end: "*/".into(),
6267 prefix: "* ".into(),
6268 tab_size: 1,
6269 }),
6270 documentation_comment: Some(BlockCommentConfig {
6271 start: "/**".into(),
6272 end: "*/".into(),
6273 prefix: "* ".into(),
6274 tab_size: 1,
6275 }),
6276
6277 ..LanguageConfig::default()
6278 },
6279 Some(tree_sitter_rust::LANGUAGE.into()),
6280 )
6281 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6282 .unwrap(),
6283 );
6284
6285 // regular block comment
6286 assert_rewrap(
6287 indoc! {"
6288 /*
6289 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6290 */
6291 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6292 "},
6293 indoc! {"
6294 /*
6295 *ˇ Lorem ipsum dolor sit amet,
6296 * consectetur adipiscing elit.
6297 */
6298 /*
6299 *ˇ Lorem ipsum dolor sit amet,
6300 * consectetur adipiscing elit.
6301 */
6302 "},
6303 rust_lang.clone(),
6304 &mut cx,
6305 );
6306
6307 // indent is respected
6308 assert_rewrap(
6309 indoc! {"
6310 {}
6311 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6312 "},
6313 indoc! {"
6314 {}
6315 /*
6316 *ˇ Lorem ipsum dolor sit amet,
6317 * consectetur adipiscing elit.
6318 */
6319 "},
6320 rust_lang.clone(),
6321 &mut cx,
6322 );
6323
6324 // short block comments with inline delimiters
6325 assert_rewrap(
6326 indoc! {"
6327 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6328 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6329 */
6330 /*
6331 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6332 "},
6333 indoc! {"
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 *ˇ Lorem ipsum dolor sit amet,
6344 * consectetur adipiscing elit.
6345 */
6346 "},
6347 rust_lang.clone(),
6348 &mut cx,
6349 );
6350
6351 // multiline block comment with inline start/end delimiters
6352 assert_rewrap(
6353 indoc! {"
6354 /*ˇ Lorem ipsum dolor sit amet,
6355 * consectetur adipiscing elit. */
6356 "},
6357 indoc! {"
6358 /*
6359 *ˇ Lorem ipsum dolor sit amet,
6360 * consectetur adipiscing elit.
6361 */
6362 "},
6363 rust_lang.clone(),
6364 &mut cx,
6365 );
6366
6367 // block comment rewrap still respects paragraph bounds
6368 assert_rewrap(
6369 indoc! {"
6370 /*
6371 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6372 *
6373 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6374 */
6375 "},
6376 indoc! {"
6377 /*
6378 *ˇ Lorem ipsum dolor sit amet,
6379 * consectetur adipiscing elit.
6380 *
6381 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6382 */
6383 "},
6384 rust_lang.clone(),
6385 &mut cx,
6386 );
6387
6388 // documentation comments
6389 assert_rewrap(
6390 indoc! {"
6391 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6392 /**
6393 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6394 */
6395 "},
6396 indoc! {"
6397 /**
6398 *ˇ Lorem ipsum dolor sit amet,
6399 * consectetur adipiscing elit.
6400 */
6401 /**
6402 *ˇ Lorem ipsum dolor sit amet,
6403 * consectetur adipiscing elit.
6404 */
6405 "},
6406 rust_lang.clone(),
6407 &mut cx,
6408 );
6409
6410 // different, adjacent comments
6411 assert_rewrap(
6412 indoc! {"
6413 /**
6414 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6415 */
6416 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6417 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6418 "},
6419 indoc! {"
6420 /**
6421 *ˇ Lorem ipsum dolor sit amet,
6422 * consectetur adipiscing elit.
6423 */
6424 /*
6425 *ˇ Lorem ipsum dolor sit amet,
6426 * consectetur adipiscing elit.
6427 */
6428 //ˇ Lorem ipsum dolor sit amet,
6429 // consectetur adipiscing elit.
6430 "},
6431 rust_lang.clone(),
6432 &mut cx,
6433 );
6434
6435 // selection w/ single short block comment
6436 assert_rewrap(
6437 indoc! {"
6438 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6439 "},
6440 indoc! {"
6441 «/*
6442 * Lorem ipsum dolor sit amet,
6443 * consectetur adipiscing elit.
6444 */ˇ»
6445 "},
6446 rust_lang.clone(),
6447 &mut cx,
6448 );
6449
6450 // rewrapping a single comment w/ abutting comments
6451 assert_rewrap(
6452 indoc! {"
6453 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6454 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6455 "},
6456 indoc! {"
6457 /*
6458 * ˇLorem ipsum dolor sit amet,
6459 * consectetur adipiscing elit.
6460 */
6461 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6462 "},
6463 rust_lang.clone(),
6464 &mut cx,
6465 );
6466
6467 // selection w/ non-abutting short block comments
6468 assert_rewrap(
6469 indoc! {"
6470 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6471
6472 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6473 "},
6474 indoc! {"
6475 «/*
6476 * Lorem ipsum dolor sit amet,
6477 * consectetur adipiscing elit.
6478 */
6479
6480 /*
6481 * Lorem ipsum dolor sit amet,
6482 * consectetur adipiscing elit.
6483 */ˇ»
6484 "},
6485 rust_lang.clone(),
6486 &mut cx,
6487 );
6488
6489 // selection of multiline block comments
6490 assert_rewrap(
6491 indoc! {"
6492 «/* Lorem ipsum dolor sit amet,
6493 * consectetur adipiscing elit. */ˇ»
6494 "},
6495 indoc! {"
6496 «/*
6497 * Lorem ipsum dolor sit amet,
6498 * consectetur adipiscing elit.
6499 */ˇ»
6500 "},
6501 rust_lang.clone(),
6502 &mut cx,
6503 );
6504
6505 // partial selection of multiline block comments
6506 assert_rewrap(
6507 indoc! {"
6508 «/* Lorem ipsum dolor sit amet,ˇ»
6509 * consectetur adipiscing elit. */
6510 /* Lorem ipsum dolor sit amet,
6511 «* consectetur adipiscing elit. */ˇ»
6512 "},
6513 indoc! {"
6514 «/*
6515 * Lorem ipsum dolor sit amet,ˇ»
6516 * consectetur adipiscing elit. */
6517 /* Lorem ipsum dolor sit amet,
6518 «* consectetur adipiscing elit.
6519 */ˇ»
6520 "},
6521 rust_lang.clone(),
6522 &mut cx,
6523 );
6524
6525 // selection w/ abutting short block comments
6526 // TODO: should not be combined; should rewrap as 2 comments
6527 assert_rewrap(
6528 indoc! {"
6529 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6530 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6531 "},
6532 // desired behavior:
6533 // indoc! {"
6534 // «/*
6535 // * Lorem ipsum dolor sit amet,
6536 // * consectetur adipiscing elit.
6537 // */
6538 // /*
6539 // * Lorem ipsum dolor sit amet,
6540 // * consectetur adipiscing elit.
6541 // */ˇ»
6542 // "},
6543 // actual behaviour:
6544 indoc! {"
6545 «/*
6546 * Lorem ipsum dolor sit amet,
6547 * consectetur adipiscing elit. Lorem
6548 * ipsum dolor sit amet, consectetur
6549 * adipiscing elit.
6550 */ˇ»
6551 "},
6552 rust_lang.clone(),
6553 &mut cx,
6554 );
6555
6556 // TODO: same as above, but with delimiters on separate line
6557 // assert_rewrap(
6558 // indoc! {"
6559 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6560 // */
6561 // /*
6562 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6563 // "},
6564 // // desired:
6565 // // indoc! {"
6566 // // «/*
6567 // // * Lorem ipsum dolor sit amet,
6568 // // * consectetur adipiscing elit.
6569 // // */
6570 // // /*
6571 // // * Lorem ipsum dolor sit amet,
6572 // // * consectetur adipiscing elit.
6573 // // */ˇ»
6574 // // "},
6575 // // actual: (but with trailing w/s on the empty lines)
6576 // indoc! {"
6577 // «/*
6578 // * Lorem ipsum dolor sit amet,
6579 // * consectetur adipiscing elit.
6580 // *
6581 // */
6582 // /*
6583 // *
6584 // * Lorem ipsum dolor sit amet,
6585 // * consectetur adipiscing elit.
6586 // */ˇ»
6587 // "},
6588 // rust_lang.clone(),
6589 // &mut cx,
6590 // );
6591
6592 // TODO these are unhandled edge cases; not correct, just documenting known issues
6593 assert_rewrap(
6594 indoc! {"
6595 /*
6596 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6597 */
6598 /*
6599 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6600 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6601 "},
6602 // desired:
6603 // indoc! {"
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 // /*
6613 // *ˇ Lorem ipsum dolor sit amet
6614 // */ /* consectetur adipiscing elit. */
6615 // "},
6616 // actual:
6617 indoc! {"
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 *ˇ Lorem ipsum dolor sit amet */ /*
6628 * consectetur adipiscing elit.
6629 */
6630 "},
6631 rust_lang,
6632 &mut cx,
6633 );
6634
6635 #[track_caller]
6636 fn assert_rewrap(
6637 unwrapped_text: &str,
6638 wrapped_text: &str,
6639 language: Arc<Language>,
6640 cx: &mut EditorTestContext,
6641 ) {
6642 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6643 cx.set_state(unwrapped_text);
6644 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6645 cx.assert_editor_state(wrapped_text);
6646 }
6647}
6648
6649#[gpui::test]
6650async fn test_hard_wrap(cx: &mut TestAppContext) {
6651 init_test(cx, |_| {});
6652 let mut cx = EditorTestContext::new(cx).await;
6653
6654 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6655 cx.update_editor(|editor, _, cx| {
6656 editor.set_hard_wrap(Some(14), cx);
6657 });
6658
6659 cx.set_state(indoc!(
6660 "
6661 one two three ˇ
6662 "
6663 ));
6664 cx.simulate_input("four");
6665 cx.run_until_parked();
6666
6667 cx.assert_editor_state(indoc!(
6668 "
6669 one two three
6670 fourˇ
6671 "
6672 ));
6673
6674 cx.update_editor(|editor, window, cx| {
6675 editor.newline(&Default::default(), window, cx);
6676 });
6677 cx.run_until_parked();
6678 cx.assert_editor_state(indoc!(
6679 "
6680 one two three
6681 four
6682 ˇ
6683 "
6684 ));
6685
6686 cx.simulate_input("five");
6687 cx.run_until_parked();
6688 cx.assert_editor_state(indoc!(
6689 "
6690 one two three
6691 four
6692 fiveˇ
6693 "
6694 ));
6695
6696 cx.update_editor(|editor, window, cx| {
6697 editor.newline(&Default::default(), window, cx);
6698 });
6699 cx.run_until_parked();
6700 cx.simulate_input("# ");
6701 cx.run_until_parked();
6702 cx.assert_editor_state(indoc!(
6703 "
6704 one two three
6705 four
6706 five
6707 # ˇ
6708 "
6709 ));
6710
6711 cx.update_editor(|editor, window, cx| {
6712 editor.newline(&Default::default(), window, cx);
6713 });
6714 cx.run_until_parked();
6715 cx.assert_editor_state(indoc!(
6716 "
6717 one two three
6718 four
6719 five
6720 #\x20
6721 #ˇ
6722 "
6723 ));
6724
6725 cx.simulate_input(" 6");
6726 cx.run_until_parked();
6727 cx.assert_editor_state(indoc!(
6728 "
6729 one two three
6730 four
6731 five
6732 #
6733 # 6ˇ
6734 "
6735 ));
6736}
6737
6738#[gpui::test]
6739async fn test_cut_line_ends(cx: &mut TestAppContext) {
6740 init_test(cx, |_| {});
6741
6742 let mut cx = EditorTestContext::new(cx).await;
6743
6744 cx.set_state(indoc! {"
6745 The quick« brownˇ»
6746 fox jumps overˇ
6747 the lazy dog"});
6748 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6749 cx.assert_editor_state(indoc! {"
6750 The quickˇ
6751 ˇthe lazy dog"});
6752
6753 cx.set_state(indoc! {"
6754 The quick« brownˇ»
6755 fox jumps overˇ
6756 the lazy dog"});
6757 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
6758 cx.assert_editor_state(indoc! {"
6759 The quickˇ
6760 fox jumps overˇthe lazy dog"});
6761
6762 cx.set_state(indoc! {"
6763 The quick« brownˇ»
6764 fox jumps overˇ
6765 the lazy dog"});
6766 cx.update_editor(|e, window, cx| {
6767 e.cut_to_end_of_line(
6768 &CutToEndOfLine {
6769 stop_at_newlines: true,
6770 },
6771 window,
6772 cx,
6773 )
6774 });
6775 cx.assert_editor_state(indoc! {"
6776 The quickˇ
6777 fox jumps overˇ
6778 the lazy dog"});
6779
6780 cx.set_state(indoc! {"
6781 The quick« brownˇ»
6782 fox jumps overˇ
6783 the lazy dog"});
6784 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
6785 cx.assert_editor_state(indoc! {"
6786 The quickˇ
6787 fox jumps overˇthe lazy dog"});
6788}
6789
6790#[gpui::test]
6791async fn test_clipboard(cx: &mut TestAppContext) {
6792 init_test(cx, |_| {});
6793
6794 let mut cx = EditorTestContext::new(cx).await;
6795
6796 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
6797 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6798 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
6799
6800 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
6801 cx.set_state("two ˇfour ˇsix ˇ");
6802 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6803 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
6804
6805 // Paste again but with only two cursors. Since the number of cursors doesn't
6806 // match the number of slices in the clipboard, the entire clipboard text
6807 // is pasted at each cursor.
6808 cx.set_state("ˇtwo one✅ four three six five ˇ");
6809 cx.update_editor(|e, window, cx| {
6810 e.handle_input("( ", window, cx);
6811 e.paste(&Paste, window, cx);
6812 e.handle_input(") ", window, cx);
6813 });
6814 cx.assert_editor_state(
6815 &([
6816 "( one✅ ",
6817 "three ",
6818 "five ) ˇtwo one✅ four three six five ( one✅ ",
6819 "three ",
6820 "five ) ˇ",
6821 ]
6822 .join("\n")),
6823 );
6824
6825 // Cut with three selections, one of which is full-line.
6826 cx.set_state(indoc! {"
6827 1«2ˇ»3
6828 4ˇ567
6829 «8ˇ»9"});
6830 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
6831 cx.assert_editor_state(indoc! {"
6832 1ˇ3
6833 ˇ9"});
6834
6835 // Paste with three selections, noticing how the copied selection that was full-line
6836 // gets inserted before the second cursor.
6837 cx.set_state(indoc! {"
6838 1ˇ3
6839 9ˇ
6840 «oˇ»ne"});
6841 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6842 cx.assert_editor_state(indoc! {"
6843 12ˇ3
6844 4567
6845 9ˇ
6846 8ˇne"});
6847
6848 // Copy with a single cursor only, which writes the whole line into the clipboard.
6849 cx.set_state(indoc! {"
6850 The quick brown
6851 fox juˇmps over
6852 the lazy dog"});
6853 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6854 assert_eq!(
6855 cx.read_from_clipboard()
6856 .and_then(|item| item.text().as_deref().map(str::to_string)),
6857 Some("fox jumps over\n".to_string())
6858 );
6859
6860 // Paste with three selections, noticing how the copied full-line selection is inserted
6861 // before the empty selections but replaces the selection that is non-empty.
6862 cx.set_state(indoc! {"
6863 Tˇhe quick brown
6864 «foˇ»x jumps over
6865 tˇhe lazy dog"});
6866 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
6867 cx.assert_editor_state(indoc! {"
6868 fox jumps over
6869 Tˇhe quick brown
6870 fox jumps over
6871 ˇx jumps over
6872 fox jumps over
6873 tˇhe lazy dog"});
6874}
6875
6876#[gpui::test]
6877async fn test_copy_trim(cx: &mut TestAppContext) {
6878 init_test(cx, |_| {});
6879
6880 let mut cx = EditorTestContext::new(cx).await;
6881 cx.set_state(
6882 r#" «for selection in selections.iter() {
6883 let mut start = selection.start;
6884 let mut end = selection.end;
6885 let is_entire_line = selection.is_empty();
6886 if is_entire_line {
6887 start = Point::new(start.row, 0);ˇ»
6888 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6889 }
6890 "#,
6891 );
6892 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6893 assert_eq!(
6894 cx.read_from_clipboard()
6895 .and_then(|item| item.text().as_deref().map(str::to_string)),
6896 Some(
6897 "for selection in selections.iter() {
6898 let mut start = selection.start;
6899 let mut end = selection.end;
6900 let is_entire_line = selection.is_empty();
6901 if is_entire_line {
6902 start = Point::new(start.row, 0);"
6903 .to_string()
6904 ),
6905 "Regular copying preserves all indentation selected",
6906 );
6907 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6908 assert_eq!(
6909 cx.read_from_clipboard()
6910 .and_then(|item| item.text().as_deref().map(str::to_string)),
6911 Some(
6912 "for selection in selections.iter() {
6913let mut start = selection.start;
6914let mut end = selection.end;
6915let is_entire_line = selection.is_empty();
6916if is_entire_line {
6917 start = Point::new(start.row, 0);"
6918 .to_string()
6919 ),
6920 "Copying with stripping should strip all leading whitespaces"
6921 );
6922
6923 cx.set_state(
6924 r#" « for selection in selections.iter() {
6925 let mut start = selection.start;
6926 let mut end = selection.end;
6927 let is_entire_line = selection.is_empty();
6928 if is_entire_line {
6929 start = Point::new(start.row, 0);ˇ»
6930 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6931 }
6932 "#,
6933 );
6934 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6935 assert_eq!(
6936 cx.read_from_clipboard()
6937 .and_then(|item| item.text().as_deref().map(str::to_string)),
6938 Some(
6939 " for selection in selections.iter() {
6940 let mut start = selection.start;
6941 let mut end = selection.end;
6942 let is_entire_line = selection.is_empty();
6943 if is_entire_line {
6944 start = Point::new(start.row, 0);"
6945 .to_string()
6946 ),
6947 "Regular copying preserves all indentation selected",
6948 );
6949 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6950 assert_eq!(
6951 cx.read_from_clipboard()
6952 .and_then(|item| item.text().as_deref().map(str::to_string)),
6953 Some(
6954 "for selection in selections.iter() {
6955let mut start = selection.start;
6956let mut end = selection.end;
6957let is_entire_line = selection.is_empty();
6958if is_entire_line {
6959 start = Point::new(start.row, 0);"
6960 .to_string()
6961 ),
6962 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
6963 );
6964
6965 cx.set_state(
6966 r#" «ˇ for selection in selections.iter() {
6967 let mut start = selection.start;
6968 let mut end = selection.end;
6969 let is_entire_line = selection.is_empty();
6970 if is_entire_line {
6971 start = Point::new(start.row, 0);»
6972 end = cmp::min(max_point, Point::new(end.row + 1, 0));
6973 }
6974 "#,
6975 );
6976 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
6977 assert_eq!(
6978 cx.read_from_clipboard()
6979 .and_then(|item| item.text().as_deref().map(str::to_string)),
6980 Some(
6981 " for selection in selections.iter() {
6982 let mut start = selection.start;
6983 let mut end = selection.end;
6984 let is_entire_line = selection.is_empty();
6985 if is_entire_line {
6986 start = Point::new(start.row, 0);"
6987 .to_string()
6988 ),
6989 "Regular copying for reverse selection works the same",
6990 );
6991 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
6992 assert_eq!(
6993 cx.read_from_clipboard()
6994 .and_then(|item| item.text().as_deref().map(str::to_string)),
6995 Some(
6996 "for selection in selections.iter() {
6997let mut start = selection.start;
6998let mut end = selection.end;
6999let is_entire_line = selection.is_empty();
7000if is_entire_line {
7001 start = Point::new(start.row, 0);"
7002 .to_string()
7003 ),
7004 "Copying with stripping for reverse selection works the same"
7005 );
7006
7007 cx.set_state(
7008 r#" for selection «in selections.iter() {
7009 let mut start = selection.start;
7010 let mut end = selection.end;
7011 let is_entire_line = selection.is_empty();
7012 if is_entire_line {
7013 start = Point::new(start.row, 0);ˇ»
7014 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7015 }
7016 "#,
7017 );
7018 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7019 assert_eq!(
7020 cx.read_from_clipboard()
7021 .and_then(|item| item.text().as_deref().map(str::to_string)),
7022 Some(
7023 "in selections.iter() {
7024 let mut start = selection.start;
7025 let mut end = selection.end;
7026 let is_entire_line = selection.is_empty();
7027 if is_entire_line {
7028 start = Point::new(start.row, 0);"
7029 .to_string()
7030 ),
7031 "When selecting past the indent, the copying works as usual",
7032 );
7033 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7034 assert_eq!(
7035 cx.read_from_clipboard()
7036 .and_then(|item| item.text().as_deref().map(str::to_string)),
7037 Some(
7038 "in selections.iter() {
7039 let mut start = selection.start;
7040 let mut end = selection.end;
7041 let is_entire_line = selection.is_empty();
7042 if is_entire_line {
7043 start = Point::new(start.row, 0);"
7044 .to_string()
7045 ),
7046 "When selecting past the indent, nothing is trimmed"
7047 );
7048
7049 cx.set_state(
7050 r#" «for selection in selections.iter() {
7051 let mut start = selection.start;
7052
7053 let mut end = selection.end;
7054 let is_entire_line = selection.is_empty();
7055 if is_entire_line {
7056 start = Point::new(start.row, 0);
7057ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7058 }
7059 "#,
7060 );
7061 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7062 assert_eq!(
7063 cx.read_from_clipboard()
7064 .and_then(|item| item.text().as_deref().map(str::to_string)),
7065 Some(
7066 "for selection in selections.iter() {
7067let mut start = selection.start;
7068
7069let mut end = selection.end;
7070let is_entire_line = selection.is_empty();
7071if is_entire_line {
7072 start = Point::new(start.row, 0);
7073"
7074 .to_string()
7075 ),
7076 "Copying with stripping should ignore empty lines"
7077 );
7078}
7079
7080#[gpui::test]
7081async fn test_paste_multiline(cx: &mut TestAppContext) {
7082 init_test(cx, |_| {});
7083
7084 let mut cx = EditorTestContext::new(cx).await;
7085 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7086
7087 // Cut an indented block, without the leading whitespace.
7088 cx.set_state(indoc! {"
7089 const a: B = (
7090 c(),
7091 «d(
7092 e,
7093 f
7094 )ˇ»
7095 );
7096 "});
7097 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7098 cx.assert_editor_state(indoc! {"
7099 const a: B = (
7100 c(),
7101 ˇ
7102 );
7103 "});
7104
7105 // Paste it at the same position.
7106 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7107 cx.assert_editor_state(indoc! {"
7108 const a: B = (
7109 c(),
7110 d(
7111 e,
7112 f
7113 )ˇ
7114 );
7115 "});
7116
7117 // Paste it at a line with a lower indent level.
7118 cx.set_state(indoc! {"
7119 ˇ
7120 const a: B = (
7121 c(),
7122 );
7123 "});
7124 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7125 cx.assert_editor_state(indoc! {"
7126 d(
7127 e,
7128 f
7129 )ˇ
7130 const a: B = (
7131 c(),
7132 );
7133 "});
7134
7135 // Cut an indented block, with the leading whitespace.
7136 cx.set_state(indoc! {"
7137 const a: B = (
7138 c(),
7139 « d(
7140 e,
7141 f
7142 )
7143 ˇ»);
7144 "});
7145 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7146 cx.assert_editor_state(indoc! {"
7147 const a: B = (
7148 c(),
7149 ˇ);
7150 "});
7151
7152 // Paste it at the same position.
7153 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7154 cx.assert_editor_state(indoc! {"
7155 const a: B = (
7156 c(),
7157 d(
7158 e,
7159 f
7160 )
7161 ˇ);
7162 "});
7163
7164 // Paste it at a line with a higher indent level.
7165 cx.set_state(indoc! {"
7166 const a: B = (
7167 c(),
7168 d(
7169 e,
7170 fˇ
7171 )
7172 );
7173 "});
7174 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7175 cx.assert_editor_state(indoc! {"
7176 const a: B = (
7177 c(),
7178 d(
7179 e,
7180 f d(
7181 e,
7182 f
7183 )
7184 ˇ
7185 )
7186 );
7187 "});
7188
7189 // Copy an indented block, starting mid-line
7190 cx.set_state(indoc! {"
7191 const a: B = (
7192 c(),
7193 somethin«g(
7194 e,
7195 f
7196 )ˇ»
7197 );
7198 "});
7199 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7200
7201 // Paste it on a line with a lower indent level
7202 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7203 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7204 cx.assert_editor_state(indoc! {"
7205 const a: B = (
7206 c(),
7207 something(
7208 e,
7209 f
7210 )
7211 );
7212 g(
7213 e,
7214 f
7215 )ˇ"});
7216}
7217
7218#[gpui::test]
7219async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7220 init_test(cx, |_| {});
7221
7222 cx.write_to_clipboard(ClipboardItem::new_string(
7223 " d(\n e\n );\n".into(),
7224 ));
7225
7226 let mut cx = EditorTestContext::new(cx).await;
7227 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7228
7229 cx.set_state(indoc! {"
7230 fn a() {
7231 b();
7232 if c() {
7233 ˇ
7234 }
7235 }
7236 "});
7237
7238 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7239 cx.assert_editor_state(indoc! {"
7240 fn a() {
7241 b();
7242 if c() {
7243 d(
7244 e
7245 );
7246 ˇ
7247 }
7248 }
7249 "});
7250
7251 cx.set_state(indoc! {"
7252 fn a() {
7253 b();
7254 ˇ
7255 }
7256 "});
7257
7258 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7259 cx.assert_editor_state(indoc! {"
7260 fn a() {
7261 b();
7262 d(
7263 e
7264 );
7265 ˇ
7266 }
7267 "});
7268}
7269
7270#[gpui::test]
7271fn test_select_all(cx: &mut TestAppContext) {
7272 init_test(cx, |_| {});
7273
7274 let editor = cx.add_window(|window, cx| {
7275 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7276 build_editor(buffer, window, cx)
7277 });
7278 _ = editor.update(cx, |editor, window, cx| {
7279 editor.select_all(&SelectAll, window, cx);
7280 assert_eq!(
7281 editor.selections.display_ranges(cx),
7282 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7283 );
7284 });
7285}
7286
7287#[gpui::test]
7288fn test_select_line(cx: &mut TestAppContext) {
7289 init_test(cx, |_| {});
7290
7291 let editor = cx.add_window(|window, cx| {
7292 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7293 build_editor(buffer, window, cx)
7294 });
7295 _ = editor.update(cx, |editor, window, cx| {
7296 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7297 s.select_display_ranges([
7298 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7299 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7300 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7301 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7302 ])
7303 });
7304 editor.select_line(&SelectLine, window, cx);
7305 assert_eq!(
7306 editor.selections.display_ranges(cx),
7307 vec![
7308 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7309 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7310 ]
7311 );
7312 });
7313
7314 _ = editor.update(cx, |editor, window, cx| {
7315 editor.select_line(&SelectLine, window, cx);
7316 assert_eq!(
7317 editor.selections.display_ranges(cx),
7318 vec![
7319 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7320 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7321 ]
7322 );
7323 });
7324
7325 _ = editor.update(cx, |editor, window, cx| {
7326 editor.select_line(&SelectLine, window, cx);
7327 assert_eq!(
7328 editor.selections.display_ranges(cx),
7329 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7330 );
7331 });
7332}
7333
7334#[gpui::test]
7335async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7336 init_test(cx, |_| {});
7337 let mut cx = EditorTestContext::new(cx).await;
7338
7339 #[track_caller]
7340 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7341 cx.set_state(initial_state);
7342 cx.update_editor(|e, window, cx| {
7343 e.split_selection_into_lines(&Default::default(), window, cx)
7344 });
7345 cx.assert_editor_state(expected_state);
7346 }
7347
7348 // Selection starts and ends at the middle of lines, left-to-right
7349 test(
7350 &mut cx,
7351 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7352 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7353 );
7354 // Same thing, right-to-left
7355 test(
7356 &mut cx,
7357 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7358 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7359 );
7360
7361 // Whole buffer, left-to-right, last line *doesn't* end with newline
7362 test(
7363 &mut cx,
7364 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7365 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7366 );
7367 // Same thing, right-to-left
7368 test(
7369 &mut cx,
7370 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7371 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7372 );
7373
7374 // Whole buffer, left-to-right, last line ends with newline
7375 test(
7376 &mut cx,
7377 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7378 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7379 );
7380 // Same thing, right-to-left
7381 test(
7382 &mut cx,
7383 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7384 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7385 );
7386
7387 // Starts at the end of a line, ends at the start of another
7388 test(
7389 &mut cx,
7390 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7391 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7392 );
7393}
7394
7395#[gpui::test]
7396async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7397 init_test(cx, |_| {});
7398
7399 let editor = cx.add_window(|window, cx| {
7400 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7401 build_editor(buffer, window, cx)
7402 });
7403
7404 // setup
7405 _ = editor.update(cx, |editor, window, cx| {
7406 editor.fold_creases(
7407 vec![
7408 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7409 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7410 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7411 ],
7412 true,
7413 window,
7414 cx,
7415 );
7416 assert_eq!(
7417 editor.display_text(cx),
7418 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7419 );
7420 });
7421
7422 _ = editor.update(cx, |editor, window, cx| {
7423 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7424 s.select_display_ranges([
7425 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7426 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7427 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7428 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7429 ])
7430 });
7431 editor.split_selection_into_lines(&Default::default(), window, cx);
7432 assert_eq!(
7433 editor.display_text(cx),
7434 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7435 );
7436 });
7437 EditorTestContext::for_editor(editor, cx)
7438 .await
7439 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7440
7441 _ = editor.update(cx, |editor, window, cx| {
7442 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7443 s.select_display_ranges([
7444 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7445 ])
7446 });
7447 editor.split_selection_into_lines(&Default::default(), window, cx);
7448 assert_eq!(
7449 editor.display_text(cx),
7450 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7451 );
7452 assert_eq!(
7453 editor.selections.display_ranges(cx),
7454 [
7455 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7456 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7457 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7458 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7459 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7460 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7461 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7462 ]
7463 );
7464 });
7465 EditorTestContext::for_editor(editor, cx)
7466 .await
7467 .assert_editor_state(
7468 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7469 );
7470}
7471
7472#[gpui::test]
7473async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7474 init_test(cx, |_| {});
7475
7476 let mut cx = EditorTestContext::new(cx).await;
7477
7478 cx.set_state(indoc!(
7479 r#"abc
7480 defˇghi
7481
7482 jk
7483 nlmo
7484 "#
7485 ));
7486
7487 cx.update_editor(|editor, window, cx| {
7488 editor.add_selection_above(&Default::default(), window, cx);
7489 });
7490
7491 cx.assert_editor_state(indoc!(
7492 r#"abcˇ
7493 defˇghi
7494
7495 jk
7496 nlmo
7497 "#
7498 ));
7499
7500 cx.update_editor(|editor, window, cx| {
7501 editor.add_selection_above(&Default::default(), window, cx);
7502 });
7503
7504 cx.assert_editor_state(indoc!(
7505 r#"abcˇ
7506 defˇghi
7507
7508 jk
7509 nlmo
7510 "#
7511 ));
7512
7513 cx.update_editor(|editor, window, cx| {
7514 editor.add_selection_below(&Default::default(), window, cx);
7515 });
7516
7517 cx.assert_editor_state(indoc!(
7518 r#"abc
7519 defˇghi
7520
7521 jk
7522 nlmo
7523 "#
7524 ));
7525
7526 cx.update_editor(|editor, window, cx| {
7527 editor.undo_selection(&Default::default(), window, cx);
7528 });
7529
7530 cx.assert_editor_state(indoc!(
7531 r#"abcˇ
7532 defˇghi
7533
7534 jk
7535 nlmo
7536 "#
7537 ));
7538
7539 cx.update_editor(|editor, window, cx| {
7540 editor.redo_selection(&Default::default(), window, cx);
7541 });
7542
7543 cx.assert_editor_state(indoc!(
7544 r#"abc
7545 defˇghi
7546
7547 jk
7548 nlmo
7549 "#
7550 ));
7551
7552 cx.update_editor(|editor, window, cx| {
7553 editor.add_selection_below(&Default::default(), window, cx);
7554 });
7555
7556 cx.assert_editor_state(indoc!(
7557 r#"abc
7558 defˇghi
7559 ˇ
7560 jk
7561 nlmo
7562 "#
7563 ));
7564
7565 cx.update_editor(|editor, window, cx| {
7566 editor.add_selection_below(&Default::default(), window, cx);
7567 });
7568
7569 cx.assert_editor_state(indoc!(
7570 r#"abc
7571 defˇghi
7572 ˇ
7573 jkˇ
7574 nlmo
7575 "#
7576 ));
7577
7578 cx.update_editor(|editor, window, cx| {
7579 editor.add_selection_below(&Default::default(), window, cx);
7580 });
7581
7582 cx.assert_editor_state(indoc!(
7583 r#"abc
7584 defˇghi
7585 ˇ
7586 jkˇ
7587 nlmˇo
7588 "#
7589 ));
7590
7591 cx.update_editor(|editor, window, cx| {
7592 editor.add_selection_below(&Default::default(), window, cx);
7593 });
7594
7595 cx.assert_editor_state(indoc!(
7596 r#"abc
7597 defˇghi
7598 ˇ
7599 jkˇ
7600 nlmˇo
7601 ˇ"#
7602 ));
7603
7604 // change selections
7605 cx.set_state(indoc!(
7606 r#"abc
7607 def«ˇg»hi
7608
7609 jk
7610 nlmo
7611 "#
7612 ));
7613
7614 cx.update_editor(|editor, window, cx| {
7615 editor.add_selection_below(&Default::default(), window, cx);
7616 });
7617
7618 cx.assert_editor_state(indoc!(
7619 r#"abc
7620 def«ˇg»hi
7621
7622 jk
7623 nlm«ˇo»
7624 "#
7625 ));
7626
7627 cx.update_editor(|editor, window, cx| {
7628 editor.add_selection_below(&Default::default(), window, cx);
7629 });
7630
7631 cx.assert_editor_state(indoc!(
7632 r#"abc
7633 def«ˇg»hi
7634
7635 jk
7636 nlm«ˇo»
7637 "#
7638 ));
7639
7640 cx.update_editor(|editor, window, cx| {
7641 editor.add_selection_above(&Default::default(), window, cx);
7642 });
7643
7644 cx.assert_editor_state(indoc!(
7645 r#"abc
7646 def«ˇg»hi
7647
7648 jk
7649 nlmo
7650 "#
7651 ));
7652
7653 cx.update_editor(|editor, window, cx| {
7654 editor.add_selection_above(&Default::default(), window, cx);
7655 });
7656
7657 cx.assert_editor_state(indoc!(
7658 r#"abc
7659 def«ˇg»hi
7660
7661 jk
7662 nlmo
7663 "#
7664 ));
7665
7666 // Change selections again
7667 cx.set_state(indoc!(
7668 r#"a«bc
7669 defgˇ»hi
7670
7671 jk
7672 nlmo
7673 "#
7674 ));
7675
7676 cx.update_editor(|editor, window, cx| {
7677 editor.add_selection_below(&Default::default(), window, cx);
7678 });
7679
7680 cx.assert_editor_state(indoc!(
7681 r#"a«bcˇ»
7682 d«efgˇ»hi
7683
7684 j«kˇ»
7685 nlmo
7686 "#
7687 ));
7688
7689 cx.update_editor(|editor, window, cx| {
7690 editor.add_selection_below(&Default::default(), window, cx);
7691 });
7692 cx.assert_editor_state(indoc!(
7693 r#"a«bcˇ»
7694 d«efgˇ»hi
7695
7696 j«kˇ»
7697 n«lmoˇ»
7698 "#
7699 ));
7700 cx.update_editor(|editor, window, cx| {
7701 editor.add_selection_above(&Default::default(), window, cx);
7702 });
7703
7704 cx.assert_editor_state(indoc!(
7705 r#"a«bcˇ»
7706 d«efgˇ»hi
7707
7708 j«kˇ»
7709 nlmo
7710 "#
7711 ));
7712
7713 // Change selections again
7714 cx.set_state(indoc!(
7715 r#"abc
7716 d«ˇefghi
7717
7718 jk
7719 nlm»o
7720 "#
7721 ));
7722
7723 cx.update_editor(|editor, window, cx| {
7724 editor.add_selection_above(&Default::default(), window, cx);
7725 });
7726
7727 cx.assert_editor_state(indoc!(
7728 r#"a«ˇbc»
7729 d«ˇef»ghi
7730
7731 j«ˇk»
7732 n«ˇlm»o
7733 "#
7734 ));
7735
7736 cx.update_editor(|editor, window, cx| {
7737 editor.add_selection_below(&Default::default(), window, cx);
7738 });
7739
7740 cx.assert_editor_state(indoc!(
7741 r#"abc
7742 d«ˇef»ghi
7743
7744 j«ˇk»
7745 n«ˇlm»o
7746 "#
7747 ));
7748}
7749
7750#[gpui::test]
7751async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
7752 init_test(cx, |_| {});
7753 let mut cx = EditorTestContext::new(cx).await;
7754
7755 cx.set_state(indoc!(
7756 r#"line onˇe
7757 liˇne two
7758 line three
7759 line four"#
7760 ));
7761
7762 cx.update_editor(|editor, window, cx| {
7763 editor.add_selection_below(&Default::default(), window, cx);
7764 });
7765
7766 // test multiple cursors expand in the same direction
7767 cx.assert_editor_state(indoc!(
7768 r#"line onˇe
7769 liˇne twˇo
7770 liˇne three
7771 line four"#
7772 ));
7773
7774 cx.update_editor(|editor, window, cx| {
7775 editor.add_selection_below(&Default::default(), window, cx);
7776 });
7777
7778 cx.update_editor(|editor, window, cx| {
7779 editor.add_selection_below(&Default::default(), window, cx);
7780 });
7781
7782 // test multiple cursors expand below overflow
7783 cx.assert_editor_state(indoc!(
7784 r#"line onˇe
7785 liˇne twˇo
7786 liˇne thˇree
7787 liˇne foˇur"#
7788 ));
7789
7790 cx.update_editor(|editor, window, cx| {
7791 editor.add_selection_above(&Default::default(), window, cx);
7792 });
7793
7794 // test multiple cursors retrieves back correctly
7795 cx.assert_editor_state(indoc!(
7796 r#"line onˇe
7797 liˇne twˇo
7798 liˇne thˇree
7799 line four"#
7800 ));
7801
7802 cx.update_editor(|editor, window, cx| {
7803 editor.add_selection_above(&Default::default(), window, cx);
7804 });
7805
7806 cx.update_editor(|editor, window, cx| {
7807 editor.add_selection_above(&Default::default(), window, cx);
7808 });
7809
7810 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
7811 cx.assert_editor_state(indoc!(
7812 r#"liˇne onˇe
7813 liˇne two
7814 line three
7815 line four"#
7816 ));
7817
7818 cx.update_editor(|editor, window, cx| {
7819 editor.undo_selection(&Default::default(), window, cx);
7820 });
7821
7822 // test undo
7823 cx.assert_editor_state(indoc!(
7824 r#"line onˇe
7825 liˇne twˇo
7826 line three
7827 line four"#
7828 ));
7829
7830 cx.update_editor(|editor, window, cx| {
7831 editor.redo_selection(&Default::default(), window, cx);
7832 });
7833
7834 // test redo
7835 cx.assert_editor_state(indoc!(
7836 r#"liˇne onˇe
7837 liˇne two
7838 line three
7839 line four"#
7840 ));
7841
7842 cx.set_state(indoc!(
7843 r#"abcd
7844 ef«ghˇ»
7845 ijkl
7846 «mˇ»nop"#
7847 ));
7848
7849 cx.update_editor(|editor, window, cx| {
7850 editor.add_selection_above(&Default::default(), window, cx);
7851 });
7852
7853 // test multiple selections expand in the same direction
7854 cx.assert_editor_state(indoc!(
7855 r#"ab«cdˇ»
7856 ef«ghˇ»
7857 «iˇ»jkl
7858 «mˇ»nop"#
7859 ));
7860
7861 cx.update_editor(|editor, window, cx| {
7862 editor.add_selection_above(&Default::default(), window, cx);
7863 });
7864
7865 // test multiple selection upward overflow
7866 cx.assert_editor_state(indoc!(
7867 r#"ab«cdˇ»
7868 «eˇ»f«ghˇ»
7869 «iˇ»jkl
7870 «mˇ»nop"#
7871 ));
7872
7873 cx.update_editor(|editor, window, cx| {
7874 editor.add_selection_below(&Default::default(), window, cx);
7875 });
7876
7877 // test multiple selection retrieves back correctly
7878 cx.assert_editor_state(indoc!(
7879 r#"abcd
7880 ef«ghˇ»
7881 «iˇ»jkl
7882 «mˇ»nop"#
7883 ));
7884
7885 cx.update_editor(|editor, window, cx| {
7886 editor.add_selection_below(&Default::default(), window, cx);
7887 });
7888
7889 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
7890 cx.assert_editor_state(indoc!(
7891 r#"abcd
7892 ef«ghˇ»
7893 ij«klˇ»
7894 «mˇ»nop"#
7895 ));
7896
7897 cx.update_editor(|editor, window, cx| {
7898 editor.undo_selection(&Default::default(), window, cx);
7899 });
7900
7901 // test undo
7902 cx.assert_editor_state(indoc!(
7903 r#"abcd
7904 ef«ghˇ»
7905 «iˇ»jkl
7906 «mˇ»nop"#
7907 ));
7908
7909 cx.update_editor(|editor, window, cx| {
7910 editor.redo_selection(&Default::default(), window, cx);
7911 });
7912
7913 // test redo
7914 cx.assert_editor_state(indoc!(
7915 r#"abcd
7916 ef«ghˇ»
7917 ij«klˇ»
7918 «mˇ»nop"#
7919 ));
7920}
7921
7922#[gpui::test]
7923async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
7924 init_test(cx, |_| {});
7925 let mut cx = EditorTestContext::new(cx).await;
7926
7927 cx.set_state(indoc!(
7928 r#"line onˇe
7929 liˇne two
7930 line three
7931 line four"#
7932 ));
7933
7934 cx.update_editor(|editor, window, cx| {
7935 editor.add_selection_below(&Default::default(), window, cx);
7936 editor.add_selection_below(&Default::default(), window, cx);
7937 editor.add_selection_below(&Default::default(), window, cx);
7938 });
7939
7940 // initial state with two multi cursor groups
7941 cx.assert_editor_state(indoc!(
7942 r#"line onˇe
7943 liˇne twˇo
7944 liˇne thˇree
7945 liˇne foˇur"#
7946 ));
7947
7948 // add single cursor in middle - simulate opt click
7949 cx.update_editor(|editor, window, cx| {
7950 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
7951 editor.begin_selection(new_cursor_point, true, 1, window, cx);
7952 editor.end_selection(window, cx);
7953 });
7954
7955 cx.assert_editor_state(indoc!(
7956 r#"line onˇe
7957 liˇne twˇo
7958 liˇneˇ thˇree
7959 liˇne foˇur"#
7960 ));
7961
7962 cx.update_editor(|editor, window, cx| {
7963 editor.add_selection_above(&Default::default(), window, cx);
7964 });
7965
7966 // test new added selection expands above and existing selection shrinks
7967 cx.assert_editor_state(indoc!(
7968 r#"line onˇe
7969 liˇneˇ twˇo
7970 liˇneˇ thˇree
7971 line four"#
7972 ));
7973
7974 cx.update_editor(|editor, window, cx| {
7975 editor.add_selection_above(&Default::default(), window, cx);
7976 });
7977
7978 // test new added selection expands above and existing selection shrinks
7979 cx.assert_editor_state(indoc!(
7980 r#"lineˇ onˇe
7981 liˇneˇ twˇo
7982 lineˇ three
7983 line four"#
7984 ));
7985
7986 // intial state with two selection groups
7987 cx.set_state(indoc!(
7988 r#"abcd
7989 ef«ghˇ»
7990 ijkl
7991 «mˇ»nop"#
7992 ));
7993
7994 cx.update_editor(|editor, window, cx| {
7995 editor.add_selection_above(&Default::default(), window, cx);
7996 editor.add_selection_above(&Default::default(), window, cx);
7997 });
7998
7999 cx.assert_editor_state(indoc!(
8000 r#"ab«cdˇ»
8001 «eˇ»f«ghˇ»
8002 «iˇ»jkl
8003 «mˇ»nop"#
8004 ));
8005
8006 // add single selection in middle - simulate opt drag
8007 cx.update_editor(|editor, window, cx| {
8008 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8009 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8010 editor.update_selection(
8011 DisplayPoint::new(DisplayRow(2), 4),
8012 0,
8013 gpui::Point::<f32>::default(),
8014 window,
8015 cx,
8016 );
8017 editor.end_selection(window, cx);
8018 });
8019
8020 cx.assert_editor_state(indoc!(
8021 r#"ab«cdˇ»
8022 «eˇ»f«ghˇ»
8023 «iˇ»jk«lˇ»
8024 «mˇ»nop"#
8025 ));
8026
8027 cx.update_editor(|editor, window, cx| {
8028 editor.add_selection_below(&Default::default(), window, cx);
8029 });
8030
8031 // test new added selection expands below, others shrinks from above
8032 cx.assert_editor_state(indoc!(
8033 r#"abcd
8034 ef«ghˇ»
8035 «iˇ»jk«lˇ»
8036 «mˇ»no«pˇ»"#
8037 ));
8038}
8039
8040#[gpui::test]
8041async fn test_select_next(cx: &mut TestAppContext) {
8042 init_test(cx, |_| {});
8043
8044 let mut cx = EditorTestContext::new(cx).await;
8045 cx.set_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(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8052 .unwrap();
8053 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8054
8055 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8056 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8057
8058 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8059 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
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 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8066 .unwrap();
8067 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8068
8069 // Test selection direction should be preserved
8070 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8071
8072 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8073 .unwrap();
8074 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8075}
8076
8077#[gpui::test]
8078async fn test_select_all_matches(cx: &mut TestAppContext) {
8079 init_test(cx, |_| {});
8080
8081 let mut cx = EditorTestContext::new(cx).await;
8082
8083 // Test caret-only selections
8084 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8085 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8086 .unwrap();
8087 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8088
8089 // Test left-to-right selections
8090 cx.set_state("abc\n«abcˇ»\nabc");
8091 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8092 .unwrap();
8093 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8094
8095 // Test right-to-left selections
8096 cx.set_state("abc\n«ˇabc»\nabc");
8097 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8098 .unwrap();
8099 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8100
8101 // Test selecting whitespace with caret selection
8102 cx.set_state("abc\nˇ abc\nabc");
8103 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8104 .unwrap();
8105 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8106
8107 // Test selecting whitespace with left-to-right selection
8108 cx.set_state("abc\n«ˇ »abc\nabc");
8109 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8110 .unwrap();
8111 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8112
8113 // Test no matches with right-to-left selection
8114 cx.set_state("abc\n« ˇ»abc\nabc");
8115 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8116 .unwrap();
8117 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8118
8119 // Test with a single word and clip_at_line_ends=true (#29823)
8120 cx.set_state("aˇbc");
8121 cx.update_editor(|e, window, cx| {
8122 e.set_clip_at_line_ends(true, cx);
8123 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8124 e.set_clip_at_line_ends(false, cx);
8125 });
8126 cx.assert_editor_state("«abcˇ»");
8127}
8128
8129#[gpui::test]
8130async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8131 init_test(cx, |_| {});
8132
8133 let mut cx = EditorTestContext::new(cx).await;
8134
8135 let large_body_1 = "\nd".repeat(200);
8136 let large_body_2 = "\ne".repeat(200);
8137
8138 cx.set_state(&format!(
8139 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8140 ));
8141 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8142 let scroll_position = editor.scroll_position(cx);
8143 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8144 scroll_position
8145 });
8146
8147 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8148 .unwrap();
8149 cx.assert_editor_state(&format!(
8150 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8151 ));
8152 let scroll_position_after_selection =
8153 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8154 assert_eq!(
8155 initial_scroll_position, scroll_position_after_selection,
8156 "Scroll position should not change after selecting all matches"
8157 );
8158}
8159
8160#[gpui::test]
8161async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8162 init_test(cx, |_| {});
8163
8164 let mut cx = EditorLspTestContext::new_rust(
8165 lsp::ServerCapabilities {
8166 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8167 ..Default::default()
8168 },
8169 cx,
8170 )
8171 .await;
8172
8173 cx.set_state(indoc! {"
8174 line 1
8175 line 2
8176 linˇe 3
8177 line 4
8178 line 5
8179 "});
8180
8181 // Make an edit
8182 cx.update_editor(|editor, window, cx| {
8183 editor.handle_input("X", window, cx);
8184 });
8185
8186 // Move cursor to a different position
8187 cx.update_editor(|editor, window, cx| {
8188 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8189 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8190 });
8191 });
8192
8193 cx.assert_editor_state(indoc! {"
8194 line 1
8195 line 2
8196 linXe 3
8197 line 4
8198 liˇne 5
8199 "});
8200
8201 cx.lsp
8202 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8203 Ok(Some(vec![lsp::TextEdit::new(
8204 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8205 "PREFIX ".to_string(),
8206 )]))
8207 });
8208
8209 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8210 .unwrap()
8211 .await
8212 .unwrap();
8213
8214 cx.assert_editor_state(indoc! {"
8215 PREFIX line 1
8216 line 2
8217 linXe 3
8218 line 4
8219 liˇne 5
8220 "});
8221
8222 // Undo formatting
8223 cx.update_editor(|editor, window, cx| {
8224 editor.undo(&Default::default(), window, cx);
8225 });
8226
8227 // Verify cursor moved back to position after edit
8228 cx.assert_editor_state(indoc! {"
8229 line 1
8230 line 2
8231 linXˇe 3
8232 line 4
8233 line 5
8234 "});
8235}
8236
8237#[gpui::test]
8238async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8239 init_test(cx, |_| {});
8240
8241 let mut cx = EditorTestContext::new(cx).await;
8242
8243 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8244 cx.update_editor(|editor, window, cx| {
8245 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8246 });
8247
8248 cx.set_state(indoc! {"
8249 line 1
8250 line 2
8251 linˇe 3
8252 line 4
8253 line 5
8254 line 6
8255 line 7
8256 line 8
8257 line 9
8258 line 10
8259 "});
8260
8261 let snapshot = cx.buffer_snapshot();
8262 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8263
8264 cx.update(|_, cx| {
8265 provider.update(cx, |provider, _| {
8266 provider.set_edit_prediction(Some(edit_prediction::EditPrediction {
8267 id: None,
8268 edits: vec![(edit_position..edit_position, "X".into())],
8269 edit_preview: None,
8270 }))
8271 })
8272 });
8273
8274 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8275 cx.update_editor(|editor, window, cx| {
8276 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8277 });
8278
8279 cx.assert_editor_state(indoc! {"
8280 line 1
8281 line 2
8282 lineXˇ 3
8283 line 4
8284 line 5
8285 line 6
8286 line 7
8287 line 8
8288 line 9
8289 line 10
8290 "});
8291
8292 cx.update_editor(|editor, window, cx| {
8293 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8294 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8295 });
8296 });
8297
8298 cx.assert_editor_state(indoc! {"
8299 line 1
8300 line 2
8301 lineX 3
8302 line 4
8303 line 5
8304 line 6
8305 line 7
8306 line 8
8307 line 9
8308 liˇne 10
8309 "});
8310
8311 cx.update_editor(|editor, window, cx| {
8312 editor.undo(&Default::default(), window, cx);
8313 });
8314
8315 cx.assert_editor_state(indoc! {"
8316 line 1
8317 line 2
8318 lineˇ 3
8319 line 4
8320 line 5
8321 line 6
8322 line 7
8323 line 8
8324 line 9
8325 line 10
8326 "});
8327}
8328
8329#[gpui::test]
8330async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8331 init_test(cx, |_| {});
8332
8333 let mut cx = EditorTestContext::new(cx).await;
8334 cx.set_state(
8335 r#"let foo = 2;
8336lˇet foo = 2;
8337let fooˇ = 2;
8338let foo = 2;
8339let foo = ˇ2;"#,
8340 );
8341
8342 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8343 .unwrap();
8344 cx.assert_editor_state(
8345 r#"let foo = 2;
8346«letˇ» foo = 2;
8347let «fooˇ» = 2;
8348let foo = 2;
8349let foo = «2ˇ»;"#,
8350 );
8351
8352 // noop for multiple selections with different contents
8353 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8354 .unwrap();
8355 cx.assert_editor_state(
8356 r#"let foo = 2;
8357«letˇ» foo = 2;
8358let «fooˇ» = 2;
8359let foo = 2;
8360let foo = «2ˇ»;"#,
8361 );
8362
8363 // Test last selection direction should be preserved
8364 cx.set_state(
8365 r#"let foo = 2;
8366let foo = 2;
8367let «fooˇ» = 2;
8368let «ˇfoo» = 2;
8369let foo = 2;"#,
8370 );
8371
8372 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8373 .unwrap();
8374 cx.assert_editor_state(
8375 r#"let foo = 2;
8376let foo = 2;
8377let «fooˇ» = 2;
8378let «ˇfoo» = 2;
8379let «ˇfoo» = 2;"#,
8380 );
8381}
8382
8383#[gpui::test]
8384async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8385 init_test(cx, |_| {});
8386
8387 let mut cx =
8388 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8389
8390 cx.assert_editor_state(indoc! {"
8391 ˇbbb
8392 ccc
8393
8394 bbb
8395 ccc
8396 "});
8397 cx.dispatch_action(SelectPrevious::default());
8398 cx.assert_editor_state(indoc! {"
8399 «bbbˇ»
8400 ccc
8401
8402 bbb
8403 ccc
8404 "});
8405 cx.dispatch_action(SelectPrevious::default());
8406 cx.assert_editor_state(indoc! {"
8407 «bbbˇ»
8408 ccc
8409
8410 «bbbˇ»
8411 ccc
8412 "});
8413}
8414
8415#[gpui::test]
8416async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8417 init_test(cx, |_| {});
8418
8419 let mut cx = EditorTestContext::new(cx).await;
8420 cx.set_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(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8427 .unwrap();
8428 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8429
8430 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8431 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8432
8433 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8434 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
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 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8441 .unwrap();
8442 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8443}
8444
8445#[gpui::test]
8446async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8447 init_test(cx, |_| {});
8448
8449 let mut cx = EditorTestContext::new(cx).await;
8450 cx.set_state("aˇ");
8451
8452 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8453 .unwrap();
8454 cx.assert_editor_state("«aˇ»");
8455 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8456 .unwrap();
8457 cx.assert_editor_state("«aˇ»");
8458}
8459
8460#[gpui::test]
8461async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8462 init_test(cx, |_| {});
8463
8464 let mut cx = EditorTestContext::new(cx).await;
8465 cx.set_state(
8466 r#"let foo = 2;
8467lˇet foo = 2;
8468let fooˇ = 2;
8469let foo = 2;
8470let foo = ˇ2;"#,
8471 );
8472
8473 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8474 .unwrap();
8475 cx.assert_editor_state(
8476 r#"let foo = 2;
8477«letˇ» foo = 2;
8478let «fooˇ» = 2;
8479let foo = 2;
8480let foo = «2ˇ»;"#,
8481 );
8482
8483 // noop for multiple selections with different contents
8484 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8485 .unwrap();
8486 cx.assert_editor_state(
8487 r#"let foo = 2;
8488«letˇ» foo = 2;
8489let «fooˇ» = 2;
8490let foo = 2;
8491let foo = «2ˇ»;"#,
8492 );
8493}
8494
8495#[gpui::test]
8496async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8497 init_test(cx, |_| {});
8498
8499 let mut cx = EditorTestContext::new(cx).await;
8500 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8501
8502 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8503 .unwrap();
8504 // selection direction is preserved
8505 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8506
8507 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8508 .unwrap();
8509 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8510
8511 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8512 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8513
8514 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8515 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\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 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8522 .unwrap();
8523 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8524}
8525
8526#[gpui::test]
8527async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8528 init_test(cx, |_| {});
8529
8530 let language = Arc::new(Language::new(
8531 LanguageConfig::default(),
8532 Some(tree_sitter_rust::LANGUAGE.into()),
8533 ));
8534
8535 let text = r#"
8536 use mod1::mod2::{mod3, mod4};
8537
8538 fn fn_1(param1: bool, param2: &str) {
8539 let var1 = "text";
8540 }
8541 "#
8542 .unindent();
8543
8544 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8545 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8546 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8547
8548 editor
8549 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8550 .await;
8551
8552 editor.update_in(cx, |editor, window, cx| {
8553 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8554 s.select_display_ranges([
8555 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8556 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8557 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8558 ]);
8559 });
8560 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8561 });
8562 editor.update(cx, |editor, cx| {
8563 assert_text_with_selections(
8564 editor,
8565 indoc! {r#"
8566 use mod1::mod2::{mod3, «mod4ˇ»};
8567
8568 fn fn_1«ˇ(param1: bool, param2: &str)» {
8569 let var1 = "«ˇtext»";
8570 }
8571 "#},
8572 cx,
8573 );
8574 });
8575
8576 editor.update_in(cx, |editor, window, cx| {
8577 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8578 });
8579 editor.update(cx, |editor, cx| {
8580 assert_text_with_selections(
8581 editor,
8582 indoc! {r#"
8583 use mod1::mod2::«{mod3, mod4}ˇ»;
8584
8585 «ˇfn fn_1(param1: bool, param2: &str) {
8586 let var1 = "text";
8587 }»
8588 "#},
8589 cx,
8590 );
8591 });
8592
8593 editor.update_in(cx, |editor, window, cx| {
8594 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8595 });
8596 assert_eq!(
8597 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8598 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8599 );
8600
8601 // Trying to expand the selected syntax node one more time has no effect.
8602 editor.update_in(cx, |editor, window, cx| {
8603 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8604 });
8605 assert_eq!(
8606 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
8607 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8608 );
8609
8610 editor.update_in(cx, |editor, window, cx| {
8611 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8612 });
8613 editor.update(cx, |editor, cx| {
8614 assert_text_with_selections(
8615 editor,
8616 indoc! {r#"
8617 use mod1::mod2::«{mod3, mod4}ˇ»;
8618
8619 «ˇfn fn_1(param1: bool, param2: &str) {
8620 let var1 = "text";
8621 }»
8622 "#},
8623 cx,
8624 );
8625 });
8626
8627 editor.update_in(cx, |editor, window, cx| {
8628 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8629 });
8630 editor.update(cx, |editor, cx| {
8631 assert_text_with_selections(
8632 editor,
8633 indoc! {r#"
8634 use mod1::mod2::{mod3, «mod4ˇ»};
8635
8636 fn fn_1«ˇ(param1: bool, param2: &str)» {
8637 let var1 = "«ˇtext»";
8638 }
8639 "#},
8640 cx,
8641 );
8642 });
8643
8644 editor.update_in(cx, |editor, window, cx| {
8645 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8646 });
8647 editor.update(cx, |editor, cx| {
8648 assert_text_with_selections(
8649 editor,
8650 indoc! {r#"
8651 use mod1::mod2::{mod3, moˇd4};
8652
8653 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8654 let var1 = "teˇxt";
8655 }
8656 "#},
8657 cx,
8658 );
8659 });
8660
8661 // Trying to shrink the selected syntax node one more time has no effect.
8662 editor.update_in(cx, |editor, window, cx| {
8663 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8664 });
8665 editor.update_in(cx, |editor, _, cx| {
8666 assert_text_with_selections(
8667 editor,
8668 indoc! {r#"
8669 use mod1::mod2::{mod3, moˇd4};
8670
8671 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8672 let var1 = "teˇxt";
8673 }
8674 "#},
8675 cx,
8676 );
8677 });
8678
8679 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8680 // a fold.
8681 editor.update_in(cx, |editor, window, cx| {
8682 editor.fold_creases(
8683 vec![
8684 Crease::simple(
8685 Point::new(0, 21)..Point::new(0, 24),
8686 FoldPlaceholder::test(),
8687 ),
8688 Crease::simple(
8689 Point::new(3, 20)..Point::new(3, 22),
8690 FoldPlaceholder::test(),
8691 ),
8692 ],
8693 true,
8694 window,
8695 cx,
8696 );
8697 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8698 });
8699 editor.update(cx, |editor, cx| {
8700 assert_text_with_selections(
8701 editor,
8702 indoc! {r#"
8703 use mod1::mod2::«{mod3, mod4}ˇ»;
8704
8705 fn fn_1«ˇ(param1: bool, param2: &str)» {
8706 let var1 = "«ˇtext»";
8707 }
8708 "#},
8709 cx,
8710 );
8711 });
8712}
8713
8714#[gpui::test]
8715async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8716 init_test(cx, |_| {});
8717
8718 let language = Arc::new(Language::new(
8719 LanguageConfig::default(),
8720 Some(tree_sitter_rust::LANGUAGE.into()),
8721 ));
8722
8723 let text = "let a = 2;";
8724
8725 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8726 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8727 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8728
8729 editor
8730 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8731 .await;
8732
8733 // Test case 1: Cursor at end of word
8734 editor.update_in(cx, |editor, window, cx| {
8735 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8736 s.select_display_ranges([
8737 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
8738 ]);
8739 });
8740 });
8741 editor.update(cx, |editor, cx| {
8742 assert_text_with_selections(editor, "let aˇ = 2;", cx);
8743 });
8744 editor.update_in(cx, |editor, window, cx| {
8745 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8746 });
8747 editor.update(cx, |editor, cx| {
8748 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
8749 });
8750 editor.update_in(cx, |editor, window, cx| {
8751 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8752 });
8753 editor.update(cx, |editor, cx| {
8754 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8755 });
8756
8757 // Test case 2: Cursor at end of statement
8758 editor.update_in(cx, |editor, window, cx| {
8759 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8760 s.select_display_ranges([
8761 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
8762 ]);
8763 });
8764 });
8765 editor.update(cx, |editor, cx| {
8766 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
8767 });
8768 editor.update_in(cx, |editor, window, cx| {
8769 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8770 });
8771 editor.update(cx, |editor, cx| {
8772 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
8773 });
8774}
8775
8776#[gpui::test]
8777async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
8778 init_test(cx, |_| {});
8779
8780 let language = Arc::new(Language::new(
8781 LanguageConfig {
8782 name: "JavaScript".into(),
8783 ..Default::default()
8784 },
8785 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
8786 ));
8787
8788 let text = r#"
8789 let a = {
8790 key: "value",
8791 };
8792 "#
8793 .unindent();
8794
8795 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8796 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8797 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8798
8799 editor
8800 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8801 .await;
8802
8803 // Test case 1: Cursor after '{'
8804 editor.update_in(cx, |editor, window, cx| {
8805 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8806 s.select_display_ranges([
8807 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
8808 ]);
8809 });
8810 });
8811 editor.update(cx, |editor, cx| {
8812 assert_text_with_selections(
8813 editor,
8814 indoc! {r#"
8815 let a = {ˇ
8816 key: "value",
8817 };
8818 "#},
8819 cx,
8820 );
8821 });
8822 editor.update_in(cx, |editor, window, cx| {
8823 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8824 });
8825 editor.update(cx, |editor, cx| {
8826 assert_text_with_selections(
8827 editor,
8828 indoc! {r#"
8829 let a = «ˇ{
8830 key: "value",
8831 }»;
8832 "#},
8833 cx,
8834 );
8835 });
8836
8837 // Test case 2: Cursor after ':'
8838 editor.update_in(cx, |editor, window, cx| {
8839 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8840 s.select_display_ranges([
8841 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
8842 ]);
8843 });
8844 });
8845 editor.update(cx, |editor, cx| {
8846 assert_text_with_selections(
8847 editor,
8848 indoc! {r#"
8849 let a = {
8850 key:ˇ "value",
8851 };
8852 "#},
8853 cx,
8854 );
8855 });
8856 editor.update_in(cx, |editor, window, cx| {
8857 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8858 });
8859 editor.update(cx, |editor, cx| {
8860 assert_text_with_selections(
8861 editor,
8862 indoc! {r#"
8863 let a = {
8864 «ˇkey: "value"»,
8865 };
8866 "#},
8867 cx,
8868 );
8869 });
8870 editor.update_in(cx, |editor, window, cx| {
8871 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8872 });
8873 editor.update(cx, |editor, cx| {
8874 assert_text_with_selections(
8875 editor,
8876 indoc! {r#"
8877 let a = «ˇ{
8878 key: "value",
8879 }»;
8880 "#},
8881 cx,
8882 );
8883 });
8884
8885 // Test case 3: Cursor after ','
8886 editor.update_in(cx, |editor, window, cx| {
8887 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8888 s.select_display_ranges([
8889 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
8890 ]);
8891 });
8892 });
8893 editor.update(cx, |editor, cx| {
8894 assert_text_with_selections(
8895 editor,
8896 indoc! {r#"
8897 let a = {
8898 key: "value",ˇ
8899 };
8900 "#},
8901 cx,
8902 );
8903 });
8904 editor.update_in(cx, |editor, window, cx| {
8905 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8906 });
8907 editor.update(cx, |editor, cx| {
8908 assert_text_with_selections(
8909 editor,
8910 indoc! {r#"
8911 let a = «ˇ{
8912 key: "value",
8913 }»;
8914 "#},
8915 cx,
8916 );
8917 });
8918
8919 // Test case 4: Cursor after ';'
8920 editor.update_in(cx, |editor, window, cx| {
8921 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8922 s.select_display_ranges([
8923 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
8924 ]);
8925 });
8926 });
8927 editor.update(cx, |editor, cx| {
8928 assert_text_with_selections(
8929 editor,
8930 indoc! {r#"
8931 let a = {
8932 key: "value",
8933 };ˇ
8934 "#},
8935 cx,
8936 );
8937 });
8938 editor.update_in(cx, |editor, window, cx| {
8939 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8940 });
8941 editor.update(cx, |editor, cx| {
8942 assert_text_with_selections(
8943 editor,
8944 indoc! {r#"
8945 «ˇlet a = {
8946 key: "value",
8947 };
8948 »"#},
8949 cx,
8950 );
8951 });
8952}
8953
8954#[gpui::test]
8955async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
8956 init_test(cx, |_| {});
8957
8958 let language = Arc::new(Language::new(
8959 LanguageConfig::default(),
8960 Some(tree_sitter_rust::LANGUAGE.into()),
8961 ));
8962
8963 let text = r#"
8964 use mod1::mod2::{mod3, mod4};
8965
8966 fn fn_1(param1: bool, param2: &str) {
8967 let var1 = "hello world";
8968 }
8969 "#
8970 .unindent();
8971
8972 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8973 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8974 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8975
8976 editor
8977 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8978 .await;
8979
8980 // Test 1: Cursor on a letter of a string word
8981 editor.update_in(cx, |editor, window, cx| {
8982 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8983 s.select_display_ranges([
8984 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
8985 ]);
8986 });
8987 });
8988 editor.update_in(cx, |editor, window, cx| {
8989 assert_text_with_selections(
8990 editor,
8991 indoc! {r#"
8992 use mod1::mod2::{mod3, mod4};
8993
8994 fn fn_1(param1: bool, param2: &str) {
8995 let var1 = "hˇello world";
8996 }
8997 "#},
8998 cx,
8999 );
9000 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9001 assert_text_with_selections(
9002 editor,
9003 indoc! {r#"
9004 use mod1::mod2::{mod3, mod4};
9005
9006 fn fn_1(param1: bool, param2: &str) {
9007 let var1 = "«ˇhello» world";
9008 }
9009 "#},
9010 cx,
9011 );
9012 });
9013
9014 // Test 2: Partial selection within a word
9015 editor.update_in(cx, |editor, window, cx| {
9016 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9017 s.select_display_ranges([
9018 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9019 ]);
9020 });
9021 });
9022 editor.update_in(cx, |editor, window, cx| {
9023 assert_text_with_selections(
9024 editor,
9025 indoc! {r#"
9026 use mod1::mod2::{mod3, mod4};
9027
9028 fn fn_1(param1: bool, param2: &str) {
9029 let var1 = "h«elˇ»lo world";
9030 }
9031 "#},
9032 cx,
9033 );
9034 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9035 assert_text_with_selections(
9036 editor,
9037 indoc! {r#"
9038 use mod1::mod2::{mod3, mod4};
9039
9040 fn fn_1(param1: bool, param2: &str) {
9041 let var1 = "«ˇhello» world";
9042 }
9043 "#},
9044 cx,
9045 );
9046 });
9047
9048 // Test 3: Complete word already selected
9049 editor.update_in(cx, |editor, window, cx| {
9050 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9051 s.select_display_ranges([
9052 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9053 ]);
9054 });
9055 });
9056 editor.update_in(cx, |editor, window, cx| {
9057 assert_text_with_selections(
9058 editor,
9059 indoc! {r#"
9060 use mod1::mod2::{mod3, mod4};
9061
9062 fn fn_1(param1: bool, param2: &str) {
9063 let var1 = "«helloˇ» world";
9064 }
9065 "#},
9066 cx,
9067 );
9068 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9069 assert_text_with_selections(
9070 editor,
9071 indoc! {r#"
9072 use mod1::mod2::{mod3, mod4};
9073
9074 fn fn_1(param1: bool, param2: &str) {
9075 let var1 = "«hello worldˇ»";
9076 }
9077 "#},
9078 cx,
9079 );
9080 });
9081
9082 // Test 4: Selection spanning across words
9083 editor.update_in(cx, |editor, window, cx| {
9084 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9085 s.select_display_ranges([
9086 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9087 ]);
9088 });
9089 });
9090 editor.update_in(cx, |editor, window, cx| {
9091 assert_text_with_selections(
9092 editor,
9093 indoc! {r#"
9094 use mod1::mod2::{mod3, mod4};
9095
9096 fn fn_1(param1: bool, param2: &str) {
9097 let var1 = "hel«lo woˇ»rld";
9098 }
9099 "#},
9100 cx,
9101 );
9102 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9103 assert_text_with_selections(
9104 editor,
9105 indoc! {r#"
9106 use mod1::mod2::{mod3, mod4};
9107
9108 fn fn_1(param1: bool, param2: &str) {
9109 let var1 = "«ˇhello world»";
9110 }
9111 "#},
9112 cx,
9113 );
9114 });
9115
9116 // Test 5: Expansion beyond string
9117 editor.update_in(cx, |editor, window, cx| {
9118 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9119 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9120 assert_text_with_selections(
9121 editor,
9122 indoc! {r#"
9123 use mod1::mod2::{mod3, mod4};
9124
9125 fn fn_1(param1: bool, param2: &str) {
9126 «ˇlet var1 = "hello world";»
9127 }
9128 "#},
9129 cx,
9130 );
9131 });
9132}
9133
9134#[gpui::test]
9135async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9136 init_test(cx, |_| {});
9137
9138 let mut cx = EditorTestContext::new(cx).await;
9139
9140 let language = Arc::new(Language::new(
9141 LanguageConfig::default(),
9142 Some(tree_sitter_rust::LANGUAGE.into()),
9143 ));
9144
9145 cx.update_buffer(|buffer, cx| {
9146 buffer.set_language(Some(language), cx);
9147 });
9148
9149 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9150 cx.update_editor(|editor, window, cx| {
9151 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9152 });
9153
9154 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9155
9156 cx.set_state(indoc! { r#"fn a() {
9157 // what
9158 // a
9159 // ˇlong
9160 // method
9161 // I
9162 // sure
9163 // hope
9164 // it
9165 // works
9166 }"# });
9167
9168 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9169 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9170 cx.update(|_, cx| {
9171 multi_buffer.update(cx, |multi_buffer, cx| {
9172 multi_buffer.set_excerpts_for_path(
9173 PathKey::for_buffer(&buffer, cx),
9174 buffer,
9175 [Point::new(1, 0)..Point::new(1, 0)],
9176 3,
9177 cx,
9178 );
9179 });
9180 });
9181
9182 let editor2 = cx.new_window_entity(|window, cx| {
9183 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9184 });
9185
9186 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9187 cx.update_editor(|editor, window, cx| {
9188 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9189 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9190 })
9191 });
9192
9193 cx.assert_editor_state(indoc! { "
9194 fn a() {
9195 // what
9196 // a
9197 ˇ // long
9198 // method"});
9199
9200 cx.update_editor(|editor, window, cx| {
9201 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9202 });
9203
9204 // Although we could potentially make the action work when the syntax node
9205 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9206 // did. Maybe we could also expand the excerpt to contain the range?
9207 cx.assert_editor_state(indoc! { "
9208 fn a() {
9209 // what
9210 // a
9211 ˇ // long
9212 // method"});
9213}
9214
9215#[gpui::test]
9216async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9217 init_test(cx, |_| {});
9218
9219 let base_text = r#"
9220 impl A {
9221 // this is an uncommitted comment
9222
9223 fn b() {
9224 c();
9225 }
9226
9227 // this is another uncommitted comment
9228
9229 fn d() {
9230 // e
9231 // f
9232 }
9233 }
9234
9235 fn g() {
9236 // h
9237 }
9238 "#
9239 .unindent();
9240
9241 let text = r#"
9242 ˇimpl A {
9243
9244 fn b() {
9245 c();
9246 }
9247
9248 fn d() {
9249 // e
9250 // f
9251 }
9252 }
9253
9254 fn g() {
9255 // h
9256 }
9257 "#
9258 .unindent();
9259
9260 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9261 cx.set_state(&text);
9262 cx.set_head_text(&base_text);
9263 cx.update_editor(|editor, window, cx| {
9264 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9265 });
9266
9267 cx.assert_state_with_diff(
9268 "
9269 ˇimpl A {
9270 - // this is an uncommitted comment
9271
9272 fn b() {
9273 c();
9274 }
9275
9276 - // this is another uncommitted comment
9277 -
9278 fn d() {
9279 // e
9280 // f
9281 }
9282 }
9283
9284 fn g() {
9285 // h
9286 }
9287 "
9288 .unindent(),
9289 );
9290
9291 let expected_display_text = "
9292 impl A {
9293 // this is an uncommitted comment
9294
9295 fn b() {
9296 ⋯
9297 }
9298
9299 // this is another uncommitted comment
9300
9301 fn d() {
9302 ⋯
9303 }
9304 }
9305
9306 fn g() {
9307 ⋯
9308 }
9309 "
9310 .unindent();
9311
9312 cx.update_editor(|editor, window, cx| {
9313 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9314 assert_eq!(editor.display_text(cx), expected_display_text);
9315 });
9316}
9317
9318#[gpui::test]
9319async fn test_autoindent(cx: &mut TestAppContext) {
9320 init_test(cx, |_| {});
9321
9322 let language = Arc::new(
9323 Language::new(
9324 LanguageConfig {
9325 brackets: BracketPairConfig {
9326 pairs: vec![
9327 BracketPair {
9328 start: "{".to_string(),
9329 end: "}".to_string(),
9330 close: false,
9331 surround: false,
9332 newline: true,
9333 },
9334 BracketPair {
9335 start: "(".to_string(),
9336 end: ")".to_string(),
9337 close: false,
9338 surround: false,
9339 newline: true,
9340 },
9341 ],
9342 ..Default::default()
9343 },
9344 ..Default::default()
9345 },
9346 Some(tree_sitter_rust::LANGUAGE.into()),
9347 )
9348 .with_indents_query(
9349 r#"
9350 (_ "(" ")" @end) @indent
9351 (_ "{" "}" @end) @indent
9352 "#,
9353 )
9354 .unwrap(),
9355 );
9356
9357 let text = "fn a() {}";
9358
9359 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9360 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9361 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9362 editor
9363 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9364 .await;
9365
9366 editor.update_in(cx, |editor, window, cx| {
9367 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9368 s.select_ranges([5..5, 8..8, 9..9])
9369 });
9370 editor.newline(&Newline, window, cx);
9371 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9372 assert_eq!(
9373 editor.selections.ranges(cx),
9374 &[
9375 Point::new(1, 4)..Point::new(1, 4),
9376 Point::new(3, 4)..Point::new(3, 4),
9377 Point::new(5, 0)..Point::new(5, 0)
9378 ]
9379 );
9380 });
9381}
9382
9383#[gpui::test]
9384async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9385 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9386
9387 let language = Arc::new(
9388 Language::new(
9389 LanguageConfig {
9390 brackets: BracketPairConfig {
9391 pairs: vec![
9392 BracketPair {
9393 start: "{".to_string(),
9394 end: "}".to_string(),
9395 close: false,
9396 surround: false,
9397 newline: true,
9398 },
9399 BracketPair {
9400 start: "(".to_string(),
9401 end: ")".to_string(),
9402 close: false,
9403 surround: false,
9404 newline: true,
9405 },
9406 ],
9407 ..Default::default()
9408 },
9409 ..Default::default()
9410 },
9411 Some(tree_sitter_rust::LANGUAGE.into()),
9412 )
9413 .with_indents_query(
9414 r#"
9415 (_ "(" ")" @end) @indent
9416 (_ "{" "}" @end) @indent
9417 "#,
9418 )
9419 .unwrap(),
9420 );
9421
9422 let text = "fn a() {}";
9423
9424 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9425 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9426 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9427 editor
9428 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9429 .await;
9430
9431 editor.update_in(cx, |editor, window, cx| {
9432 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9433 s.select_ranges([5..5, 8..8, 9..9])
9434 });
9435 editor.newline(&Newline, window, cx);
9436 assert_eq!(
9437 editor.text(cx),
9438 indoc!(
9439 "
9440 fn a(
9441
9442 ) {
9443
9444 }
9445 "
9446 )
9447 );
9448 assert_eq!(
9449 editor.selections.ranges(cx),
9450 &[
9451 Point::new(1, 0)..Point::new(1, 0),
9452 Point::new(3, 0)..Point::new(3, 0),
9453 Point::new(5, 0)..Point::new(5, 0)
9454 ]
9455 );
9456 });
9457}
9458
9459#[gpui::test]
9460async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9461 init_test(cx, |settings| {
9462 settings.defaults.auto_indent = Some(true);
9463 settings.languages.0.insert(
9464 "python".into(),
9465 LanguageSettingsContent {
9466 auto_indent: Some(false),
9467 ..Default::default()
9468 },
9469 );
9470 });
9471
9472 let mut cx = EditorTestContext::new(cx).await;
9473
9474 let injected_language = Arc::new(
9475 Language::new(
9476 LanguageConfig {
9477 brackets: BracketPairConfig {
9478 pairs: vec![
9479 BracketPair {
9480 start: "{".to_string(),
9481 end: "}".to_string(),
9482 close: false,
9483 surround: false,
9484 newline: true,
9485 },
9486 BracketPair {
9487 start: "(".to_string(),
9488 end: ")".to_string(),
9489 close: true,
9490 surround: false,
9491 newline: true,
9492 },
9493 ],
9494 ..Default::default()
9495 },
9496 name: "python".into(),
9497 ..Default::default()
9498 },
9499 Some(tree_sitter_python::LANGUAGE.into()),
9500 )
9501 .with_indents_query(
9502 r#"
9503 (_ "(" ")" @end) @indent
9504 (_ "{" "}" @end) @indent
9505 "#,
9506 )
9507 .unwrap(),
9508 );
9509
9510 let language = Arc::new(
9511 Language::new(
9512 LanguageConfig {
9513 brackets: BracketPairConfig {
9514 pairs: vec![
9515 BracketPair {
9516 start: "{".to_string(),
9517 end: "}".to_string(),
9518 close: false,
9519 surround: false,
9520 newline: true,
9521 },
9522 BracketPair {
9523 start: "(".to_string(),
9524 end: ")".to_string(),
9525 close: true,
9526 surround: false,
9527 newline: true,
9528 },
9529 ],
9530 ..Default::default()
9531 },
9532 name: LanguageName::new("rust"),
9533 ..Default::default()
9534 },
9535 Some(tree_sitter_rust::LANGUAGE.into()),
9536 )
9537 .with_indents_query(
9538 r#"
9539 (_ "(" ")" @end) @indent
9540 (_ "{" "}" @end) @indent
9541 "#,
9542 )
9543 .unwrap()
9544 .with_injection_query(
9545 r#"
9546 (macro_invocation
9547 macro: (identifier) @_macro_name
9548 (token_tree) @injection.content
9549 (#set! injection.language "python"))
9550 "#,
9551 )
9552 .unwrap(),
9553 );
9554
9555 cx.language_registry().add(injected_language);
9556 cx.language_registry().add(language.clone());
9557
9558 cx.update_buffer(|buffer, cx| {
9559 buffer.set_language(Some(language), cx);
9560 });
9561
9562 cx.set_state(r#"struct A {ˇ}"#);
9563
9564 cx.update_editor(|editor, window, cx| {
9565 editor.newline(&Default::default(), window, cx);
9566 });
9567
9568 cx.assert_editor_state(indoc!(
9569 "struct A {
9570 ˇ
9571 }"
9572 ));
9573
9574 cx.set_state(r#"select_biased!(ˇ)"#);
9575
9576 cx.update_editor(|editor, window, cx| {
9577 editor.newline(&Default::default(), window, cx);
9578 editor.handle_input("def ", window, cx);
9579 editor.handle_input("(", window, cx);
9580 editor.newline(&Default::default(), window, cx);
9581 editor.handle_input("a", window, cx);
9582 });
9583
9584 cx.assert_editor_state(indoc!(
9585 "select_biased!(
9586 def (
9587 aˇ
9588 )
9589 )"
9590 ));
9591}
9592
9593#[gpui::test]
9594async fn test_autoindent_selections(cx: &mut TestAppContext) {
9595 init_test(cx, |_| {});
9596
9597 {
9598 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9599 cx.set_state(indoc! {"
9600 impl A {
9601
9602 fn b() {}
9603
9604 «fn c() {
9605
9606 }ˇ»
9607 }
9608 "});
9609
9610 cx.update_editor(|editor, window, cx| {
9611 editor.autoindent(&Default::default(), window, cx);
9612 });
9613
9614 cx.assert_editor_state(indoc! {"
9615 impl A {
9616
9617 fn b() {}
9618
9619 «fn c() {
9620
9621 }ˇ»
9622 }
9623 "});
9624 }
9625
9626 {
9627 let mut cx = EditorTestContext::new_multibuffer(
9628 cx,
9629 [indoc! { "
9630 impl A {
9631 «
9632 // a
9633 fn b(){}
9634 »
9635 «
9636 }
9637 fn c(){}
9638 »
9639 "}],
9640 );
9641
9642 let buffer = cx.update_editor(|editor, _, cx| {
9643 let buffer = editor.buffer().update(cx, |buffer, _| {
9644 buffer.all_buffers().iter().next().unwrap().clone()
9645 });
9646 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9647 buffer
9648 });
9649
9650 cx.run_until_parked();
9651 cx.update_editor(|editor, window, cx| {
9652 editor.select_all(&Default::default(), window, cx);
9653 editor.autoindent(&Default::default(), window, cx)
9654 });
9655 cx.run_until_parked();
9656
9657 cx.update(|_, cx| {
9658 assert_eq!(
9659 buffer.read(cx).text(),
9660 indoc! { "
9661 impl A {
9662
9663 // a
9664 fn b(){}
9665
9666
9667 }
9668 fn c(){}
9669
9670 " }
9671 )
9672 });
9673 }
9674}
9675
9676#[gpui::test]
9677async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9678 init_test(cx, |_| {});
9679
9680 let mut cx = EditorTestContext::new(cx).await;
9681
9682 let language = Arc::new(Language::new(
9683 LanguageConfig {
9684 brackets: BracketPairConfig {
9685 pairs: vec![
9686 BracketPair {
9687 start: "{".to_string(),
9688 end: "}".to_string(),
9689 close: true,
9690 surround: true,
9691 newline: true,
9692 },
9693 BracketPair {
9694 start: "(".to_string(),
9695 end: ")".to_string(),
9696 close: true,
9697 surround: true,
9698 newline: true,
9699 },
9700 BracketPair {
9701 start: "/*".to_string(),
9702 end: " */".to_string(),
9703 close: true,
9704 surround: true,
9705 newline: true,
9706 },
9707 BracketPair {
9708 start: "[".to_string(),
9709 end: "]".to_string(),
9710 close: false,
9711 surround: false,
9712 newline: true,
9713 },
9714 BracketPair {
9715 start: "\"".to_string(),
9716 end: "\"".to_string(),
9717 close: true,
9718 surround: true,
9719 newline: false,
9720 },
9721 BracketPair {
9722 start: "<".to_string(),
9723 end: ">".to_string(),
9724 close: false,
9725 surround: true,
9726 newline: true,
9727 },
9728 ],
9729 ..Default::default()
9730 },
9731 autoclose_before: "})]".to_string(),
9732 ..Default::default()
9733 },
9734 Some(tree_sitter_rust::LANGUAGE.into()),
9735 ));
9736
9737 cx.language_registry().add(language.clone());
9738 cx.update_buffer(|buffer, cx| {
9739 buffer.set_language(Some(language), cx);
9740 });
9741
9742 cx.set_state(
9743 &r#"
9744 🏀ˇ
9745 εˇ
9746 ❤️ˇ
9747 "#
9748 .unindent(),
9749 );
9750
9751 // autoclose multiple nested brackets at multiple cursors
9752 cx.update_editor(|editor, window, cx| {
9753 editor.handle_input("{", window, cx);
9754 editor.handle_input("{", window, cx);
9755 editor.handle_input("{", window, cx);
9756 });
9757 cx.assert_editor_state(
9758 &"
9759 🏀{{{ˇ}}}
9760 ε{{{ˇ}}}
9761 ❤️{{{ˇ}}}
9762 "
9763 .unindent(),
9764 );
9765
9766 // insert a different closing bracket
9767 cx.update_editor(|editor, window, cx| {
9768 editor.handle_input(")", window, cx);
9769 });
9770 cx.assert_editor_state(
9771 &"
9772 🏀{{{)ˇ}}}
9773 ε{{{)ˇ}}}
9774 ❤️{{{)ˇ}}}
9775 "
9776 .unindent(),
9777 );
9778
9779 // skip over the auto-closed brackets when typing a closing bracket
9780 cx.update_editor(|editor, window, cx| {
9781 editor.move_right(&MoveRight, window, cx);
9782 editor.handle_input("}", window, cx);
9783 editor.handle_input("}", window, cx);
9784 editor.handle_input("}", window, cx);
9785 });
9786 cx.assert_editor_state(
9787 &"
9788 🏀{{{)}}}}ˇ
9789 ε{{{)}}}}ˇ
9790 ❤️{{{)}}}}ˇ
9791 "
9792 .unindent(),
9793 );
9794
9795 // autoclose multi-character pairs
9796 cx.set_state(
9797 &"
9798 ˇ
9799 ˇ
9800 "
9801 .unindent(),
9802 );
9803 cx.update_editor(|editor, window, cx| {
9804 editor.handle_input("/", window, cx);
9805 editor.handle_input("*", window, cx);
9806 });
9807 cx.assert_editor_state(
9808 &"
9809 /*ˇ */
9810 /*ˇ */
9811 "
9812 .unindent(),
9813 );
9814
9815 // one cursor autocloses a multi-character pair, one cursor
9816 // does not autoclose.
9817 cx.set_state(
9818 &"
9819 /ˇ
9820 ˇ
9821 "
9822 .unindent(),
9823 );
9824 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
9825 cx.assert_editor_state(
9826 &"
9827 /*ˇ */
9828 *ˇ
9829 "
9830 .unindent(),
9831 );
9832
9833 // Don't autoclose if the next character isn't whitespace and isn't
9834 // listed in the language's "autoclose_before" section.
9835 cx.set_state("ˇa b");
9836 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9837 cx.assert_editor_state("{ˇa b");
9838
9839 // Don't autoclose if `close` is false for the bracket pair
9840 cx.set_state("ˇ");
9841 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
9842 cx.assert_editor_state("[ˇ");
9843
9844 // Surround with brackets if text is selected
9845 cx.set_state("«aˇ» b");
9846 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9847 cx.assert_editor_state("{«aˇ»} b");
9848
9849 // Autoclose when not immediately after a word character
9850 cx.set_state("a ˇ");
9851 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9852 cx.assert_editor_state("a \"ˇ\"");
9853
9854 // Autoclose pair where the start and end characters are the same
9855 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9856 cx.assert_editor_state("a \"\"ˇ");
9857
9858 // Don't autoclose when immediately after a word character
9859 cx.set_state("aˇ");
9860 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9861 cx.assert_editor_state("a\"ˇ");
9862
9863 // Do autoclose when after a non-word character
9864 cx.set_state("{ˇ");
9865 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
9866 cx.assert_editor_state("{\"ˇ\"");
9867
9868 // Non identical pairs autoclose regardless of preceding character
9869 cx.set_state("aˇ");
9870 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
9871 cx.assert_editor_state("a{ˇ}");
9872
9873 // Don't autoclose pair if autoclose is disabled
9874 cx.set_state("ˇ");
9875 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9876 cx.assert_editor_state("<ˇ");
9877
9878 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
9879 cx.set_state("«aˇ» b");
9880 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
9881 cx.assert_editor_state("<«aˇ»> b");
9882}
9883
9884#[gpui::test]
9885async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
9886 init_test(cx, |settings| {
9887 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
9888 });
9889
9890 let mut cx = EditorTestContext::new(cx).await;
9891
9892 let language = Arc::new(Language::new(
9893 LanguageConfig {
9894 brackets: BracketPairConfig {
9895 pairs: vec![
9896 BracketPair {
9897 start: "{".to_string(),
9898 end: "}".to_string(),
9899 close: true,
9900 surround: true,
9901 newline: true,
9902 },
9903 BracketPair {
9904 start: "(".to_string(),
9905 end: ")".to_string(),
9906 close: true,
9907 surround: true,
9908 newline: true,
9909 },
9910 BracketPair {
9911 start: "[".to_string(),
9912 end: "]".to_string(),
9913 close: false,
9914 surround: false,
9915 newline: true,
9916 },
9917 ],
9918 ..Default::default()
9919 },
9920 autoclose_before: "})]".to_string(),
9921 ..Default::default()
9922 },
9923 Some(tree_sitter_rust::LANGUAGE.into()),
9924 ));
9925
9926 cx.language_registry().add(language.clone());
9927 cx.update_buffer(|buffer, cx| {
9928 buffer.set_language(Some(language), cx);
9929 });
9930
9931 cx.set_state(
9932 &"
9933 ˇ
9934 ˇ
9935 ˇ
9936 "
9937 .unindent(),
9938 );
9939
9940 // ensure only matching closing brackets are skipped over
9941 cx.update_editor(|editor, window, cx| {
9942 editor.handle_input("}", window, cx);
9943 editor.move_left(&MoveLeft, window, cx);
9944 editor.handle_input(")", window, cx);
9945 editor.move_left(&MoveLeft, window, cx);
9946 });
9947 cx.assert_editor_state(
9948 &"
9949 ˇ)}
9950 ˇ)}
9951 ˇ)}
9952 "
9953 .unindent(),
9954 );
9955
9956 // skip-over closing brackets at multiple cursors
9957 cx.update_editor(|editor, window, cx| {
9958 editor.handle_input(")", window, cx);
9959 editor.handle_input("}", window, cx);
9960 });
9961 cx.assert_editor_state(
9962 &"
9963 )}ˇ
9964 )}ˇ
9965 )}ˇ
9966 "
9967 .unindent(),
9968 );
9969
9970 // ignore non-close brackets
9971 cx.update_editor(|editor, window, cx| {
9972 editor.handle_input("]", window, cx);
9973 editor.move_left(&MoveLeft, window, cx);
9974 editor.handle_input("]", window, cx);
9975 });
9976 cx.assert_editor_state(
9977 &"
9978 )}]ˇ]
9979 )}]ˇ]
9980 )}]ˇ]
9981 "
9982 .unindent(),
9983 );
9984}
9985
9986#[gpui::test]
9987async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
9988 init_test(cx, |_| {});
9989
9990 let mut cx = EditorTestContext::new(cx).await;
9991
9992 let html_language = Arc::new(
9993 Language::new(
9994 LanguageConfig {
9995 name: "HTML".into(),
9996 brackets: BracketPairConfig {
9997 pairs: vec![
9998 BracketPair {
9999 start: "<".into(),
10000 end: ">".into(),
10001 close: true,
10002 ..Default::default()
10003 },
10004 BracketPair {
10005 start: "{".into(),
10006 end: "}".into(),
10007 close: true,
10008 ..Default::default()
10009 },
10010 BracketPair {
10011 start: "(".into(),
10012 end: ")".into(),
10013 close: true,
10014 ..Default::default()
10015 },
10016 ],
10017 ..Default::default()
10018 },
10019 autoclose_before: "})]>".into(),
10020 ..Default::default()
10021 },
10022 Some(tree_sitter_html::LANGUAGE.into()),
10023 )
10024 .with_injection_query(
10025 r#"
10026 (script_element
10027 (raw_text) @injection.content
10028 (#set! injection.language "javascript"))
10029 "#,
10030 )
10031 .unwrap(),
10032 );
10033
10034 let javascript_language = Arc::new(Language::new(
10035 LanguageConfig {
10036 name: "JavaScript".into(),
10037 brackets: BracketPairConfig {
10038 pairs: vec![
10039 BracketPair {
10040 start: "/*".into(),
10041 end: " */".into(),
10042 close: true,
10043 ..Default::default()
10044 },
10045 BracketPair {
10046 start: "{".into(),
10047 end: "}".into(),
10048 close: true,
10049 ..Default::default()
10050 },
10051 BracketPair {
10052 start: "(".into(),
10053 end: ")".into(),
10054 close: true,
10055 ..Default::default()
10056 },
10057 ],
10058 ..Default::default()
10059 },
10060 autoclose_before: "})]>".into(),
10061 ..Default::default()
10062 },
10063 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10064 ));
10065
10066 cx.language_registry().add(html_language.clone());
10067 cx.language_registry().add(javascript_language);
10068 cx.executor().run_until_parked();
10069
10070 cx.update_buffer(|buffer, cx| {
10071 buffer.set_language(Some(html_language), cx);
10072 });
10073
10074 cx.set_state(
10075 &r#"
10076 <body>ˇ
10077 <script>
10078 var x = 1;ˇ
10079 </script>
10080 </body>ˇ
10081 "#
10082 .unindent(),
10083 );
10084
10085 // Precondition: different languages are active at different locations.
10086 cx.update_editor(|editor, window, cx| {
10087 let snapshot = editor.snapshot(window, cx);
10088 let cursors = editor.selections.ranges::<usize>(cx);
10089 let languages = cursors
10090 .iter()
10091 .map(|c| snapshot.language_at(c.start).unwrap().name())
10092 .collect::<Vec<_>>();
10093 assert_eq!(
10094 languages,
10095 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10096 );
10097 });
10098
10099 // Angle brackets autoclose in HTML, but not JavaScript.
10100 cx.update_editor(|editor, window, cx| {
10101 editor.handle_input("<", window, cx);
10102 editor.handle_input("a", window, cx);
10103 });
10104 cx.assert_editor_state(
10105 &r#"
10106 <body><aˇ>
10107 <script>
10108 var x = 1;<aˇ
10109 </script>
10110 </body><aˇ>
10111 "#
10112 .unindent(),
10113 );
10114
10115 // Curly braces and parens autoclose in both HTML and JavaScript.
10116 cx.update_editor(|editor, window, cx| {
10117 editor.handle_input(" b=", window, cx);
10118 editor.handle_input("{", window, cx);
10119 editor.handle_input("c", window, cx);
10120 editor.handle_input("(", window, cx);
10121 });
10122 cx.assert_editor_state(
10123 &r#"
10124 <body><a b={c(ˇ)}>
10125 <script>
10126 var x = 1;<a b={c(ˇ)}
10127 </script>
10128 </body><a b={c(ˇ)}>
10129 "#
10130 .unindent(),
10131 );
10132
10133 // Brackets that were already autoclosed are skipped.
10134 cx.update_editor(|editor, window, cx| {
10135 editor.handle_input(")", window, cx);
10136 editor.handle_input("d", window, cx);
10137 editor.handle_input("}", window, cx);
10138 });
10139 cx.assert_editor_state(
10140 &r#"
10141 <body><a b={c()d}ˇ>
10142 <script>
10143 var x = 1;<a b={c()d}ˇ
10144 </script>
10145 </body><a b={c()d}ˇ>
10146 "#
10147 .unindent(),
10148 );
10149 cx.update_editor(|editor, window, cx| {
10150 editor.handle_input(">", window, cx);
10151 });
10152 cx.assert_editor_state(
10153 &r#"
10154 <body><a b={c()d}>ˇ
10155 <script>
10156 var x = 1;<a b={c()d}>ˇ
10157 </script>
10158 </body><a b={c()d}>ˇ
10159 "#
10160 .unindent(),
10161 );
10162
10163 // Reset
10164 cx.set_state(
10165 &r#"
10166 <body>ˇ
10167 <script>
10168 var x = 1;ˇ
10169 </script>
10170 </body>ˇ
10171 "#
10172 .unindent(),
10173 );
10174
10175 cx.update_editor(|editor, window, cx| {
10176 editor.handle_input("<", window, cx);
10177 });
10178 cx.assert_editor_state(
10179 &r#"
10180 <body><ˇ>
10181 <script>
10182 var x = 1;<ˇ
10183 </script>
10184 </body><ˇ>
10185 "#
10186 .unindent(),
10187 );
10188
10189 // When backspacing, the closing angle brackets are removed.
10190 cx.update_editor(|editor, window, cx| {
10191 editor.backspace(&Backspace, window, cx);
10192 });
10193 cx.assert_editor_state(
10194 &r#"
10195 <body>ˇ
10196 <script>
10197 var x = 1;ˇ
10198 </script>
10199 </body>ˇ
10200 "#
10201 .unindent(),
10202 );
10203
10204 // Block comments autoclose in JavaScript, but not HTML.
10205 cx.update_editor(|editor, window, cx| {
10206 editor.handle_input("/", window, cx);
10207 editor.handle_input("*", window, cx);
10208 });
10209 cx.assert_editor_state(
10210 &r#"
10211 <body>/*ˇ
10212 <script>
10213 var x = 1;/*ˇ */
10214 </script>
10215 </body>/*ˇ
10216 "#
10217 .unindent(),
10218 );
10219}
10220
10221#[gpui::test]
10222async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10223 init_test(cx, |_| {});
10224
10225 let mut cx = EditorTestContext::new(cx).await;
10226
10227 let rust_language = Arc::new(
10228 Language::new(
10229 LanguageConfig {
10230 name: "Rust".into(),
10231 brackets: serde_json::from_value(json!([
10232 { "start": "{", "end": "}", "close": true, "newline": true },
10233 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10234 ]))
10235 .unwrap(),
10236 autoclose_before: "})]>".into(),
10237 ..Default::default()
10238 },
10239 Some(tree_sitter_rust::LANGUAGE.into()),
10240 )
10241 .with_override_query("(string_literal) @string")
10242 .unwrap(),
10243 );
10244
10245 cx.language_registry().add(rust_language.clone());
10246 cx.update_buffer(|buffer, cx| {
10247 buffer.set_language(Some(rust_language), cx);
10248 });
10249
10250 cx.set_state(
10251 &r#"
10252 let x = ˇ
10253 "#
10254 .unindent(),
10255 );
10256
10257 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10258 cx.update_editor(|editor, window, cx| {
10259 editor.handle_input("\"", window, cx);
10260 });
10261 cx.assert_editor_state(
10262 &r#"
10263 let x = "ˇ"
10264 "#
10265 .unindent(),
10266 );
10267
10268 // Inserting another quotation mark. The cursor moves across the existing
10269 // automatically-inserted quotation mark.
10270 cx.update_editor(|editor, window, cx| {
10271 editor.handle_input("\"", window, cx);
10272 });
10273 cx.assert_editor_state(
10274 &r#"
10275 let x = ""ˇ
10276 "#
10277 .unindent(),
10278 );
10279
10280 // Reset
10281 cx.set_state(
10282 &r#"
10283 let x = ˇ
10284 "#
10285 .unindent(),
10286 );
10287
10288 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10289 cx.update_editor(|editor, window, cx| {
10290 editor.handle_input("\"", window, cx);
10291 editor.handle_input(" ", window, cx);
10292 editor.move_left(&Default::default(), window, cx);
10293 editor.handle_input("\\", window, cx);
10294 editor.handle_input("\"", window, cx);
10295 });
10296 cx.assert_editor_state(
10297 &r#"
10298 let x = "\"ˇ "
10299 "#
10300 .unindent(),
10301 );
10302
10303 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10304 // mark. Nothing is inserted.
10305 cx.update_editor(|editor, window, cx| {
10306 editor.move_right(&Default::default(), window, cx);
10307 editor.handle_input("\"", window, cx);
10308 });
10309 cx.assert_editor_state(
10310 &r#"
10311 let x = "\" "ˇ
10312 "#
10313 .unindent(),
10314 );
10315}
10316
10317#[gpui::test]
10318async fn test_surround_with_pair(cx: &mut TestAppContext) {
10319 init_test(cx, |_| {});
10320
10321 let language = Arc::new(Language::new(
10322 LanguageConfig {
10323 brackets: BracketPairConfig {
10324 pairs: vec![
10325 BracketPair {
10326 start: "{".to_string(),
10327 end: "}".to_string(),
10328 close: true,
10329 surround: true,
10330 newline: true,
10331 },
10332 BracketPair {
10333 start: "/* ".to_string(),
10334 end: "*/".to_string(),
10335 close: true,
10336 surround: true,
10337 ..Default::default()
10338 },
10339 ],
10340 ..Default::default()
10341 },
10342 ..Default::default()
10343 },
10344 Some(tree_sitter_rust::LANGUAGE.into()),
10345 ));
10346
10347 let text = r#"
10348 a
10349 b
10350 c
10351 "#
10352 .unindent();
10353
10354 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10355 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10356 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10357 editor
10358 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10359 .await;
10360
10361 editor.update_in(cx, |editor, window, cx| {
10362 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10363 s.select_display_ranges([
10364 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10365 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10366 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10367 ])
10368 });
10369
10370 editor.handle_input("{", window, cx);
10371 editor.handle_input("{", window, cx);
10372 editor.handle_input("{", window, cx);
10373 assert_eq!(
10374 editor.text(cx),
10375 "
10376 {{{a}}}
10377 {{{b}}}
10378 {{{c}}}
10379 "
10380 .unindent()
10381 );
10382 assert_eq!(
10383 editor.selections.display_ranges(cx),
10384 [
10385 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10386 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10387 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10388 ]
10389 );
10390
10391 editor.undo(&Undo, window, cx);
10392 editor.undo(&Undo, window, cx);
10393 editor.undo(&Undo, window, cx);
10394 assert_eq!(
10395 editor.text(cx),
10396 "
10397 a
10398 b
10399 c
10400 "
10401 .unindent()
10402 );
10403 assert_eq!(
10404 editor.selections.display_ranges(cx),
10405 [
10406 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10407 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10408 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10409 ]
10410 );
10411
10412 // Ensure inserting the first character of a multi-byte bracket pair
10413 // doesn't surround the selections with the bracket.
10414 editor.handle_input("/", window, cx);
10415 assert_eq!(
10416 editor.text(cx),
10417 "
10418 /
10419 /
10420 /
10421 "
10422 .unindent()
10423 );
10424 assert_eq!(
10425 editor.selections.display_ranges(cx),
10426 [
10427 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10428 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10429 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10430 ]
10431 );
10432
10433 editor.undo(&Undo, window, cx);
10434 assert_eq!(
10435 editor.text(cx),
10436 "
10437 a
10438 b
10439 c
10440 "
10441 .unindent()
10442 );
10443 assert_eq!(
10444 editor.selections.display_ranges(cx),
10445 [
10446 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10447 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10448 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10449 ]
10450 );
10451
10452 // Ensure inserting the last character of a multi-byte bracket pair
10453 // doesn't surround the selections with the bracket.
10454 editor.handle_input("*", window, cx);
10455 assert_eq!(
10456 editor.text(cx),
10457 "
10458 *
10459 *
10460 *
10461 "
10462 .unindent()
10463 );
10464 assert_eq!(
10465 editor.selections.display_ranges(cx),
10466 [
10467 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10468 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10469 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10470 ]
10471 );
10472 });
10473}
10474
10475#[gpui::test]
10476async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10477 init_test(cx, |_| {});
10478
10479 let language = Arc::new(Language::new(
10480 LanguageConfig {
10481 brackets: BracketPairConfig {
10482 pairs: vec![BracketPair {
10483 start: "{".to_string(),
10484 end: "}".to_string(),
10485 close: true,
10486 surround: true,
10487 newline: true,
10488 }],
10489 ..Default::default()
10490 },
10491 autoclose_before: "}".to_string(),
10492 ..Default::default()
10493 },
10494 Some(tree_sitter_rust::LANGUAGE.into()),
10495 ));
10496
10497 let text = r#"
10498 a
10499 b
10500 c
10501 "#
10502 .unindent();
10503
10504 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10505 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10506 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10507 editor
10508 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10509 .await;
10510
10511 editor.update_in(cx, |editor, window, cx| {
10512 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10513 s.select_ranges([
10514 Point::new(0, 1)..Point::new(0, 1),
10515 Point::new(1, 1)..Point::new(1, 1),
10516 Point::new(2, 1)..Point::new(2, 1),
10517 ])
10518 });
10519
10520 editor.handle_input("{", window, cx);
10521 editor.handle_input("{", window, cx);
10522 editor.handle_input("_", window, cx);
10523 assert_eq!(
10524 editor.text(cx),
10525 "
10526 a{{_}}
10527 b{{_}}
10528 c{{_}}
10529 "
10530 .unindent()
10531 );
10532 assert_eq!(
10533 editor.selections.ranges::<Point>(cx),
10534 [
10535 Point::new(0, 4)..Point::new(0, 4),
10536 Point::new(1, 4)..Point::new(1, 4),
10537 Point::new(2, 4)..Point::new(2, 4)
10538 ]
10539 );
10540
10541 editor.backspace(&Default::default(), window, cx);
10542 editor.backspace(&Default::default(), window, cx);
10543 assert_eq!(
10544 editor.text(cx),
10545 "
10546 a{}
10547 b{}
10548 c{}
10549 "
10550 .unindent()
10551 );
10552 assert_eq!(
10553 editor.selections.ranges::<Point>(cx),
10554 [
10555 Point::new(0, 2)..Point::new(0, 2),
10556 Point::new(1, 2)..Point::new(1, 2),
10557 Point::new(2, 2)..Point::new(2, 2)
10558 ]
10559 );
10560
10561 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10562 assert_eq!(
10563 editor.text(cx),
10564 "
10565 a
10566 b
10567 c
10568 "
10569 .unindent()
10570 );
10571 assert_eq!(
10572 editor.selections.ranges::<Point>(cx),
10573 [
10574 Point::new(0, 1)..Point::new(0, 1),
10575 Point::new(1, 1)..Point::new(1, 1),
10576 Point::new(2, 1)..Point::new(2, 1)
10577 ]
10578 );
10579 });
10580}
10581
10582#[gpui::test]
10583async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10584 init_test(cx, |settings| {
10585 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10586 });
10587
10588 let mut cx = EditorTestContext::new(cx).await;
10589
10590 let language = Arc::new(Language::new(
10591 LanguageConfig {
10592 brackets: BracketPairConfig {
10593 pairs: vec![
10594 BracketPair {
10595 start: "{".to_string(),
10596 end: "}".to_string(),
10597 close: true,
10598 surround: true,
10599 newline: true,
10600 },
10601 BracketPair {
10602 start: "(".to_string(),
10603 end: ")".to_string(),
10604 close: true,
10605 surround: true,
10606 newline: true,
10607 },
10608 BracketPair {
10609 start: "[".to_string(),
10610 end: "]".to_string(),
10611 close: false,
10612 surround: true,
10613 newline: true,
10614 },
10615 ],
10616 ..Default::default()
10617 },
10618 autoclose_before: "})]".to_string(),
10619 ..Default::default()
10620 },
10621 Some(tree_sitter_rust::LANGUAGE.into()),
10622 ));
10623
10624 cx.language_registry().add(language.clone());
10625 cx.update_buffer(|buffer, cx| {
10626 buffer.set_language(Some(language), cx);
10627 });
10628
10629 cx.set_state(
10630 &"
10631 {(ˇ)}
10632 [[ˇ]]
10633 {(ˇ)}
10634 "
10635 .unindent(),
10636 );
10637
10638 cx.update_editor(|editor, window, cx| {
10639 editor.backspace(&Default::default(), window, cx);
10640 editor.backspace(&Default::default(), window, cx);
10641 });
10642
10643 cx.assert_editor_state(
10644 &"
10645 ˇ
10646 ˇ]]
10647 ˇ
10648 "
10649 .unindent(),
10650 );
10651
10652 cx.update_editor(|editor, window, cx| {
10653 editor.handle_input("{", window, cx);
10654 editor.handle_input("{", window, cx);
10655 editor.move_right(&MoveRight, window, cx);
10656 editor.move_right(&MoveRight, window, cx);
10657 editor.move_left(&MoveLeft, window, cx);
10658 editor.move_left(&MoveLeft, window, cx);
10659 editor.backspace(&Default::default(), window, cx);
10660 });
10661
10662 cx.assert_editor_state(
10663 &"
10664 {ˇ}
10665 {ˇ}]]
10666 {ˇ}
10667 "
10668 .unindent(),
10669 );
10670
10671 cx.update_editor(|editor, window, cx| {
10672 editor.backspace(&Default::default(), window, cx);
10673 });
10674
10675 cx.assert_editor_state(
10676 &"
10677 ˇ
10678 ˇ]]
10679 ˇ
10680 "
10681 .unindent(),
10682 );
10683}
10684
10685#[gpui::test]
10686async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10687 init_test(cx, |_| {});
10688
10689 let language = Arc::new(Language::new(
10690 LanguageConfig::default(),
10691 Some(tree_sitter_rust::LANGUAGE.into()),
10692 ));
10693
10694 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10695 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10696 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10697 editor
10698 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10699 .await;
10700
10701 editor.update_in(cx, |editor, window, cx| {
10702 editor.set_auto_replace_emoji_shortcode(true);
10703
10704 editor.handle_input("Hello ", window, cx);
10705 editor.handle_input(":wave", window, cx);
10706 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10707
10708 editor.handle_input(":", window, cx);
10709 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10710
10711 editor.handle_input(" :smile", window, cx);
10712 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10713
10714 editor.handle_input(":", window, cx);
10715 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
10716
10717 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
10718 editor.handle_input(":wave", window, cx);
10719 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
10720
10721 editor.handle_input(":", window, cx);
10722 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
10723
10724 editor.handle_input(":1", window, cx);
10725 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
10726
10727 editor.handle_input(":", window, cx);
10728 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
10729
10730 // Ensure shortcode does not get replaced when it is part of a word
10731 editor.handle_input(" Test:wave", window, cx);
10732 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
10733
10734 editor.handle_input(":", window, cx);
10735 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
10736
10737 editor.set_auto_replace_emoji_shortcode(false);
10738
10739 // Ensure shortcode does not get replaced when auto replace is off
10740 editor.handle_input(" :wave", window, cx);
10741 assert_eq!(
10742 editor.text(cx),
10743 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
10744 );
10745
10746 editor.handle_input(":", window, cx);
10747 assert_eq!(
10748 editor.text(cx),
10749 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
10750 );
10751 });
10752}
10753
10754#[gpui::test]
10755async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
10756 init_test(cx, |_| {});
10757
10758 let (text, insertion_ranges) = marked_text_ranges(
10759 indoc! {"
10760 ˇ
10761 "},
10762 false,
10763 );
10764
10765 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
10766 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10767
10768 _ = editor.update_in(cx, |editor, window, cx| {
10769 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
10770
10771 editor
10772 .insert_snippet(&insertion_ranges, snippet, window, cx)
10773 .unwrap();
10774
10775 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
10776 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
10777 assert_eq!(editor.text(cx), expected_text);
10778 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
10779 }
10780
10781 assert(
10782 editor,
10783 cx,
10784 indoc! {"
10785 type «» =•
10786 "},
10787 );
10788
10789 assert!(editor.context_menu_visible(), "There should be a matches");
10790 });
10791}
10792
10793#[gpui::test]
10794async fn test_snippets(cx: &mut TestAppContext) {
10795 init_test(cx, |_| {});
10796
10797 let mut cx = EditorTestContext::new(cx).await;
10798
10799 cx.set_state(indoc! {"
10800 a.ˇ b
10801 a.ˇ b
10802 a.ˇ b
10803 "});
10804
10805 cx.update_editor(|editor, window, cx| {
10806 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
10807 let insertion_ranges = editor
10808 .selections
10809 .all(cx)
10810 .iter()
10811 .map(|s| s.range())
10812 .collect::<Vec<_>>();
10813 editor
10814 .insert_snippet(&insertion_ranges, snippet, window, cx)
10815 .unwrap();
10816 });
10817
10818 cx.assert_editor_state(indoc! {"
10819 a.f(«oneˇ», two, «threeˇ») b
10820 a.f(«oneˇ», two, «threeˇ») b
10821 a.f(«oneˇ», two, «threeˇ») b
10822 "});
10823
10824 // Can't move earlier than the first tab stop
10825 cx.update_editor(|editor, window, cx| {
10826 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10827 });
10828 cx.assert_editor_state(indoc! {"
10829 a.f(«oneˇ», two, «threeˇ») b
10830 a.f(«oneˇ», two, «threeˇ») b
10831 a.f(«oneˇ», two, «threeˇ») b
10832 "});
10833
10834 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10835 cx.assert_editor_state(indoc! {"
10836 a.f(one, «twoˇ», three) b
10837 a.f(one, «twoˇ», three) b
10838 a.f(one, «twoˇ», three) b
10839 "});
10840
10841 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
10842 cx.assert_editor_state(indoc! {"
10843 a.f(«oneˇ», two, «threeˇ») b
10844 a.f(«oneˇ», two, «threeˇ») b
10845 a.f(«oneˇ», two, «threeˇ») b
10846 "});
10847
10848 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10849 cx.assert_editor_state(indoc! {"
10850 a.f(one, «twoˇ», three) b
10851 a.f(one, «twoˇ», three) b
10852 a.f(one, «twoˇ», three) b
10853 "});
10854 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10855 cx.assert_editor_state(indoc! {"
10856 a.f(one, two, three)ˇ b
10857 a.f(one, two, three)ˇ b
10858 a.f(one, two, three)ˇ b
10859 "});
10860
10861 // As soon as the last tab stop is reached, snippet state is gone
10862 cx.update_editor(|editor, window, cx| {
10863 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
10864 });
10865 cx.assert_editor_state(indoc! {"
10866 a.f(one, two, three)ˇ b
10867 a.f(one, two, three)ˇ b
10868 a.f(one, two, three)ˇ b
10869 "});
10870}
10871
10872#[gpui::test]
10873async fn test_snippet_indentation(cx: &mut TestAppContext) {
10874 init_test(cx, |_| {});
10875
10876 let mut cx = EditorTestContext::new(cx).await;
10877
10878 cx.update_editor(|editor, window, cx| {
10879 let snippet = Snippet::parse(indoc! {"
10880 /*
10881 * Multiline comment with leading indentation
10882 *
10883 * $1
10884 */
10885 $0"})
10886 .unwrap();
10887 let insertion_ranges = editor
10888 .selections
10889 .all(cx)
10890 .iter()
10891 .map(|s| s.range())
10892 .collect::<Vec<_>>();
10893 editor
10894 .insert_snippet(&insertion_ranges, snippet, window, cx)
10895 .unwrap();
10896 });
10897
10898 cx.assert_editor_state(indoc! {"
10899 /*
10900 * Multiline comment with leading indentation
10901 *
10902 * ˇ
10903 */
10904 "});
10905
10906 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
10907 cx.assert_editor_state(indoc! {"
10908 /*
10909 * Multiline comment with leading indentation
10910 *
10911 *•
10912 */
10913 ˇ"});
10914}
10915
10916#[gpui::test]
10917async fn test_document_format_during_save(cx: &mut TestAppContext) {
10918 init_test(cx, |_| {});
10919
10920 let fs = FakeFs::new(cx.executor());
10921 fs.insert_file(path!("/file.rs"), Default::default()).await;
10922
10923 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
10924
10925 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10926 language_registry.add(rust_lang());
10927 let mut fake_servers = language_registry.register_fake_lsp(
10928 "Rust",
10929 FakeLspAdapter {
10930 capabilities: lsp::ServerCapabilities {
10931 document_formatting_provider: Some(lsp::OneOf::Left(true)),
10932 ..Default::default()
10933 },
10934 ..Default::default()
10935 },
10936 );
10937
10938 let buffer = project
10939 .update(cx, |project, cx| {
10940 project.open_local_buffer(path!("/file.rs"), cx)
10941 })
10942 .await
10943 .unwrap();
10944
10945 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10946 let (editor, cx) = cx.add_window_view(|window, cx| {
10947 build_editor_with_project(project.clone(), buffer, window, cx)
10948 });
10949 editor.update_in(cx, |editor, window, cx| {
10950 editor.set_text("one\ntwo\nthree\n", window, cx)
10951 });
10952 assert!(cx.read(|cx| editor.is_dirty(cx)));
10953
10954 cx.executor().start_waiting();
10955 let fake_server = fake_servers.next().await.unwrap();
10956
10957 {
10958 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
10959 move |params, _| async move {
10960 assert_eq!(
10961 params.text_document.uri,
10962 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
10963 );
10964 assert_eq!(params.options.tab_size, 4);
10965 Ok(Some(vec![lsp::TextEdit::new(
10966 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
10967 ", ".to_string(),
10968 )]))
10969 },
10970 );
10971 let save = editor
10972 .update_in(cx, |editor, window, cx| {
10973 editor.save(
10974 SaveOptions {
10975 format: true,
10976 autosave: false,
10977 },
10978 project.clone(),
10979 window,
10980 cx,
10981 )
10982 })
10983 .unwrap();
10984 cx.executor().start_waiting();
10985 save.await;
10986
10987 assert_eq!(
10988 editor.update(cx, |editor, cx| editor.text(cx)),
10989 "one, two\nthree\n"
10990 );
10991 assert!(!cx.read(|cx| editor.is_dirty(cx)));
10992 }
10993
10994 {
10995 editor.update_in(cx, |editor, window, cx| {
10996 editor.set_text("one\ntwo\nthree\n", window, cx)
10997 });
10998 assert!(cx.read(|cx| editor.is_dirty(cx)));
10999
11000 // Ensure we can still save even if formatting hangs.
11001 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11002 move |params, _| async move {
11003 assert_eq!(
11004 params.text_document.uri,
11005 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11006 );
11007 futures::future::pending::<()>().await;
11008 unreachable!()
11009 },
11010 );
11011 let save = editor
11012 .update_in(cx, |editor, window, cx| {
11013 editor.save(
11014 SaveOptions {
11015 format: true,
11016 autosave: false,
11017 },
11018 project.clone(),
11019 window,
11020 cx,
11021 )
11022 })
11023 .unwrap();
11024 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11025 cx.executor().start_waiting();
11026 save.await;
11027 assert_eq!(
11028 editor.update(cx, |editor, cx| editor.text(cx)),
11029 "one\ntwo\nthree\n"
11030 );
11031 }
11032
11033 // Set rust language override and assert overridden tabsize is sent to language server
11034 update_test_language_settings(cx, |settings| {
11035 settings.languages.0.insert(
11036 "Rust".into(),
11037 LanguageSettingsContent {
11038 tab_size: NonZeroU32::new(8),
11039 ..Default::default()
11040 },
11041 );
11042 });
11043
11044 {
11045 editor.update_in(cx, |editor, window, cx| {
11046 editor.set_text("somehting_new\n", window, cx)
11047 });
11048 assert!(cx.read(|cx| editor.is_dirty(cx)));
11049 let _formatting_request_signal = fake_server
11050 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11051 assert_eq!(
11052 params.text_document.uri,
11053 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11054 );
11055 assert_eq!(params.options.tab_size, 8);
11056 Ok(Some(vec![]))
11057 });
11058 let save = editor
11059 .update_in(cx, |editor, window, cx| {
11060 editor.save(
11061 SaveOptions {
11062 format: true,
11063 autosave: false,
11064 },
11065 project.clone(),
11066 window,
11067 cx,
11068 )
11069 })
11070 .unwrap();
11071 cx.executor().start_waiting();
11072 save.await;
11073 }
11074}
11075
11076#[gpui::test]
11077async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11078 init_test(cx, |settings| {
11079 settings.defaults.ensure_final_newline_on_save = Some(false);
11080 });
11081
11082 let fs = FakeFs::new(cx.executor());
11083 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11084
11085 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11086
11087 let buffer = project
11088 .update(cx, |project, cx| {
11089 project.open_local_buffer(path!("/file.txt"), cx)
11090 })
11091 .await
11092 .unwrap();
11093
11094 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11095 let (editor, cx) = cx.add_window_view(|window, cx| {
11096 build_editor_with_project(project.clone(), buffer, window, cx)
11097 });
11098 editor.update_in(cx, |editor, window, cx| {
11099 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11100 s.select_ranges([0..0])
11101 });
11102 });
11103 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11104
11105 editor.update_in(cx, |editor, window, cx| {
11106 editor.handle_input("\n", window, cx)
11107 });
11108 cx.run_until_parked();
11109 save(&editor, &project, cx).await;
11110 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11111
11112 editor.update_in(cx, |editor, window, cx| {
11113 editor.undo(&Default::default(), window, cx);
11114 });
11115 save(&editor, &project, cx).await;
11116 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11117
11118 editor.update_in(cx, |editor, window, cx| {
11119 editor.redo(&Default::default(), window, cx);
11120 });
11121 cx.run_until_parked();
11122 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11123
11124 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11125 let save = editor
11126 .update_in(cx, |editor, window, cx| {
11127 editor.save(
11128 SaveOptions {
11129 format: true,
11130 autosave: false,
11131 },
11132 project.clone(),
11133 window,
11134 cx,
11135 )
11136 })
11137 .unwrap();
11138 cx.executor().start_waiting();
11139 save.await;
11140 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11141 }
11142}
11143
11144#[gpui::test]
11145async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11146 init_test(cx, |_| {});
11147
11148 let cols = 4;
11149 let rows = 10;
11150 let sample_text_1 = sample_text(rows, cols, 'a');
11151 assert_eq!(
11152 sample_text_1,
11153 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11154 );
11155 let sample_text_2 = sample_text(rows, cols, 'l');
11156 assert_eq!(
11157 sample_text_2,
11158 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11159 );
11160 let sample_text_3 = sample_text(rows, cols, 'v');
11161 assert_eq!(
11162 sample_text_3,
11163 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11164 );
11165
11166 let fs = FakeFs::new(cx.executor());
11167 fs.insert_tree(
11168 path!("/a"),
11169 json!({
11170 "main.rs": sample_text_1,
11171 "other.rs": sample_text_2,
11172 "lib.rs": sample_text_3,
11173 }),
11174 )
11175 .await;
11176
11177 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11178 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11179 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11180
11181 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11182 language_registry.add(rust_lang());
11183 let mut fake_servers = language_registry.register_fake_lsp(
11184 "Rust",
11185 FakeLspAdapter {
11186 capabilities: lsp::ServerCapabilities {
11187 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11188 ..Default::default()
11189 },
11190 ..Default::default()
11191 },
11192 );
11193
11194 let worktree = project.update(cx, |project, cx| {
11195 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11196 assert_eq!(worktrees.len(), 1);
11197 worktrees.pop().unwrap()
11198 });
11199 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11200
11201 let buffer_1 = project
11202 .update(cx, |project, cx| {
11203 project.open_buffer((worktree_id, "main.rs"), cx)
11204 })
11205 .await
11206 .unwrap();
11207 let buffer_2 = project
11208 .update(cx, |project, cx| {
11209 project.open_buffer((worktree_id, "other.rs"), cx)
11210 })
11211 .await
11212 .unwrap();
11213 let buffer_3 = project
11214 .update(cx, |project, cx| {
11215 project.open_buffer((worktree_id, "lib.rs"), cx)
11216 })
11217 .await
11218 .unwrap();
11219
11220 let multi_buffer = cx.new(|cx| {
11221 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11222 multi_buffer.push_excerpts(
11223 buffer_1.clone(),
11224 [
11225 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11226 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11227 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11228 ],
11229 cx,
11230 );
11231 multi_buffer.push_excerpts(
11232 buffer_2.clone(),
11233 [
11234 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11235 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11236 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11237 ],
11238 cx,
11239 );
11240 multi_buffer.push_excerpts(
11241 buffer_3.clone(),
11242 [
11243 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11244 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11245 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11246 ],
11247 cx,
11248 );
11249 multi_buffer
11250 });
11251 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11252 Editor::new(
11253 EditorMode::full(),
11254 multi_buffer,
11255 Some(project.clone()),
11256 window,
11257 cx,
11258 )
11259 });
11260
11261 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11262 editor.change_selections(
11263 SelectionEffects::scroll(Autoscroll::Next),
11264 window,
11265 cx,
11266 |s| s.select_ranges(Some(1..2)),
11267 );
11268 editor.insert("|one|two|three|", window, cx);
11269 });
11270 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11271 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11272 editor.change_selections(
11273 SelectionEffects::scroll(Autoscroll::Next),
11274 window,
11275 cx,
11276 |s| s.select_ranges(Some(60..70)),
11277 );
11278 editor.insert("|four|five|six|", window, cx);
11279 });
11280 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11281
11282 // First two buffers should be edited, but not the third one.
11283 assert_eq!(
11284 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11285 "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}",
11286 );
11287 buffer_1.update(cx, |buffer, _| {
11288 assert!(buffer.is_dirty());
11289 assert_eq!(
11290 buffer.text(),
11291 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11292 )
11293 });
11294 buffer_2.update(cx, |buffer, _| {
11295 assert!(buffer.is_dirty());
11296 assert_eq!(
11297 buffer.text(),
11298 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11299 )
11300 });
11301 buffer_3.update(cx, |buffer, _| {
11302 assert!(!buffer.is_dirty());
11303 assert_eq!(buffer.text(), sample_text_3,)
11304 });
11305 cx.executor().run_until_parked();
11306
11307 cx.executor().start_waiting();
11308 let save = multi_buffer_editor
11309 .update_in(cx, |editor, window, cx| {
11310 editor.save(
11311 SaveOptions {
11312 format: true,
11313 autosave: false,
11314 },
11315 project.clone(),
11316 window,
11317 cx,
11318 )
11319 })
11320 .unwrap();
11321
11322 let fake_server = fake_servers.next().await.unwrap();
11323 fake_server
11324 .server
11325 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11326 Ok(Some(vec![lsp::TextEdit::new(
11327 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11328 format!("[{} formatted]", params.text_document.uri),
11329 )]))
11330 })
11331 .detach();
11332 save.await;
11333
11334 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11335 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11336 assert_eq!(
11337 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11338 uri!(
11339 "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}"
11340 ),
11341 );
11342 buffer_1.update(cx, |buffer, _| {
11343 assert!(!buffer.is_dirty());
11344 assert_eq!(
11345 buffer.text(),
11346 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11347 )
11348 });
11349 buffer_2.update(cx, |buffer, _| {
11350 assert!(!buffer.is_dirty());
11351 assert_eq!(
11352 buffer.text(),
11353 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11354 )
11355 });
11356 buffer_3.update(cx, |buffer, _| {
11357 assert!(!buffer.is_dirty());
11358 assert_eq!(buffer.text(), sample_text_3,)
11359 });
11360}
11361
11362#[gpui::test]
11363async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11364 init_test(cx, |_| {});
11365
11366 let fs = FakeFs::new(cx.executor());
11367 fs.insert_tree(
11368 path!("/dir"),
11369 json!({
11370 "file1.rs": "fn main() { println!(\"hello\"); }",
11371 "file2.rs": "fn test() { println!(\"test\"); }",
11372 "file3.rs": "fn other() { println!(\"other\"); }\n",
11373 }),
11374 )
11375 .await;
11376
11377 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11378 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11379 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11380
11381 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11382 language_registry.add(rust_lang());
11383
11384 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11385 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11386
11387 // Open three buffers
11388 let buffer_1 = project
11389 .update(cx, |project, cx| {
11390 project.open_buffer((worktree_id, "file1.rs"), cx)
11391 })
11392 .await
11393 .unwrap();
11394 let buffer_2 = project
11395 .update(cx, |project, cx| {
11396 project.open_buffer((worktree_id, "file2.rs"), cx)
11397 })
11398 .await
11399 .unwrap();
11400 let buffer_3 = project
11401 .update(cx, |project, cx| {
11402 project.open_buffer((worktree_id, "file3.rs"), cx)
11403 })
11404 .await
11405 .unwrap();
11406
11407 // Create a multi-buffer with all three buffers
11408 let multi_buffer = cx.new(|cx| {
11409 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11410 multi_buffer.push_excerpts(
11411 buffer_1.clone(),
11412 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11413 cx,
11414 );
11415 multi_buffer.push_excerpts(
11416 buffer_2.clone(),
11417 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11418 cx,
11419 );
11420 multi_buffer.push_excerpts(
11421 buffer_3.clone(),
11422 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11423 cx,
11424 );
11425 multi_buffer
11426 });
11427
11428 let editor = cx.new_window_entity(|window, cx| {
11429 Editor::new(
11430 EditorMode::full(),
11431 multi_buffer,
11432 Some(project.clone()),
11433 window,
11434 cx,
11435 )
11436 });
11437
11438 // Edit only the first buffer
11439 editor.update_in(cx, |editor, window, cx| {
11440 editor.change_selections(
11441 SelectionEffects::scroll(Autoscroll::Next),
11442 window,
11443 cx,
11444 |s| s.select_ranges(Some(10..10)),
11445 );
11446 editor.insert("// edited", window, cx);
11447 });
11448
11449 // Verify that only buffer 1 is dirty
11450 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11451 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11452 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11453
11454 // Get write counts after file creation (files were created with initial content)
11455 // We expect each file to have been written once during creation
11456 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11457 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11458 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11459
11460 // Perform autosave
11461 let save_task = editor.update_in(cx, |editor, window, cx| {
11462 editor.save(
11463 SaveOptions {
11464 format: true,
11465 autosave: true,
11466 },
11467 project.clone(),
11468 window,
11469 cx,
11470 )
11471 });
11472 save_task.await.unwrap();
11473
11474 // Only the dirty buffer should have been saved
11475 assert_eq!(
11476 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11477 1,
11478 "Buffer 1 was dirty, so it should have been written once during autosave"
11479 );
11480 assert_eq!(
11481 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11482 0,
11483 "Buffer 2 was clean, so it should not have been written during autosave"
11484 );
11485 assert_eq!(
11486 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11487 0,
11488 "Buffer 3 was clean, so it should not have been written during autosave"
11489 );
11490
11491 // Verify buffer states after autosave
11492 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11493 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11494 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11495
11496 // Now perform a manual save (format = true)
11497 let save_task = editor.update_in(cx, |editor, window, cx| {
11498 editor.save(
11499 SaveOptions {
11500 format: true,
11501 autosave: false,
11502 },
11503 project.clone(),
11504 window,
11505 cx,
11506 )
11507 });
11508 save_task.await.unwrap();
11509
11510 // During manual save, clean buffers don't get written to disk
11511 // They just get did_save called for language server notifications
11512 assert_eq!(
11513 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11514 1,
11515 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11516 );
11517 assert_eq!(
11518 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11519 0,
11520 "Buffer 2 should not have been written at all"
11521 );
11522 assert_eq!(
11523 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11524 0,
11525 "Buffer 3 should not have been written at all"
11526 );
11527}
11528
11529async fn setup_range_format_test(
11530 cx: &mut TestAppContext,
11531) -> (
11532 Entity<Project>,
11533 Entity<Editor>,
11534 &mut gpui::VisualTestContext,
11535 lsp::FakeLanguageServer,
11536) {
11537 init_test(cx, |_| {});
11538
11539 let fs = FakeFs::new(cx.executor());
11540 fs.insert_file(path!("/file.rs"), Default::default()).await;
11541
11542 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11543
11544 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11545 language_registry.add(rust_lang());
11546 let mut fake_servers = language_registry.register_fake_lsp(
11547 "Rust",
11548 FakeLspAdapter {
11549 capabilities: lsp::ServerCapabilities {
11550 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11551 ..lsp::ServerCapabilities::default()
11552 },
11553 ..FakeLspAdapter::default()
11554 },
11555 );
11556
11557 let buffer = project
11558 .update(cx, |project, cx| {
11559 project.open_local_buffer(path!("/file.rs"), cx)
11560 })
11561 .await
11562 .unwrap();
11563
11564 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11565 let (editor, cx) = cx.add_window_view(|window, cx| {
11566 build_editor_with_project(project.clone(), buffer, window, cx)
11567 });
11568
11569 cx.executor().start_waiting();
11570 let fake_server = fake_servers.next().await.unwrap();
11571
11572 (project, editor, cx, fake_server)
11573}
11574
11575#[gpui::test]
11576async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11577 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11578
11579 editor.update_in(cx, |editor, window, cx| {
11580 editor.set_text("one\ntwo\nthree\n", window, cx)
11581 });
11582 assert!(cx.read(|cx| editor.is_dirty(cx)));
11583
11584 let save = editor
11585 .update_in(cx, |editor, window, cx| {
11586 editor.save(
11587 SaveOptions {
11588 format: true,
11589 autosave: false,
11590 },
11591 project.clone(),
11592 window,
11593 cx,
11594 )
11595 })
11596 .unwrap();
11597 fake_server
11598 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11599 assert_eq!(
11600 params.text_document.uri,
11601 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11602 );
11603 assert_eq!(params.options.tab_size, 4);
11604 Ok(Some(vec![lsp::TextEdit::new(
11605 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11606 ", ".to_string(),
11607 )]))
11608 })
11609 .next()
11610 .await;
11611 cx.executor().start_waiting();
11612 save.await;
11613 assert_eq!(
11614 editor.update(cx, |editor, cx| editor.text(cx)),
11615 "one, two\nthree\n"
11616 );
11617 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11618}
11619
11620#[gpui::test]
11621async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
11622 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11623
11624 editor.update_in(cx, |editor, window, cx| {
11625 editor.set_text("one\ntwo\nthree\n", window, cx)
11626 });
11627 assert!(cx.read(|cx| editor.is_dirty(cx)));
11628
11629 // Test that save still works when formatting hangs
11630 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
11631 move |params, _| async move {
11632 assert_eq!(
11633 params.text_document.uri,
11634 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11635 );
11636 futures::future::pending::<()>().await;
11637 unreachable!()
11638 },
11639 );
11640 let save = editor
11641 .update_in(cx, |editor, window, cx| {
11642 editor.save(
11643 SaveOptions {
11644 format: true,
11645 autosave: false,
11646 },
11647 project.clone(),
11648 window,
11649 cx,
11650 )
11651 })
11652 .unwrap();
11653 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11654 cx.executor().start_waiting();
11655 save.await;
11656 assert_eq!(
11657 editor.update(cx, |editor, cx| editor.text(cx)),
11658 "one\ntwo\nthree\n"
11659 );
11660 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11661}
11662
11663#[gpui::test]
11664async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
11665 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11666
11667 // Buffer starts clean, no formatting should be requested
11668 let save = editor
11669 .update_in(cx, |editor, window, cx| {
11670 editor.save(
11671 SaveOptions {
11672 format: false,
11673 autosave: false,
11674 },
11675 project.clone(),
11676 window,
11677 cx,
11678 )
11679 })
11680 .unwrap();
11681 let _pending_format_request = fake_server
11682 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
11683 panic!("Should not be invoked");
11684 })
11685 .next();
11686 cx.executor().start_waiting();
11687 save.await;
11688 cx.run_until_parked();
11689}
11690
11691#[gpui::test]
11692async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
11693 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11694
11695 // Set Rust language override and assert overridden tabsize is sent to language server
11696 update_test_language_settings(cx, |settings| {
11697 settings.languages.0.insert(
11698 "Rust".into(),
11699 LanguageSettingsContent {
11700 tab_size: NonZeroU32::new(8),
11701 ..Default::default()
11702 },
11703 );
11704 });
11705
11706 editor.update_in(cx, |editor, window, cx| {
11707 editor.set_text("something_new\n", window, cx)
11708 });
11709 assert!(cx.read(|cx| editor.is_dirty(cx)));
11710 let save = editor
11711 .update_in(cx, |editor, window, cx| {
11712 editor.save(
11713 SaveOptions {
11714 format: true,
11715 autosave: false,
11716 },
11717 project.clone(),
11718 window,
11719 cx,
11720 )
11721 })
11722 .unwrap();
11723 fake_server
11724 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
11725 assert_eq!(
11726 params.text_document.uri,
11727 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11728 );
11729 assert_eq!(params.options.tab_size, 8);
11730 Ok(Some(Vec::new()))
11731 })
11732 .next()
11733 .await;
11734 save.await;
11735}
11736
11737#[gpui::test]
11738async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
11739 init_test(cx, |settings| {
11740 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
11741 Formatter::LanguageServer { name: None },
11742 )))
11743 });
11744
11745 let fs = FakeFs::new(cx.executor());
11746 fs.insert_file(path!("/file.rs"), Default::default()).await;
11747
11748 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11749
11750 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11751 language_registry.add(Arc::new(Language::new(
11752 LanguageConfig {
11753 name: "Rust".into(),
11754 matcher: LanguageMatcher {
11755 path_suffixes: vec!["rs".to_string()],
11756 ..Default::default()
11757 },
11758 ..LanguageConfig::default()
11759 },
11760 Some(tree_sitter_rust::LANGUAGE.into()),
11761 )));
11762 update_test_language_settings(cx, |settings| {
11763 // Enable Prettier formatting for the same buffer, and ensure
11764 // LSP is called instead of Prettier.
11765 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
11766 });
11767 let mut fake_servers = language_registry.register_fake_lsp(
11768 "Rust",
11769 FakeLspAdapter {
11770 capabilities: lsp::ServerCapabilities {
11771 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11772 ..Default::default()
11773 },
11774 ..Default::default()
11775 },
11776 );
11777
11778 let buffer = project
11779 .update(cx, |project, cx| {
11780 project.open_local_buffer(path!("/file.rs"), cx)
11781 })
11782 .await
11783 .unwrap();
11784
11785 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11786 let (editor, cx) = cx.add_window_view(|window, cx| {
11787 build_editor_with_project(project.clone(), buffer, window, cx)
11788 });
11789 editor.update_in(cx, |editor, window, cx| {
11790 editor.set_text("one\ntwo\nthree\n", window, cx)
11791 });
11792
11793 cx.executor().start_waiting();
11794 let fake_server = fake_servers.next().await.unwrap();
11795
11796 let format = editor
11797 .update_in(cx, |editor, window, cx| {
11798 editor.perform_format(
11799 project.clone(),
11800 FormatTrigger::Manual,
11801 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11802 window,
11803 cx,
11804 )
11805 })
11806 .unwrap();
11807 fake_server
11808 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11809 assert_eq!(
11810 params.text_document.uri,
11811 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11812 );
11813 assert_eq!(params.options.tab_size, 4);
11814 Ok(Some(vec![lsp::TextEdit::new(
11815 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11816 ", ".to_string(),
11817 )]))
11818 })
11819 .next()
11820 .await;
11821 cx.executor().start_waiting();
11822 format.await;
11823 assert_eq!(
11824 editor.update(cx, |editor, cx| editor.text(cx)),
11825 "one, two\nthree\n"
11826 );
11827
11828 editor.update_in(cx, |editor, window, cx| {
11829 editor.set_text("one\ntwo\nthree\n", window, cx)
11830 });
11831 // Ensure we don't lock if formatting hangs.
11832 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11833 move |params, _| async move {
11834 assert_eq!(
11835 params.text_document.uri,
11836 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11837 );
11838 futures::future::pending::<()>().await;
11839 unreachable!()
11840 },
11841 );
11842 let format = editor
11843 .update_in(cx, |editor, window, cx| {
11844 editor.perform_format(
11845 project,
11846 FormatTrigger::Manual,
11847 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
11848 window,
11849 cx,
11850 )
11851 })
11852 .unwrap();
11853 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11854 cx.executor().start_waiting();
11855 format.await;
11856 assert_eq!(
11857 editor.update(cx, |editor, cx| editor.text(cx)),
11858 "one\ntwo\nthree\n"
11859 );
11860}
11861
11862#[gpui::test]
11863async fn test_multiple_formatters(cx: &mut TestAppContext) {
11864 init_test(cx, |settings| {
11865 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
11866 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
11867 Formatter::LanguageServer { name: None },
11868 Formatter::CodeActions(
11869 [
11870 ("code-action-1".into(), true),
11871 ("code-action-2".into(), true),
11872 ]
11873 .into_iter()
11874 .collect(),
11875 ),
11876 ])))
11877 });
11878
11879 let fs = FakeFs::new(cx.executor());
11880 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
11881 .await;
11882
11883 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11884 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11885 language_registry.add(rust_lang());
11886
11887 let mut fake_servers = language_registry.register_fake_lsp(
11888 "Rust",
11889 FakeLspAdapter {
11890 capabilities: lsp::ServerCapabilities {
11891 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11892 execute_command_provider: Some(lsp::ExecuteCommandOptions {
11893 commands: vec!["the-command-for-code-action-1".into()],
11894 ..Default::default()
11895 }),
11896 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
11897 ..Default::default()
11898 },
11899 ..Default::default()
11900 },
11901 );
11902
11903 let buffer = project
11904 .update(cx, |project, cx| {
11905 project.open_local_buffer(path!("/file.rs"), cx)
11906 })
11907 .await
11908 .unwrap();
11909
11910 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11911 let (editor, cx) = cx.add_window_view(|window, cx| {
11912 build_editor_with_project(project.clone(), buffer, window, cx)
11913 });
11914
11915 cx.executor().start_waiting();
11916
11917 let fake_server = fake_servers.next().await.unwrap();
11918 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11919 move |_params, _| async move {
11920 Ok(Some(vec![lsp::TextEdit::new(
11921 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11922 "applied-formatting\n".to_string(),
11923 )]))
11924 },
11925 );
11926 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
11927 move |params, _| async move {
11928 let requested_code_actions = params.context.only.expect("Expected code action request");
11929 assert_eq!(requested_code_actions.len(), 1);
11930
11931 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11932 let code_action = match requested_code_actions[0].as_str() {
11933 "code-action-1" => lsp::CodeAction {
11934 kind: Some("code-action-1".into()),
11935 edit: Some(lsp::WorkspaceEdit::new(
11936 [(
11937 uri,
11938 vec![lsp::TextEdit::new(
11939 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11940 "applied-code-action-1-edit\n".to_string(),
11941 )],
11942 )]
11943 .into_iter()
11944 .collect(),
11945 )),
11946 command: Some(lsp::Command {
11947 command: "the-command-for-code-action-1".into(),
11948 ..Default::default()
11949 }),
11950 ..Default::default()
11951 },
11952 "code-action-2" => lsp::CodeAction {
11953 kind: Some("code-action-2".into()),
11954 edit: Some(lsp::WorkspaceEdit::new(
11955 [(
11956 uri,
11957 vec![lsp::TextEdit::new(
11958 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11959 "applied-code-action-2-edit\n".to_string(),
11960 )],
11961 )]
11962 .into_iter()
11963 .collect(),
11964 )),
11965 ..Default::default()
11966 },
11967 req => panic!("Unexpected code action request: {:?}", req),
11968 };
11969 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
11970 code_action,
11971 )]))
11972 },
11973 );
11974
11975 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11976 move |params, _| async move { Ok(params) }
11977 });
11978
11979 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11980 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11981 let fake = fake_server.clone();
11982 let lock = command_lock.clone();
11983 move |params, _| {
11984 assert_eq!(params.command, "the-command-for-code-action-1");
11985 let fake = fake.clone();
11986 let lock = lock.clone();
11987 async move {
11988 lock.lock().await;
11989 fake.server
11990 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11991 label: None,
11992 edit: lsp::WorkspaceEdit {
11993 changes: Some(
11994 [(
11995 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11996 vec![lsp::TextEdit {
11997 range: lsp::Range::new(
11998 lsp::Position::new(0, 0),
11999 lsp::Position::new(0, 0),
12000 ),
12001 new_text: "applied-code-action-1-command\n".into(),
12002 }],
12003 )]
12004 .into_iter()
12005 .collect(),
12006 ),
12007 ..Default::default()
12008 },
12009 })
12010 .await
12011 .into_response()
12012 .unwrap();
12013 Ok(Some(json!(null)))
12014 }
12015 }
12016 });
12017
12018 cx.executor().start_waiting();
12019 editor
12020 .update_in(cx, |editor, window, cx| {
12021 editor.perform_format(
12022 project.clone(),
12023 FormatTrigger::Manual,
12024 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12025 window,
12026 cx,
12027 )
12028 })
12029 .unwrap()
12030 .await;
12031 editor.update(cx, |editor, cx| {
12032 assert_eq!(
12033 editor.text(cx),
12034 r#"
12035 applied-code-action-2-edit
12036 applied-code-action-1-command
12037 applied-code-action-1-edit
12038 applied-formatting
12039 one
12040 two
12041 three
12042 "#
12043 .unindent()
12044 );
12045 });
12046
12047 editor.update_in(cx, |editor, window, cx| {
12048 editor.undo(&Default::default(), window, cx);
12049 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12050 });
12051
12052 // Perform a manual edit while waiting for an LSP command
12053 // that's being run as part of a formatting code action.
12054 let lock_guard = command_lock.lock().await;
12055 let format = editor
12056 .update_in(cx, |editor, window, cx| {
12057 editor.perform_format(
12058 project.clone(),
12059 FormatTrigger::Manual,
12060 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12061 window,
12062 cx,
12063 )
12064 })
12065 .unwrap();
12066 cx.run_until_parked();
12067 editor.update(cx, |editor, cx| {
12068 assert_eq!(
12069 editor.text(cx),
12070 r#"
12071 applied-code-action-1-edit
12072 applied-formatting
12073 one
12074 two
12075 three
12076 "#
12077 .unindent()
12078 );
12079
12080 editor.buffer.update(cx, |buffer, cx| {
12081 let ix = buffer.len(cx);
12082 buffer.edit([(ix..ix, "edited\n")], None, cx);
12083 });
12084 });
12085
12086 // Allow the LSP command to proceed. Because the buffer was edited,
12087 // the second code action will not be run.
12088 drop(lock_guard);
12089 format.await;
12090 editor.update_in(cx, |editor, window, cx| {
12091 assert_eq!(
12092 editor.text(cx),
12093 r#"
12094 applied-code-action-1-command
12095 applied-code-action-1-edit
12096 applied-formatting
12097 one
12098 two
12099 three
12100 edited
12101 "#
12102 .unindent()
12103 );
12104
12105 // The manual edit is undone first, because it is the last thing the user did
12106 // (even though the command completed afterwards).
12107 editor.undo(&Default::default(), window, cx);
12108 assert_eq!(
12109 editor.text(cx),
12110 r#"
12111 applied-code-action-1-command
12112 applied-code-action-1-edit
12113 applied-formatting
12114 one
12115 two
12116 three
12117 "#
12118 .unindent()
12119 );
12120
12121 // All the formatting (including the command, which completed after the manual edit)
12122 // is undone together.
12123 editor.undo(&Default::default(), window, cx);
12124 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12125 });
12126}
12127
12128#[gpui::test]
12129async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12130 init_test(cx, |settings| {
12131 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12132 Formatter::LanguageServer { name: None },
12133 ])))
12134 });
12135
12136 let fs = FakeFs::new(cx.executor());
12137 fs.insert_file(path!("/file.ts"), Default::default()).await;
12138
12139 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12140
12141 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12142 language_registry.add(Arc::new(Language::new(
12143 LanguageConfig {
12144 name: "TypeScript".into(),
12145 matcher: LanguageMatcher {
12146 path_suffixes: vec!["ts".to_string()],
12147 ..Default::default()
12148 },
12149 ..LanguageConfig::default()
12150 },
12151 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12152 )));
12153 update_test_language_settings(cx, |settings| {
12154 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12155 });
12156 let mut fake_servers = language_registry.register_fake_lsp(
12157 "TypeScript",
12158 FakeLspAdapter {
12159 capabilities: lsp::ServerCapabilities {
12160 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12161 ..Default::default()
12162 },
12163 ..Default::default()
12164 },
12165 );
12166
12167 let buffer = project
12168 .update(cx, |project, cx| {
12169 project.open_local_buffer(path!("/file.ts"), cx)
12170 })
12171 .await
12172 .unwrap();
12173
12174 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12175 let (editor, cx) = cx.add_window_view(|window, cx| {
12176 build_editor_with_project(project.clone(), buffer, window, cx)
12177 });
12178 editor.update_in(cx, |editor, window, cx| {
12179 editor.set_text(
12180 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12181 window,
12182 cx,
12183 )
12184 });
12185
12186 cx.executor().start_waiting();
12187 let fake_server = fake_servers.next().await.unwrap();
12188
12189 let format = editor
12190 .update_in(cx, |editor, window, cx| {
12191 editor.perform_code_action_kind(
12192 project.clone(),
12193 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12194 window,
12195 cx,
12196 )
12197 })
12198 .unwrap();
12199 fake_server
12200 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12201 assert_eq!(
12202 params.text_document.uri,
12203 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12204 );
12205 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12206 lsp::CodeAction {
12207 title: "Organize Imports".to_string(),
12208 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12209 edit: Some(lsp::WorkspaceEdit {
12210 changes: Some(
12211 [(
12212 params.text_document.uri.clone(),
12213 vec![lsp::TextEdit::new(
12214 lsp::Range::new(
12215 lsp::Position::new(1, 0),
12216 lsp::Position::new(2, 0),
12217 ),
12218 "".to_string(),
12219 )],
12220 )]
12221 .into_iter()
12222 .collect(),
12223 ),
12224 ..Default::default()
12225 }),
12226 ..Default::default()
12227 },
12228 )]))
12229 })
12230 .next()
12231 .await;
12232 cx.executor().start_waiting();
12233 format.await;
12234 assert_eq!(
12235 editor.update(cx, |editor, cx| editor.text(cx)),
12236 "import { a } from 'module';\n\nconst x = a;\n"
12237 );
12238
12239 editor.update_in(cx, |editor, window, cx| {
12240 editor.set_text(
12241 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12242 window,
12243 cx,
12244 )
12245 });
12246 // Ensure we don't lock if code action hangs.
12247 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12248 move |params, _| async move {
12249 assert_eq!(
12250 params.text_document.uri,
12251 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12252 );
12253 futures::future::pending::<()>().await;
12254 unreachable!()
12255 },
12256 );
12257 let format = editor
12258 .update_in(cx, |editor, window, cx| {
12259 editor.perform_code_action_kind(
12260 project,
12261 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12262 window,
12263 cx,
12264 )
12265 })
12266 .unwrap();
12267 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12268 cx.executor().start_waiting();
12269 format.await;
12270 assert_eq!(
12271 editor.update(cx, |editor, cx| editor.text(cx)),
12272 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12273 );
12274}
12275
12276#[gpui::test]
12277async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12278 init_test(cx, |_| {});
12279
12280 let mut cx = EditorLspTestContext::new_rust(
12281 lsp::ServerCapabilities {
12282 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12283 ..Default::default()
12284 },
12285 cx,
12286 )
12287 .await;
12288
12289 cx.set_state(indoc! {"
12290 one.twoˇ
12291 "});
12292
12293 // The format request takes a long time. When it completes, it inserts
12294 // a newline and an indent before the `.`
12295 cx.lsp
12296 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12297 let executor = cx.background_executor().clone();
12298 async move {
12299 executor.timer(Duration::from_millis(100)).await;
12300 Ok(Some(vec![lsp::TextEdit {
12301 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12302 new_text: "\n ".into(),
12303 }]))
12304 }
12305 });
12306
12307 // Submit a format request.
12308 let format_1 = cx
12309 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12310 .unwrap();
12311 cx.executor().run_until_parked();
12312
12313 // Submit a second format request.
12314 let format_2 = cx
12315 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12316 .unwrap();
12317 cx.executor().run_until_parked();
12318
12319 // Wait for both format requests to complete
12320 cx.executor().advance_clock(Duration::from_millis(200));
12321 cx.executor().start_waiting();
12322 format_1.await.unwrap();
12323 cx.executor().start_waiting();
12324 format_2.await.unwrap();
12325
12326 // The formatting edits only happens once.
12327 cx.assert_editor_state(indoc! {"
12328 one
12329 .twoˇ
12330 "});
12331}
12332
12333#[gpui::test]
12334async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12335 init_test(cx, |settings| {
12336 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12337 });
12338
12339 let mut cx = EditorLspTestContext::new_rust(
12340 lsp::ServerCapabilities {
12341 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12342 ..Default::default()
12343 },
12344 cx,
12345 )
12346 .await;
12347
12348 // Set up a buffer white some trailing whitespace and no trailing newline.
12349 cx.set_state(
12350 &[
12351 "one ", //
12352 "twoˇ", //
12353 "three ", //
12354 "four", //
12355 ]
12356 .join("\n"),
12357 );
12358
12359 // Submit a format request.
12360 let format = cx
12361 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12362 .unwrap();
12363
12364 // Record which buffer changes have been sent to the language server
12365 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12366 cx.lsp
12367 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12368 let buffer_changes = buffer_changes.clone();
12369 move |params, _| {
12370 buffer_changes.lock().extend(
12371 params
12372 .content_changes
12373 .into_iter()
12374 .map(|e| (e.range.unwrap(), e.text)),
12375 );
12376 }
12377 });
12378
12379 // Handle formatting requests to the language server.
12380 cx.lsp
12381 .set_request_handler::<lsp::request::Formatting, _, _>({
12382 let buffer_changes = buffer_changes.clone();
12383 move |_, _| {
12384 // When formatting is requested, trailing whitespace has already been stripped,
12385 // and the trailing newline has already been added.
12386 assert_eq!(
12387 &buffer_changes.lock()[1..],
12388 &[
12389 (
12390 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12391 "".into()
12392 ),
12393 (
12394 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12395 "".into()
12396 ),
12397 (
12398 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12399 "\n".into()
12400 ),
12401 ]
12402 );
12403
12404 // Insert blank lines between each line of the buffer.
12405 async move {
12406 Ok(Some(vec![
12407 lsp::TextEdit {
12408 range: lsp::Range::new(
12409 lsp::Position::new(1, 0),
12410 lsp::Position::new(1, 0),
12411 ),
12412 new_text: "\n".into(),
12413 },
12414 lsp::TextEdit {
12415 range: lsp::Range::new(
12416 lsp::Position::new(2, 0),
12417 lsp::Position::new(2, 0),
12418 ),
12419 new_text: "\n".into(),
12420 },
12421 ]))
12422 }
12423 }
12424 });
12425
12426 // After formatting the buffer, the trailing whitespace is stripped,
12427 // a newline is appended, and the edits provided by the language server
12428 // have been applied.
12429 format.await.unwrap();
12430 cx.assert_editor_state(
12431 &[
12432 "one", //
12433 "", //
12434 "twoˇ", //
12435 "", //
12436 "three", //
12437 "four", //
12438 "", //
12439 ]
12440 .join("\n"),
12441 );
12442
12443 // Undoing the formatting undoes the trailing whitespace removal, the
12444 // trailing newline, and the LSP edits.
12445 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12446 cx.assert_editor_state(
12447 &[
12448 "one ", //
12449 "twoˇ", //
12450 "three ", //
12451 "four", //
12452 ]
12453 .join("\n"),
12454 );
12455}
12456
12457#[gpui::test]
12458async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12459 cx: &mut TestAppContext,
12460) {
12461 init_test(cx, |_| {});
12462
12463 cx.update(|cx| {
12464 cx.update_global::<SettingsStore, _>(|settings, cx| {
12465 settings.update_user_settings(cx, |settings| {
12466 settings.editor.auto_signature_help = Some(true);
12467 });
12468 });
12469 });
12470
12471 let mut cx = EditorLspTestContext::new_rust(
12472 lsp::ServerCapabilities {
12473 signature_help_provider: Some(lsp::SignatureHelpOptions {
12474 ..Default::default()
12475 }),
12476 ..Default::default()
12477 },
12478 cx,
12479 )
12480 .await;
12481
12482 let language = Language::new(
12483 LanguageConfig {
12484 name: "Rust".into(),
12485 brackets: BracketPairConfig {
12486 pairs: vec![
12487 BracketPair {
12488 start: "{".to_string(),
12489 end: "}".to_string(),
12490 close: true,
12491 surround: true,
12492 newline: true,
12493 },
12494 BracketPair {
12495 start: "(".to_string(),
12496 end: ")".to_string(),
12497 close: true,
12498 surround: true,
12499 newline: true,
12500 },
12501 BracketPair {
12502 start: "/*".to_string(),
12503 end: " */".to_string(),
12504 close: true,
12505 surround: true,
12506 newline: true,
12507 },
12508 BracketPair {
12509 start: "[".to_string(),
12510 end: "]".to_string(),
12511 close: false,
12512 surround: false,
12513 newline: true,
12514 },
12515 BracketPair {
12516 start: "\"".to_string(),
12517 end: "\"".to_string(),
12518 close: true,
12519 surround: true,
12520 newline: false,
12521 },
12522 BracketPair {
12523 start: "<".to_string(),
12524 end: ">".to_string(),
12525 close: false,
12526 surround: true,
12527 newline: true,
12528 },
12529 ],
12530 ..Default::default()
12531 },
12532 autoclose_before: "})]".to_string(),
12533 ..Default::default()
12534 },
12535 Some(tree_sitter_rust::LANGUAGE.into()),
12536 );
12537 let language = Arc::new(language);
12538
12539 cx.language_registry().add(language.clone());
12540 cx.update_buffer(|buffer, cx| {
12541 buffer.set_language(Some(language), cx);
12542 });
12543
12544 cx.set_state(
12545 &r#"
12546 fn main() {
12547 sampleˇ
12548 }
12549 "#
12550 .unindent(),
12551 );
12552
12553 cx.update_editor(|editor, window, cx| {
12554 editor.handle_input("(", window, cx);
12555 });
12556 cx.assert_editor_state(
12557 &"
12558 fn main() {
12559 sample(ˇ)
12560 }
12561 "
12562 .unindent(),
12563 );
12564
12565 let mocked_response = lsp::SignatureHelp {
12566 signatures: vec![lsp::SignatureInformation {
12567 label: "fn sample(param1: u8, param2: u8)".to_string(),
12568 documentation: None,
12569 parameters: Some(vec![
12570 lsp::ParameterInformation {
12571 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12572 documentation: None,
12573 },
12574 lsp::ParameterInformation {
12575 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12576 documentation: None,
12577 },
12578 ]),
12579 active_parameter: None,
12580 }],
12581 active_signature: Some(0),
12582 active_parameter: Some(0),
12583 };
12584 handle_signature_help_request(&mut cx, mocked_response).await;
12585
12586 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12587 .await;
12588
12589 cx.editor(|editor, _, _| {
12590 let signature_help_state = editor.signature_help_state.popover().cloned();
12591 let signature = signature_help_state.unwrap();
12592 assert_eq!(
12593 signature.signatures[signature.current_signature].label,
12594 "fn sample(param1: u8, param2: u8)"
12595 );
12596 });
12597}
12598
12599#[gpui::test]
12600async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12601 init_test(cx, |_| {});
12602
12603 cx.update(|cx| {
12604 cx.update_global::<SettingsStore, _>(|settings, cx| {
12605 settings.update_user_settings(cx, |settings| {
12606 settings.editor.auto_signature_help = Some(false);
12607 settings.editor.show_signature_help_after_edits = Some(false);
12608 });
12609 });
12610 });
12611
12612 let mut cx = EditorLspTestContext::new_rust(
12613 lsp::ServerCapabilities {
12614 signature_help_provider: Some(lsp::SignatureHelpOptions {
12615 ..Default::default()
12616 }),
12617 ..Default::default()
12618 },
12619 cx,
12620 )
12621 .await;
12622
12623 let language = Language::new(
12624 LanguageConfig {
12625 name: "Rust".into(),
12626 brackets: BracketPairConfig {
12627 pairs: vec![
12628 BracketPair {
12629 start: "{".to_string(),
12630 end: "}".to_string(),
12631 close: true,
12632 surround: true,
12633 newline: true,
12634 },
12635 BracketPair {
12636 start: "(".to_string(),
12637 end: ")".to_string(),
12638 close: true,
12639 surround: true,
12640 newline: true,
12641 },
12642 BracketPair {
12643 start: "/*".to_string(),
12644 end: " */".to_string(),
12645 close: true,
12646 surround: true,
12647 newline: true,
12648 },
12649 BracketPair {
12650 start: "[".to_string(),
12651 end: "]".to_string(),
12652 close: false,
12653 surround: false,
12654 newline: true,
12655 },
12656 BracketPair {
12657 start: "\"".to_string(),
12658 end: "\"".to_string(),
12659 close: true,
12660 surround: true,
12661 newline: false,
12662 },
12663 BracketPair {
12664 start: "<".to_string(),
12665 end: ">".to_string(),
12666 close: false,
12667 surround: true,
12668 newline: true,
12669 },
12670 ],
12671 ..Default::default()
12672 },
12673 autoclose_before: "})]".to_string(),
12674 ..Default::default()
12675 },
12676 Some(tree_sitter_rust::LANGUAGE.into()),
12677 );
12678 let language = Arc::new(language);
12679
12680 cx.language_registry().add(language.clone());
12681 cx.update_buffer(|buffer, cx| {
12682 buffer.set_language(Some(language), cx);
12683 });
12684
12685 // Ensure that signature_help is not called when no signature help is enabled.
12686 cx.set_state(
12687 &r#"
12688 fn main() {
12689 sampleˇ
12690 }
12691 "#
12692 .unindent(),
12693 );
12694 cx.update_editor(|editor, window, cx| {
12695 editor.handle_input("(", window, cx);
12696 });
12697 cx.assert_editor_state(
12698 &"
12699 fn main() {
12700 sample(ˇ)
12701 }
12702 "
12703 .unindent(),
12704 );
12705 cx.editor(|editor, _, _| {
12706 assert!(editor.signature_help_state.task().is_none());
12707 });
12708
12709 let mocked_response = lsp::SignatureHelp {
12710 signatures: vec![lsp::SignatureInformation {
12711 label: "fn sample(param1: u8, param2: u8)".to_string(),
12712 documentation: None,
12713 parameters: Some(vec![
12714 lsp::ParameterInformation {
12715 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12716 documentation: None,
12717 },
12718 lsp::ParameterInformation {
12719 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12720 documentation: None,
12721 },
12722 ]),
12723 active_parameter: None,
12724 }],
12725 active_signature: Some(0),
12726 active_parameter: Some(0),
12727 };
12728
12729 // Ensure that signature_help is called when enabled afte edits
12730 cx.update(|_, cx| {
12731 cx.update_global::<SettingsStore, _>(|settings, cx| {
12732 settings.update_user_settings(cx, |settings| {
12733 settings.editor.auto_signature_help = Some(false);
12734 settings.editor.show_signature_help_after_edits = Some(true);
12735 });
12736 });
12737 });
12738 cx.set_state(
12739 &r#"
12740 fn main() {
12741 sampleˇ
12742 }
12743 "#
12744 .unindent(),
12745 );
12746 cx.update_editor(|editor, window, cx| {
12747 editor.handle_input("(", window, cx);
12748 });
12749 cx.assert_editor_state(
12750 &"
12751 fn main() {
12752 sample(ˇ)
12753 }
12754 "
12755 .unindent(),
12756 );
12757 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12758 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12759 .await;
12760 cx.update_editor(|editor, _, _| {
12761 let signature_help_state = editor.signature_help_state.popover().cloned();
12762 assert!(signature_help_state.is_some());
12763 let signature = signature_help_state.unwrap();
12764 assert_eq!(
12765 signature.signatures[signature.current_signature].label,
12766 "fn sample(param1: u8, param2: u8)"
12767 );
12768 editor.signature_help_state = SignatureHelpState::default();
12769 });
12770
12771 // Ensure that signature_help is called when auto signature help override is enabled
12772 cx.update(|_, cx| {
12773 cx.update_global::<SettingsStore, _>(|settings, cx| {
12774 settings.update_user_settings(cx, |settings| {
12775 settings.editor.auto_signature_help = Some(true);
12776 settings.editor.show_signature_help_after_edits = Some(false);
12777 });
12778 });
12779 });
12780 cx.set_state(
12781 &r#"
12782 fn main() {
12783 sampleˇ
12784 }
12785 "#
12786 .unindent(),
12787 );
12788 cx.update_editor(|editor, window, cx| {
12789 editor.handle_input("(", window, cx);
12790 });
12791 cx.assert_editor_state(
12792 &"
12793 fn main() {
12794 sample(ˇ)
12795 }
12796 "
12797 .unindent(),
12798 );
12799 handle_signature_help_request(&mut cx, mocked_response).await;
12800 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12801 .await;
12802 cx.editor(|editor, _, _| {
12803 let signature_help_state = editor.signature_help_state.popover().cloned();
12804 assert!(signature_help_state.is_some());
12805 let signature = signature_help_state.unwrap();
12806 assert_eq!(
12807 signature.signatures[signature.current_signature].label,
12808 "fn sample(param1: u8, param2: u8)"
12809 );
12810 });
12811}
12812
12813#[gpui::test]
12814async fn test_signature_help(cx: &mut TestAppContext) {
12815 init_test(cx, |_| {});
12816 cx.update(|cx| {
12817 cx.update_global::<SettingsStore, _>(|settings, cx| {
12818 settings.update_user_settings(cx, |settings| {
12819 settings.editor.auto_signature_help = Some(true);
12820 });
12821 });
12822 });
12823
12824 let mut cx = EditorLspTestContext::new_rust(
12825 lsp::ServerCapabilities {
12826 signature_help_provider: Some(lsp::SignatureHelpOptions {
12827 ..Default::default()
12828 }),
12829 ..Default::default()
12830 },
12831 cx,
12832 )
12833 .await;
12834
12835 // A test that directly calls `show_signature_help`
12836 cx.update_editor(|editor, window, cx| {
12837 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12838 });
12839
12840 let mocked_response = lsp::SignatureHelp {
12841 signatures: vec![lsp::SignatureInformation {
12842 label: "fn sample(param1: u8, param2: u8)".to_string(),
12843 documentation: None,
12844 parameters: Some(vec![
12845 lsp::ParameterInformation {
12846 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12847 documentation: None,
12848 },
12849 lsp::ParameterInformation {
12850 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12851 documentation: None,
12852 },
12853 ]),
12854 active_parameter: None,
12855 }],
12856 active_signature: Some(0),
12857 active_parameter: Some(0),
12858 };
12859 handle_signature_help_request(&mut cx, mocked_response).await;
12860
12861 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12862 .await;
12863
12864 cx.editor(|editor, _, _| {
12865 let signature_help_state = editor.signature_help_state.popover().cloned();
12866 assert!(signature_help_state.is_some());
12867 let signature = signature_help_state.unwrap();
12868 assert_eq!(
12869 signature.signatures[signature.current_signature].label,
12870 "fn sample(param1: u8, param2: u8)"
12871 );
12872 });
12873
12874 // When exiting outside from inside the brackets, `signature_help` is closed.
12875 cx.set_state(indoc! {"
12876 fn main() {
12877 sample(ˇ);
12878 }
12879
12880 fn sample(param1: u8, param2: u8) {}
12881 "});
12882
12883 cx.update_editor(|editor, window, cx| {
12884 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12885 s.select_ranges([0..0])
12886 });
12887 });
12888
12889 let mocked_response = lsp::SignatureHelp {
12890 signatures: Vec::new(),
12891 active_signature: None,
12892 active_parameter: None,
12893 };
12894 handle_signature_help_request(&mut cx, mocked_response).await;
12895
12896 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12897 .await;
12898
12899 cx.editor(|editor, _, _| {
12900 assert!(!editor.signature_help_state.is_shown());
12901 });
12902
12903 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12904 cx.set_state(indoc! {"
12905 fn main() {
12906 sample(ˇ);
12907 }
12908
12909 fn sample(param1: u8, param2: u8) {}
12910 "});
12911
12912 let mocked_response = lsp::SignatureHelp {
12913 signatures: vec![lsp::SignatureInformation {
12914 label: "fn sample(param1: u8, param2: u8)".to_string(),
12915 documentation: None,
12916 parameters: Some(vec![
12917 lsp::ParameterInformation {
12918 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12919 documentation: None,
12920 },
12921 lsp::ParameterInformation {
12922 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12923 documentation: None,
12924 },
12925 ]),
12926 active_parameter: None,
12927 }],
12928 active_signature: Some(0),
12929 active_parameter: Some(0),
12930 };
12931 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12932 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12933 .await;
12934 cx.editor(|editor, _, _| {
12935 assert!(editor.signature_help_state.is_shown());
12936 });
12937
12938 // Restore the popover with more parameter input
12939 cx.set_state(indoc! {"
12940 fn main() {
12941 sample(param1, param2ˇ);
12942 }
12943
12944 fn sample(param1: u8, param2: u8) {}
12945 "});
12946
12947 let mocked_response = lsp::SignatureHelp {
12948 signatures: vec![lsp::SignatureInformation {
12949 label: "fn sample(param1: u8, param2: u8)".to_string(),
12950 documentation: None,
12951 parameters: Some(vec![
12952 lsp::ParameterInformation {
12953 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12954 documentation: None,
12955 },
12956 lsp::ParameterInformation {
12957 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12958 documentation: None,
12959 },
12960 ]),
12961 active_parameter: None,
12962 }],
12963 active_signature: Some(0),
12964 active_parameter: Some(1),
12965 };
12966 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12967 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12968 .await;
12969
12970 // When selecting a range, the popover is gone.
12971 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12972 cx.update_editor(|editor, window, cx| {
12973 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12974 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12975 })
12976 });
12977 cx.assert_editor_state(indoc! {"
12978 fn main() {
12979 sample(param1, «ˇparam2»);
12980 }
12981
12982 fn sample(param1: u8, param2: u8) {}
12983 "});
12984 cx.editor(|editor, _, _| {
12985 assert!(!editor.signature_help_state.is_shown());
12986 });
12987
12988 // When unselecting again, the popover is back if within the brackets.
12989 cx.update_editor(|editor, window, cx| {
12990 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12991 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12992 })
12993 });
12994 cx.assert_editor_state(indoc! {"
12995 fn main() {
12996 sample(param1, ˇparam2);
12997 }
12998
12999 fn sample(param1: u8, param2: u8) {}
13000 "});
13001 handle_signature_help_request(&mut cx, mocked_response).await;
13002 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13003 .await;
13004 cx.editor(|editor, _, _| {
13005 assert!(editor.signature_help_state.is_shown());
13006 });
13007
13008 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13009 cx.update_editor(|editor, window, cx| {
13010 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13011 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13012 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13013 })
13014 });
13015 cx.assert_editor_state(indoc! {"
13016 fn main() {
13017 sample(param1, ˇparam2);
13018 }
13019
13020 fn sample(param1: u8, param2: u8) {}
13021 "});
13022
13023 let mocked_response = lsp::SignatureHelp {
13024 signatures: vec![lsp::SignatureInformation {
13025 label: "fn sample(param1: u8, param2: u8)".to_string(),
13026 documentation: None,
13027 parameters: Some(vec![
13028 lsp::ParameterInformation {
13029 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13030 documentation: None,
13031 },
13032 lsp::ParameterInformation {
13033 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13034 documentation: None,
13035 },
13036 ]),
13037 active_parameter: None,
13038 }],
13039 active_signature: Some(0),
13040 active_parameter: Some(1),
13041 };
13042 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13043 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13044 .await;
13045 cx.update_editor(|editor, _, cx| {
13046 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13047 });
13048 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13049 .await;
13050 cx.update_editor(|editor, window, cx| {
13051 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13052 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13053 })
13054 });
13055 cx.assert_editor_state(indoc! {"
13056 fn main() {
13057 sample(param1, «ˇparam2»);
13058 }
13059
13060 fn sample(param1: u8, param2: u8) {}
13061 "});
13062 cx.update_editor(|editor, window, cx| {
13063 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13064 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13065 })
13066 });
13067 cx.assert_editor_state(indoc! {"
13068 fn main() {
13069 sample(param1, ˇparam2);
13070 }
13071
13072 fn sample(param1: u8, param2: u8) {}
13073 "});
13074 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13075 .await;
13076}
13077
13078#[gpui::test]
13079async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13080 init_test(cx, |_| {});
13081
13082 let mut cx = EditorLspTestContext::new_rust(
13083 lsp::ServerCapabilities {
13084 signature_help_provider: Some(lsp::SignatureHelpOptions {
13085 ..Default::default()
13086 }),
13087 ..Default::default()
13088 },
13089 cx,
13090 )
13091 .await;
13092
13093 cx.set_state(indoc! {"
13094 fn main() {
13095 overloadedˇ
13096 }
13097 "});
13098
13099 cx.update_editor(|editor, window, cx| {
13100 editor.handle_input("(", window, cx);
13101 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13102 });
13103
13104 // Mock response with 3 signatures
13105 let mocked_response = lsp::SignatureHelp {
13106 signatures: vec![
13107 lsp::SignatureInformation {
13108 label: "fn overloaded(x: i32)".to_string(),
13109 documentation: None,
13110 parameters: Some(vec![lsp::ParameterInformation {
13111 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13112 documentation: None,
13113 }]),
13114 active_parameter: None,
13115 },
13116 lsp::SignatureInformation {
13117 label: "fn overloaded(x: i32, y: i32)".to_string(),
13118 documentation: None,
13119 parameters: Some(vec![
13120 lsp::ParameterInformation {
13121 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13122 documentation: None,
13123 },
13124 lsp::ParameterInformation {
13125 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13126 documentation: None,
13127 },
13128 ]),
13129 active_parameter: None,
13130 },
13131 lsp::SignatureInformation {
13132 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13133 documentation: None,
13134 parameters: Some(vec![
13135 lsp::ParameterInformation {
13136 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13137 documentation: None,
13138 },
13139 lsp::ParameterInformation {
13140 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13141 documentation: None,
13142 },
13143 lsp::ParameterInformation {
13144 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13145 documentation: None,
13146 },
13147 ]),
13148 active_parameter: None,
13149 },
13150 ],
13151 active_signature: Some(1),
13152 active_parameter: Some(0),
13153 };
13154 handle_signature_help_request(&mut cx, mocked_response).await;
13155
13156 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13157 .await;
13158
13159 // Verify we have multiple signatures and the right one is selected
13160 cx.editor(|editor, _, _| {
13161 let popover = editor.signature_help_state.popover().cloned().unwrap();
13162 assert_eq!(popover.signatures.len(), 3);
13163 // active_signature was 1, so that should be the current
13164 assert_eq!(popover.current_signature, 1);
13165 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13166 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13167 assert_eq!(
13168 popover.signatures[2].label,
13169 "fn overloaded(x: i32, y: i32, z: i32)"
13170 );
13171 });
13172
13173 // Test navigation functionality
13174 cx.update_editor(|editor, window, cx| {
13175 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13176 });
13177
13178 cx.editor(|editor, _, _| {
13179 let popover = editor.signature_help_state.popover().cloned().unwrap();
13180 assert_eq!(popover.current_signature, 2);
13181 });
13182
13183 // Test wrap around
13184 cx.update_editor(|editor, window, cx| {
13185 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13186 });
13187
13188 cx.editor(|editor, _, _| {
13189 let popover = editor.signature_help_state.popover().cloned().unwrap();
13190 assert_eq!(popover.current_signature, 0);
13191 });
13192
13193 // Test previous navigation
13194 cx.update_editor(|editor, window, cx| {
13195 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13196 });
13197
13198 cx.editor(|editor, _, _| {
13199 let popover = editor.signature_help_state.popover().cloned().unwrap();
13200 assert_eq!(popover.current_signature, 2);
13201 });
13202}
13203
13204#[gpui::test]
13205async fn test_completion_mode(cx: &mut TestAppContext) {
13206 init_test(cx, |_| {});
13207 let mut cx = EditorLspTestContext::new_rust(
13208 lsp::ServerCapabilities {
13209 completion_provider: Some(lsp::CompletionOptions {
13210 resolve_provider: Some(true),
13211 ..Default::default()
13212 }),
13213 ..Default::default()
13214 },
13215 cx,
13216 )
13217 .await;
13218
13219 struct Run {
13220 run_description: &'static str,
13221 initial_state: String,
13222 buffer_marked_text: String,
13223 completion_label: &'static str,
13224 completion_text: &'static str,
13225 expected_with_insert_mode: String,
13226 expected_with_replace_mode: String,
13227 expected_with_replace_subsequence_mode: String,
13228 expected_with_replace_suffix_mode: String,
13229 }
13230
13231 let runs = [
13232 Run {
13233 run_description: "Start of word matches completion text",
13234 initial_state: "before ediˇ after".into(),
13235 buffer_marked_text: "before <edi|> after".into(),
13236 completion_label: "editor",
13237 completion_text: "editor",
13238 expected_with_insert_mode: "before editorˇ after".into(),
13239 expected_with_replace_mode: "before editorˇ after".into(),
13240 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13241 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13242 },
13243 Run {
13244 run_description: "Accept same text at the middle of the word",
13245 initial_state: "before ediˇtor after".into(),
13246 buffer_marked_text: "before <edi|tor> after".into(),
13247 completion_label: "editor",
13248 completion_text: "editor",
13249 expected_with_insert_mode: "before editorˇtor after".into(),
13250 expected_with_replace_mode: "before editorˇ after".into(),
13251 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13252 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13253 },
13254 Run {
13255 run_description: "End of word matches completion text -- cursor at end",
13256 initial_state: "before torˇ after".into(),
13257 buffer_marked_text: "before <tor|> after".into(),
13258 completion_label: "editor",
13259 completion_text: "editor",
13260 expected_with_insert_mode: "before editorˇ after".into(),
13261 expected_with_replace_mode: "before editorˇ after".into(),
13262 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13263 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13264 },
13265 Run {
13266 run_description: "End of word matches completion text -- cursor at start",
13267 initial_state: "before ˇtor after".into(),
13268 buffer_marked_text: "before <|tor> after".into(),
13269 completion_label: "editor",
13270 completion_text: "editor",
13271 expected_with_insert_mode: "before editorˇtor after".into(),
13272 expected_with_replace_mode: "before editorˇ after".into(),
13273 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13274 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13275 },
13276 Run {
13277 run_description: "Prepend text containing whitespace",
13278 initial_state: "pˇfield: bool".into(),
13279 buffer_marked_text: "<p|field>: bool".into(),
13280 completion_label: "pub ",
13281 completion_text: "pub ",
13282 expected_with_insert_mode: "pub ˇfield: bool".into(),
13283 expected_with_replace_mode: "pub ˇ: bool".into(),
13284 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13285 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13286 },
13287 Run {
13288 run_description: "Add element to start of list",
13289 initial_state: "[element_ˇelement_2]".into(),
13290 buffer_marked_text: "[<element_|element_2>]".into(),
13291 completion_label: "element_1",
13292 completion_text: "element_1",
13293 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13294 expected_with_replace_mode: "[element_1ˇ]".into(),
13295 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13296 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13297 },
13298 Run {
13299 run_description: "Add element to start of list -- first and second elements are equal",
13300 initial_state: "[elˇelement]".into(),
13301 buffer_marked_text: "[<el|element>]".into(),
13302 completion_label: "element",
13303 completion_text: "element",
13304 expected_with_insert_mode: "[elementˇelement]".into(),
13305 expected_with_replace_mode: "[elementˇ]".into(),
13306 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13307 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13308 },
13309 Run {
13310 run_description: "Ends with matching suffix",
13311 initial_state: "SubˇError".into(),
13312 buffer_marked_text: "<Sub|Error>".into(),
13313 completion_label: "SubscriptionError",
13314 completion_text: "SubscriptionError",
13315 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13316 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13317 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13318 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13319 },
13320 Run {
13321 run_description: "Suffix is a subsequence -- contiguous",
13322 initial_state: "SubˇErr".into(),
13323 buffer_marked_text: "<Sub|Err>".into(),
13324 completion_label: "SubscriptionError",
13325 completion_text: "SubscriptionError",
13326 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13327 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13328 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13329 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13330 },
13331 Run {
13332 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13333 initial_state: "Suˇscrirr".into(),
13334 buffer_marked_text: "<Su|scrirr>".into(),
13335 completion_label: "SubscriptionError",
13336 completion_text: "SubscriptionError",
13337 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13338 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13339 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13340 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13341 },
13342 Run {
13343 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13344 initial_state: "foo(indˇix)".into(),
13345 buffer_marked_text: "foo(<ind|ix>)".into(),
13346 completion_label: "node_index",
13347 completion_text: "node_index",
13348 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13349 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13350 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13351 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13352 },
13353 Run {
13354 run_description: "Replace range ends before cursor - should extend to cursor",
13355 initial_state: "before editˇo after".into(),
13356 buffer_marked_text: "before <{ed}>it|o after".into(),
13357 completion_label: "editor",
13358 completion_text: "editor",
13359 expected_with_insert_mode: "before editorˇo after".into(),
13360 expected_with_replace_mode: "before editorˇo after".into(),
13361 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13362 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13363 },
13364 Run {
13365 run_description: "Uses label for suffix matching",
13366 initial_state: "before ediˇtor after".into(),
13367 buffer_marked_text: "before <edi|tor> after".into(),
13368 completion_label: "editor",
13369 completion_text: "editor()",
13370 expected_with_insert_mode: "before editor()ˇtor after".into(),
13371 expected_with_replace_mode: "before editor()ˇ after".into(),
13372 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13373 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13374 },
13375 Run {
13376 run_description: "Case insensitive subsequence and suffix matching",
13377 initial_state: "before EDiˇtoR after".into(),
13378 buffer_marked_text: "before <EDi|toR> after".into(),
13379 completion_label: "editor",
13380 completion_text: "editor",
13381 expected_with_insert_mode: "before editorˇtoR after".into(),
13382 expected_with_replace_mode: "before editorˇ after".into(),
13383 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13384 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13385 },
13386 ];
13387
13388 for run in runs {
13389 let run_variations = [
13390 (LspInsertMode::Insert, run.expected_with_insert_mode),
13391 (LspInsertMode::Replace, run.expected_with_replace_mode),
13392 (
13393 LspInsertMode::ReplaceSubsequence,
13394 run.expected_with_replace_subsequence_mode,
13395 ),
13396 (
13397 LspInsertMode::ReplaceSuffix,
13398 run.expected_with_replace_suffix_mode,
13399 ),
13400 ];
13401
13402 for (lsp_insert_mode, expected_text) in run_variations {
13403 eprintln!(
13404 "run = {:?}, mode = {lsp_insert_mode:.?}",
13405 run.run_description,
13406 );
13407
13408 update_test_language_settings(&mut cx, |settings| {
13409 settings.defaults.completions = Some(CompletionSettingsContent {
13410 lsp_insert_mode: Some(lsp_insert_mode),
13411 words: Some(WordsCompletionMode::Disabled),
13412 words_min_length: Some(0),
13413 ..Default::default()
13414 });
13415 });
13416
13417 cx.set_state(&run.initial_state);
13418 cx.update_editor(|editor, window, cx| {
13419 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13420 });
13421
13422 let counter = Arc::new(AtomicUsize::new(0));
13423 handle_completion_request_with_insert_and_replace(
13424 &mut cx,
13425 &run.buffer_marked_text,
13426 vec![(run.completion_label, run.completion_text)],
13427 counter.clone(),
13428 )
13429 .await;
13430 cx.condition(|editor, _| editor.context_menu_visible())
13431 .await;
13432 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13433
13434 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13435 editor
13436 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13437 .unwrap()
13438 });
13439 cx.assert_editor_state(&expected_text);
13440 handle_resolve_completion_request(&mut cx, None).await;
13441 apply_additional_edits.await.unwrap();
13442 }
13443 }
13444}
13445
13446#[gpui::test]
13447async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13448 init_test(cx, |_| {});
13449 let mut cx = EditorLspTestContext::new_rust(
13450 lsp::ServerCapabilities {
13451 completion_provider: Some(lsp::CompletionOptions {
13452 resolve_provider: Some(true),
13453 ..Default::default()
13454 }),
13455 ..Default::default()
13456 },
13457 cx,
13458 )
13459 .await;
13460
13461 let initial_state = "SubˇError";
13462 let buffer_marked_text = "<Sub|Error>";
13463 let completion_text = "SubscriptionError";
13464 let expected_with_insert_mode = "SubscriptionErrorˇError";
13465 let expected_with_replace_mode = "SubscriptionErrorˇ";
13466
13467 update_test_language_settings(&mut cx, |settings| {
13468 settings.defaults.completions = Some(CompletionSettingsContent {
13469 words: Some(WordsCompletionMode::Disabled),
13470 words_min_length: Some(0),
13471 // set the opposite here to ensure that the action is overriding the default behavior
13472 lsp_insert_mode: Some(LspInsertMode::Insert),
13473 ..Default::default()
13474 });
13475 });
13476
13477 cx.set_state(initial_state);
13478 cx.update_editor(|editor, window, cx| {
13479 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13480 });
13481
13482 let counter = Arc::new(AtomicUsize::new(0));
13483 handle_completion_request_with_insert_and_replace(
13484 &mut cx,
13485 buffer_marked_text,
13486 vec![(completion_text, completion_text)],
13487 counter.clone(),
13488 )
13489 .await;
13490 cx.condition(|editor, _| editor.context_menu_visible())
13491 .await;
13492 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13493
13494 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13495 editor
13496 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13497 .unwrap()
13498 });
13499 cx.assert_editor_state(expected_with_replace_mode);
13500 handle_resolve_completion_request(&mut cx, None).await;
13501 apply_additional_edits.await.unwrap();
13502
13503 update_test_language_settings(&mut cx, |settings| {
13504 settings.defaults.completions = Some(CompletionSettingsContent {
13505 words: Some(WordsCompletionMode::Disabled),
13506 words_min_length: Some(0),
13507 // set the opposite here to ensure that the action is overriding the default behavior
13508 lsp_insert_mode: Some(LspInsertMode::Replace),
13509 ..Default::default()
13510 });
13511 });
13512
13513 cx.set_state(initial_state);
13514 cx.update_editor(|editor, window, cx| {
13515 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13516 });
13517 handle_completion_request_with_insert_and_replace(
13518 &mut cx,
13519 buffer_marked_text,
13520 vec![(completion_text, completion_text)],
13521 counter.clone(),
13522 )
13523 .await;
13524 cx.condition(|editor, _| editor.context_menu_visible())
13525 .await;
13526 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13527
13528 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13529 editor
13530 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13531 .unwrap()
13532 });
13533 cx.assert_editor_state(expected_with_insert_mode);
13534 handle_resolve_completion_request(&mut cx, None).await;
13535 apply_additional_edits.await.unwrap();
13536}
13537
13538#[gpui::test]
13539async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13540 init_test(cx, |_| {});
13541 let mut cx = EditorLspTestContext::new_rust(
13542 lsp::ServerCapabilities {
13543 completion_provider: Some(lsp::CompletionOptions {
13544 resolve_provider: Some(true),
13545 ..Default::default()
13546 }),
13547 ..Default::default()
13548 },
13549 cx,
13550 )
13551 .await;
13552
13553 // scenario: surrounding text matches completion text
13554 let completion_text = "to_offset";
13555 let initial_state = indoc! {"
13556 1. buf.to_offˇsuffix
13557 2. buf.to_offˇsuf
13558 3. buf.to_offˇfix
13559 4. buf.to_offˇ
13560 5. into_offˇensive
13561 6. ˇsuffix
13562 7. let ˇ //
13563 8. aaˇzz
13564 9. buf.to_off«zzzzzˇ»suffix
13565 10. buf.«ˇzzzzz»suffix
13566 11. to_off«ˇzzzzz»
13567
13568 buf.to_offˇsuffix // newest cursor
13569 "};
13570 let completion_marked_buffer = indoc! {"
13571 1. buf.to_offsuffix
13572 2. buf.to_offsuf
13573 3. buf.to_offfix
13574 4. buf.to_off
13575 5. into_offensive
13576 6. suffix
13577 7. let //
13578 8. aazz
13579 9. buf.to_offzzzzzsuffix
13580 10. buf.zzzzzsuffix
13581 11. to_offzzzzz
13582
13583 buf.<to_off|suffix> // newest cursor
13584 "};
13585 let expected = indoc! {"
13586 1. buf.to_offsetˇ
13587 2. buf.to_offsetˇsuf
13588 3. buf.to_offsetˇfix
13589 4. buf.to_offsetˇ
13590 5. into_offsetˇensive
13591 6. to_offsetˇsuffix
13592 7. let to_offsetˇ //
13593 8. aato_offsetˇzz
13594 9. buf.to_offsetˇ
13595 10. buf.to_offsetˇsuffix
13596 11. to_offsetˇ
13597
13598 buf.to_offsetˇ // newest cursor
13599 "};
13600 cx.set_state(initial_state);
13601 cx.update_editor(|editor, window, cx| {
13602 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13603 });
13604 handle_completion_request_with_insert_and_replace(
13605 &mut cx,
13606 completion_marked_buffer,
13607 vec![(completion_text, completion_text)],
13608 Arc::new(AtomicUsize::new(0)),
13609 )
13610 .await;
13611 cx.condition(|editor, _| editor.context_menu_visible())
13612 .await;
13613 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13614 editor
13615 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13616 .unwrap()
13617 });
13618 cx.assert_editor_state(expected);
13619 handle_resolve_completion_request(&mut cx, None).await;
13620 apply_additional_edits.await.unwrap();
13621
13622 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13623 let completion_text = "foo_and_bar";
13624 let initial_state = indoc! {"
13625 1. ooanbˇ
13626 2. zooanbˇ
13627 3. ooanbˇz
13628 4. zooanbˇz
13629 5. ooanˇ
13630 6. oanbˇ
13631
13632 ooanbˇ
13633 "};
13634 let completion_marked_buffer = indoc! {"
13635 1. ooanb
13636 2. zooanb
13637 3. ooanbz
13638 4. zooanbz
13639 5. ooan
13640 6. oanb
13641
13642 <ooanb|>
13643 "};
13644 let expected = indoc! {"
13645 1. foo_and_barˇ
13646 2. zfoo_and_barˇ
13647 3. foo_and_barˇz
13648 4. zfoo_and_barˇz
13649 5. ooanfoo_and_barˇ
13650 6. oanbfoo_and_barˇ
13651
13652 foo_and_barˇ
13653 "};
13654 cx.set_state(initial_state);
13655 cx.update_editor(|editor, window, cx| {
13656 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13657 });
13658 handle_completion_request_with_insert_and_replace(
13659 &mut cx,
13660 completion_marked_buffer,
13661 vec![(completion_text, completion_text)],
13662 Arc::new(AtomicUsize::new(0)),
13663 )
13664 .await;
13665 cx.condition(|editor, _| editor.context_menu_visible())
13666 .await;
13667 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13668 editor
13669 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13670 .unwrap()
13671 });
13672 cx.assert_editor_state(expected);
13673 handle_resolve_completion_request(&mut cx, None).await;
13674 apply_additional_edits.await.unwrap();
13675
13676 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13677 // (expects the same as if it was inserted at the end)
13678 let completion_text = "foo_and_bar";
13679 let initial_state = indoc! {"
13680 1. ooˇanb
13681 2. zooˇanb
13682 3. ooˇanbz
13683 4. zooˇanbz
13684
13685 ooˇanb
13686 "};
13687 let completion_marked_buffer = indoc! {"
13688 1. ooanb
13689 2. zooanb
13690 3. ooanbz
13691 4. zooanbz
13692
13693 <oo|anb>
13694 "};
13695 let expected = indoc! {"
13696 1. foo_and_barˇ
13697 2. zfoo_and_barˇ
13698 3. foo_and_barˇz
13699 4. zfoo_and_barˇz
13700
13701 foo_and_barˇ
13702 "};
13703 cx.set_state(initial_state);
13704 cx.update_editor(|editor, window, cx| {
13705 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13706 });
13707 handle_completion_request_with_insert_and_replace(
13708 &mut cx,
13709 completion_marked_buffer,
13710 vec![(completion_text, completion_text)],
13711 Arc::new(AtomicUsize::new(0)),
13712 )
13713 .await;
13714 cx.condition(|editor, _| editor.context_menu_visible())
13715 .await;
13716 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13717 editor
13718 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13719 .unwrap()
13720 });
13721 cx.assert_editor_state(expected);
13722 handle_resolve_completion_request(&mut cx, None).await;
13723 apply_additional_edits.await.unwrap();
13724}
13725
13726// This used to crash
13727#[gpui::test]
13728async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13729 init_test(cx, |_| {});
13730
13731 let buffer_text = indoc! {"
13732 fn main() {
13733 10.satu;
13734
13735 //
13736 // separate cursors so they open in different excerpts (manually reproducible)
13737 //
13738
13739 10.satu20;
13740 }
13741 "};
13742 let multibuffer_text_with_selections = indoc! {"
13743 fn main() {
13744 10.satuˇ;
13745
13746 //
13747
13748 //
13749
13750 10.satuˇ20;
13751 }
13752 "};
13753 let expected_multibuffer = indoc! {"
13754 fn main() {
13755 10.saturating_sub()ˇ;
13756
13757 //
13758
13759 //
13760
13761 10.saturating_sub()ˇ;
13762 }
13763 "};
13764
13765 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13766 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13767
13768 let fs = FakeFs::new(cx.executor());
13769 fs.insert_tree(
13770 path!("/a"),
13771 json!({
13772 "main.rs": buffer_text,
13773 }),
13774 )
13775 .await;
13776
13777 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13778 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13779 language_registry.add(rust_lang());
13780 let mut fake_servers = language_registry.register_fake_lsp(
13781 "Rust",
13782 FakeLspAdapter {
13783 capabilities: lsp::ServerCapabilities {
13784 completion_provider: Some(lsp::CompletionOptions {
13785 resolve_provider: None,
13786 ..lsp::CompletionOptions::default()
13787 }),
13788 ..lsp::ServerCapabilities::default()
13789 },
13790 ..FakeLspAdapter::default()
13791 },
13792 );
13793 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13794 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13795 let buffer = project
13796 .update(cx, |project, cx| {
13797 project.open_local_buffer(path!("/a/main.rs"), cx)
13798 })
13799 .await
13800 .unwrap();
13801
13802 let multi_buffer = cx.new(|cx| {
13803 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13804 multi_buffer.push_excerpts(
13805 buffer.clone(),
13806 [ExcerptRange::new(0..first_excerpt_end)],
13807 cx,
13808 );
13809 multi_buffer.push_excerpts(
13810 buffer.clone(),
13811 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13812 cx,
13813 );
13814 multi_buffer
13815 });
13816
13817 let editor = workspace
13818 .update(cx, |_, window, cx| {
13819 cx.new(|cx| {
13820 Editor::new(
13821 EditorMode::Full {
13822 scale_ui_elements_with_buffer_font_size: false,
13823 show_active_line_background: false,
13824 sized_by_content: false,
13825 },
13826 multi_buffer.clone(),
13827 Some(project.clone()),
13828 window,
13829 cx,
13830 )
13831 })
13832 })
13833 .unwrap();
13834
13835 let pane = workspace
13836 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13837 .unwrap();
13838 pane.update_in(cx, |pane, window, cx| {
13839 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13840 });
13841
13842 let fake_server = fake_servers.next().await.unwrap();
13843
13844 editor.update_in(cx, |editor, window, cx| {
13845 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13846 s.select_ranges([
13847 Point::new(1, 11)..Point::new(1, 11),
13848 Point::new(7, 11)..Point::new(7, 11),
13849 ])
13850 });
13851
13852 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13853 });
13854
13855 editor.update_in(cx, |editor, window, cx| {
13856 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13857 });
13858
13859 fake_server
13860 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13861 let completion_item = lsp::CompletionItem {
13862 label: "saturating_sub()".into(),
13863 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13864 lsp::InsertReplaceEdit {
13865 new_text: "saturating_sub()".to_owned(),
13866 insert: lsp::Range::new(
13867 lsp::Position::new(7, 7),
13868 lsp::Position::new(7, 11),
13869 ),
13870 replace: lsp::Range::new(
13871 lsp::Position::new(7, 7),
13872 lsp::Position::new(7, 13),
13873 ),
13874 },
13875 )),
13876 ..lsp::CompletionItem::default()
13877 };
13878
13879 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13880 })
13881 .next()
13882 .await
13883 .unwrap();
13884
13885 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13886 .await;
13887
13888 editor
13889 .update_in(cx, |editor, window, cx| {
13890 editor
13891 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13892 .unwrap()
13893 })
13894 .await
13895 .unwrap();
13896
13897 editor.update(cx, |editor, cx| {
13898 assert_text_with_selections(editor, expected_multibuffer, cx);
13899 })
13900}
13901
13902#[gpui::test]
13903async fn test_completion(cx: &mut TestAppContext) {
13904 init_test(cx, |_| {});
13905
13906 let mut cx = EditorLspTestContext::new_rust(
13907 lsp::ServerCapabilities {
13908 completion_provider: Some(lsp::CompletionOptions {
13909 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13910 resolve_provider: Some(true),
13911 ..Default::default()
13912 }),
13913 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13914 ..Default::default()
13915 },
13916 cx,
13917 )
13918 .await;
13919 let counter = Arc::new(AtomicUsize::new(0));
13920
13921 cx.set_state(indoc! {"
13922 oneˇ
13923 two
13924 three
13925 "});
13926 cx.simulate_keystroke(".");
13927 handle_completion_request(
13928 indoc! {"
13929 one.|<>
13930 two
13931 three
13932 "},
13933 vec!["first_completion", "second_completion"],
13934 true,
13935 counter.clone(),
13936 &mut cx,
13937 )
13938 .await;
13939 cx.condition(|editor, _| editor.context_menu_visible())
13940 .await;
13941 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13942
13943 let _handler = handle_signature_help_request(
13944 &mut cx,
13945 lsp::SignatureHelp {
13946 signatures: vec![lsp::SignatureInformation {
13947 label: "test signature".to_string(),
13948 documentation: None,
13949 parameters: Some(vec![lsp::ParameterInformation {
13950 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13951 documentation: None,
13952 }]),
13953 active_parameter: None,
13954 }],
13955 active_signature: None,
13956 active_parameter: None,
13957 },
13958 );
13959 cx.update_editor(|editor, window, cx| {
13960 assert!(
13961 !editor.signature_help_state.is_shown(),
13962 "No signature help was called for"
13963 );
13964 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13965 });
13966 cx.run_until_parked();
13967 cx.update_editor(|editor, _, _| {
13968 assert!(
13969 !editor.signature_help_state.is_shown(),
13970 "No signature help should be shown when completions menu is open"
13971 );
13972 });
13973
13974 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13975 editor.context_menu_next(&Default::default(), window, cx);
13976 editor
13977 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13978 .unwrap()
13979 });
13980 cx.assert_editor_state(indoc! {"
13981 one.second_completionˇ
13982 two
13983 three
13984 "});
13985
13986 handle_resolve_completion_request(
13987 &mut cx,
13988 Some(vec![
13989 (
13990 //This overlaps with the primary completion edit which is
13991 //misbehavior from the LSP spec, test that we filter it out
13992 indoc! {"
13993 one.second_ˇcompletion
13994 two
13995 threeˇ
13996 "},
13997 "overlapping additional edit",
13998 ),
13999 (
14000 indoc! {"
14001 one.second_completion
14002 two
14003 threeˇ
14004 "},
14005 "\nadditional edit",
14006 ),
14007 ]),
14008 )
14009 .await;
14010 apply_additional_edits.await.unwrap();
14011 cx.assert_editor_state(indoc! {"
14012 one.second_completionˇ
14013 two
14014 three
14015 additional edit
14016 "});
14017
14018 cx.set_state(indoc! {"
14019 one.second_completion
14020 twoˇ
14021 threeˇ
14022 additional edit
14023 "});
14024 cx.simulate_keystroke(" ");
14025 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14026 cx.simulate_keystroke("s");
14027 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14028
14029 cx.assert_editor_state(indoc! {"
14030 one.second_completion
14031 two sˇ
14032 three sˇ
14033 additional edit
14034 "});
14035 handle_completion_request(
14036 indoc! {"
14037 one.second_completion
14038 two s
14039 three <s|>
14040 additional edit
14041 "},
14042 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14043 true,
14044 counter.clone(),
14045 &mut cx,
14046 )
14047 .await;
14048 cx.condition(|editor, _| editor.context_menu_visible())
14049 .await;
14050 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14051
14052 cx.simulate_keystroke("i");
14053
14054 handle_completion_request(
14055 indoc! {"
14056 one.second_completion
14057 two si
14058 three <si|>
14059 additional edit
14060 "},
14061 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14062 true,
14063 counter.clone(),
14064 &mut cx,
14065 )
14066 .await;
14067 cx.condition(|editor, _| editor.context_menu_visible())
14068 .await;
14069 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14070
14071 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14072 editor
14073 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14074 .unwrap()
14075 });
14076 cx.assert_editor_state(indoc! {"
14077 one.second_completion
14078 two sixth_completionˇ
14079 three sixth_completionˇ
14080 additional edit
14081 "});
14082
14083 apply_additional_edits.await.unwrap();
14084
14085 update_test_language_settings(&mut cx, |settings| {
14086 settings.defaults.show_completions_on_input = Some(false);
14087 });
14088 cx.set_state("editorˇ");
14089 cx.simulate_keystroke(".");
14090 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14091 cx.simulate_keystrokes("c l o");
14092 cx.assert_editor_state("editor.cloˇ");
14093 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14094 cx.update_editor(|editor, window, cx| {
14095 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14096 });
14097 handle_completion_request(
14098 "editor.<clo|>",
14099 vec!["close", "clobber"],
14100 true,
14101 counter.clone(),
14102 &mut cx,
14103 )
14104 .await;
14105 cx.condition(|editor, _| editor.context_menu_visible())
14106 .await;
14107 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14108
14109 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14110 editor
14111 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14112 .unwrap()
14113 });
14114 cx.assert_editor_state("editor.clobberˇ");
14115 handle_resolve_completion_request(&mut cx, None).await;
14116 apply_additional_edits.await.unwrap();
14117}
14118
14119#[gpui::test]
14120async fn test_completion_reuse(cx: &mut TestAppContext) {
14121 init_test(cx, |_| {});
14122
14123 let mut cx = EditorLspTestContext::new_rust(
14124 lsp::ServerCapabilities {
14125 completion_provider: Some(lsp::CompletionOptions {
14126 trigger_characters: Some(vec![".".to_string()]),
14127 ..Default::default()
14128 }),
14129 ..Default::default()
14130 },
14131 cx,
14132 )
14133 .await;
14134
14135 let counter = Arc::new(AtomicUsize::new(0));
14136 cx.set_state("objˇ");
14137 cx.simulate_keystroke(".");
14138
14139 // Initial completion request returns complete results
14140 let is_incomplete = false;
14141 handle_completion_request(
14142 "obj.|<>",
14143 vec!["a", "ab", "abc"],
14144 is_incomplete,
14145 counter.clone(),
14146 &mut cx,
14147 )
14148 .await;
14149 cx.run_until_parked();
14150 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14151 cx.assert_editor_state("obj.ˇ");
14152 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14153
14154 // Type "a" - filters existing completions
14155 cx.simulate_keystroke("a");
14156 cx.run_until_parked();
14157 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14158 cx.assert_editor_state("obj.aˇ");
14159 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14160
14161 // Type "b" - filters existing completions
14162 cx.simulate_keystroke("b");
14163 cx.run_until_parked();
14164 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14165 cx.assert_editor_state("obj.abˇ");
14166 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14167
14168 // Type "c" - filters existing completions
14169 cx.simulate_keystroke("c");
14170 cx.run_until_parked();
14171 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14172 cx.assert_editor_state("obj.abcˇ");
14173 check_displayed_completions(vec!["abc"], &mut cx);
14174
14175 // Backspace to delete "c" - filters existing completions
14176 cx.update_editor(|editor, window, cx| {
14177 editor.backspace(&Backspace, window, cx);
14178 });
14179 cx.run_until_parked();
14180 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14181 cx.assert_editor_state("obj.abˇ");
14182 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14183
14184 // Moving cursor to the left dismisses menu.
14185 cx.update_editor(|editor, window, cx| {
14186 editor.move_left(&MoveLeft, window, cx);
14187 });
14188 cx.run_until_parked();
14189 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14190 cx.assert_editor_state("obj.aˇb");
14191 cx.update_editor(|editor, _, _| {
14192 assert_eq!(editor.context_menu_visible(), false);
14193 });
14194
14195 // Type "b" - new request
14196 cx.simulate_keystroke("b");
14197 let is_incomplete = false;
14198 handle_completion_request(
14199 "obj.<ab|>a",
14200 vec!["ab", "abc"],
14201 is_incomplete,
14202 counter.clone(),
14203 &mut cx,
14204 )
14205 .await;
14206 cx.run_until_parked();
14207 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14208 cx.assert_editor_state("obj.abˇb");
14209 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14210
14211 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14212 cx.update_editor(|editor, window, cx| {
14213 editor.backspace(&Backspace, window, cx);
14214 });
14215 let is_incomplete = false;
14216 handle_completion_request(
14217 "obj.<a|>b",
14218 vec!["a", "ab", "abc"],
14219 is_incomplete,
14220 counter.clone(),
14221 &mut cx,
14222 )
14223 .await;
14224 cx.run_until_parked();
14225 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14226 cx.assert_editor_state("obj.aˇb");
14227 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14228
14229 // Backspace to delete "a" - dismisses menu.
14230 cx.update_editor(|editor, window, cx| {
14231 editor.backspace(&Backspace, window, cx);
14232 });
14233 cx.run_until_parked();
14234 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14235 cx.assert_editor_state("obj.ˇb");
14236 cx.update_editor(|editor, _, _| {
14237 assert_eq!(editor.context_menu_visible(), false);
14238 });
14239}
14240
14241#[gpui::test]
14242async fn test_word_completion(cx: &mut TestAppContext) {
14243 let lsp_fetch_timeout_ms = 10;
14244 init_test(cx, |language_settings| {
14245 language_settings.defaults.completions = Some(CompletionSettingsContent {
14246 words_min_length: Some(0),
14247 lsp_fetch_timeout_ms: Some(10),
14248 lsp_insert_mode: Some(LspInsertMode::Insert),
14249 ..Default::default()
14250 });
14251 });
14252
14253 let mut cx = EditorLspTestContext::new_rust(
14254 lsp::ServerCapabilities {
14255 completion_provider: Some(lsp::CompletionOptions {
14256 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14257 ..lsp::CompletionOptions::default()
14258 }),
14259 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14260 ..lsp::ServerCapabilities::default()
14261 },
14262 cx,
14263 )
14264 .await;
14265
14266 let throttle_completions = Arc::new(AtomicBool::new(false));
14267
14268 let lsp_throttle_completions = throttle_completions.clone();
14269 let _completion_requests_handler =
14270 cx.lsp
14271 .server
14272 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14273 let lsp_throttle_completions = lsp_throttle_completions.clone();
14274 let cx = cx.clone();
14275 async move {
14276 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14277 cx.background_executor()
14278 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14279 .await;
14280 }
14281 Ok(Some(lsp::CompletionResponse::Array(vec![
14282 lsp::CompletionItem {
14283 label: "first".into(),
14284 ..lsp::CompletionItem::default()
14285 },
14286 lsp::CompletionItem {
14287 label: "last".into(),
14288 ..lsp::CompletionItem::default()
14289 },
14290 ])))
14291 }
14292 });
14293
14294 cx.set_state(indoc! {"
14295 oneˇ
14296 two
14297 three
14298 "});
14299 cx.simulate_keystroke(".");
14300 cx.executor().run_until_parked();
14301 cx.condition(|editor, _| editor.context_menu_visible())
14302 .await;
14303 cx.update_editor(|editor, window, cx| {
14304 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14305 {
14306 assert_eq!(
14307 completion_menu_entries(menu),
14308 &["first", "last"],
14309 "When LSP server is fast to reply, no fallback word completions are used"
14310 );
14311 } else {
14312 panic!("expected completion menu to be open");
14313 }
14314 editor.cancel(&Cancel, window, cx);
14315 });
14316 cx.executor().run_until_parked();
14317 cx.condition(|editor, _| !editor.context_menu_visible())
14318 .await;
14319
14320 throttle_completions.store(true, atomic::Ordering::Release);
14321 cx.simulate_keystroke(".");
14322 cx.executor()
14323 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
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!(completion_menu_entries(menu), &["one", "three", "two"],
14331 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14332 } else {
14333 panic!("expected completion menu to be open");
14334 }
14335 });
14336}
14337
14338#[gpui::test]
14339async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14340 init_test(cx, |language_settings| {
14341 language_settings.defaults.completions = Some(CompletionSettingsContent {
14342 words: Some(WordsCompletionMode::Enabled),
14343 words_min_length: Some(0),
14344 lsp_insert_mode: Some(LspInsertMode::Insert),
14345 ..Default::default()
14346 });
14347 });
14348
14349 let mut cx = EditorLspTestContext::new_rust(
14350 lsp::ServerCapabilities {
14351 completion_provider: Some(lsp::CompletionOptions {
14352 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14353 ..lsp::CompletionOptions::default()
14354 }),
14355 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14356 ..lsp::ServerCapabilities::default()
14357 },
14358 cx,
14359 )
14360 .await;
14361
14362 let _completion_requests_handler =
14363 cx.lsp
14364 .server
14365 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14366 Ok(Some(lsp::CompletionResponse::Array(vec![
14367 lsp::CompletionItem {
14368 label: "first".into(),
14369 ..lsp::CompletionItem::default()
14370 },
14371 lsp::CompletionItem {
14372 label: "last".into(),
14373 ..lsp::CompletionItem::default()
14374 },
14375 ])))
14376 });
14377
14378 cx.set_state(indoc! {"ˇ
14379 first
14380 last
14381 second
14382 "});
14383 cx.simulate_keystroke(".");
14384 cx.executor().run_until_parked();
14385 cx.condition(|editor, _| editor.context_menu_visible())
14386 .await;
14387 cx.update_editor(|editor, _, _| {
14388 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14389 {
14390 assert_eq!(
14391 completion_menu_entries(menu),
14392 &["first", "last", "second"],
14393 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14394 );
14395 } else {
14396 panic!("expected completion menu to be open");
14397 }
14398 });
14399}
14400
14401#[gpui::test]
14402async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14403 init_test(cx, |language_settings| {
14404 language_settings.defaults.completions = Some(CompletionSettingsContent {
14405 words: Some(WordsCompletionMode::Disabled),
14406 words_min_length: Some(0),
14407 lsp_insert_mode: Some(LspInsertMode::Insert),
14408 ..Default::default()
14409 });
14410 });
14411
14412 let mut cx = EditorLspTestContext::new_rust(
14413 lsp::ServerCapabilities {
14414 completion_provider: Some(lsp::CompletionOptions {
14415 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14416 ..lsp::CompletionOptions::default()
14417 }),
14418 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14419 ..lsp::ServerCapabilities::default()
14420 },
14421 cx,
14422 )
14423 .await;
14424
14425 let _completion_requests_handler =
14426 cx.lsp
14427 .server
14428 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14429 panic!("LSP completions should not be queried when dealing with word completions")
14430 });
14431
14432 cx.set_state(indoc! {"ˇ
14433 first
14434 last
14435 second
14436 "});
14437 cx.update_editor(|editor, window, cx| {
14438 editor.show_word_completions(&ShowWordCompletions, window, cx);
14439 });
14440 cx.executor().run_until_parked();
14441 cx.condition(|editor, _| editor.context_menu_visible())
14442 .await;
14443 cx.update_editor(|editor, _, _| {
14444 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14445 {
14446 assert_eq!(
14447 completion_menu_entries(menu),
14448 &["first", "last", "second"],
14449 "`ShowWordCompletions` action should show word completions"
14450 );
14451 } else {
14452 panic!("expected completion menu to be open");
14453 }
14454 });
14455
14456 cx.simulate_keystroke("l");
14457 cx.executor().run_until_parked();
14458 cx.condition(|editor, _| editor.context_menu_visible())
14459 .await;
14460 cx.update_editor(|editor, _, _| {
14461 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14462 {
14463 assert_eq!(
14464 completion_menu_entries(menu),
14465 &["last"],
14466 "After showing word completions, further editing should filter them and not query the LSP"
14467 );
14468 } else {
14469 panic!("expected completion menu to be open");
14470 }
14471 });
14472}
14473
14474#[gpui::test]
14475async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14476 init_test(cx, |language_settings| {
14477 language_settings.defaults.completions = Some(CompletionSettingsContent {
14478 words_min_length: Some(0),
14479 lsp: Some(false),
14480 lsp_insert_mode: Some(LspInsertMode::Insert),
14481 ..Default::default()
14482 });
14483 });
14484
14485 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14486
14487 cx.set_state(indoc! {"ˇ
14488 0_usize
14489 let
14490 33
14491 4.5f32
14492 "});
14493 cx.update_editor(|editor, window, cx| {
14494 editor.show_completions(&ShowCompletions::default(), window, cx);
14495 });
14496 cx.executor().run_until_parked();
14497 cx.condition(|editor, _| editor.context_menu_visible())
14498 .await;
14499 cx.update_editor(|editor, window, cx| {
14500 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14501 {
14502 assert_eq!(
14503 completion_menu_entries(menu),
14504 &["let"],
14505 "With no digits in the completion query, no digits should be in the word completions"
14506 );
14507 } else {
14508 panic!("expected completion menu to be open");
14509 }
14510 editor.cancel(&Cancel, window, cx);
14511 });
14512
14513 cx.set_state(indoc! {"3ˇ
14514 0_usize
14515 let
14516 3
14517 33.35f32
14518 "});
14519 cx.update_editor(|editor, window, cx| {
14520 editor.show_completions(&ShowCompletions::default(), window, cx);
14521 });
14522 cx.executor().run_until_parked();
14523 cx.condition(|editor, _| editor.context_menu_visible())
14524 .await;
14525 cx.update_editor(|editor, _, _| {
14526 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14527 {
14528 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14529 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14530 } else {
14531 panic!("expected completion menu to be open");
14532 }
14533 });
14534}
14535
14536#[gpui::test]
14537async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14538 init_test(cx, |language_settings| {
14539 language_settings.defaults.completions = Some(CompletionSettingsContent {
14540 words: Some(WordsCompletionMode::Enabled),
14541 words_min_length: Some(3),
14542 lsp_insert_mode: Some(LspInsertMode::Insert),
14543 ..Default::default()
14544 });
14545 });
14546
14547 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14548 cx.set_state(indoc! {"ˇ
14549 wow
14550 wowen
14551 wowser
14552 "});
14553 cx.simulate_keystroke("w");
14554 cx.executor().run_until_parked();
14555 cx.update_editor(|editor, _, _| {
14556 if editor.context_menu.borrow_mut().is_some() {
14557 panic!(
14558 "expected completion menu to be hidden, as words completion threshold is not met"
14559 );
14560 }
14561 });
14562
14563 cx.update_editor(|editor, window, cx| {
14564 editor.show_word_completions(&ShowWordCompletions, window, cx);
14565 });
14566 cx.executor().run_until_parked();
14567 cx.update_editor(|editor, window, cx| {
14568 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14569 {
14570 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");
14571 } else {
14572 panic!("expected completion menu to be open after the word completions are called with an action");
14573 }
14574
14575 editor.cancel(&Cancel, window, cx);
14576 });
14577 cx.update_editor(|editor, _, _| {
14578 if editor.context_menu.borrow_mut().is_some() {
14579 panic!("expected completion menu to be hidden after canceling");
14580 }
14581 });
14582
14583 cx.simulate_keystroke("o");
14584 cx.executor().run_until_parked();
14585 cx.update_editor(|editor, _, _| {
14586 if editor.context_menu.borrow_mut().is_some() {
14587 panic!(
14588 "expected completion menu to be hidden, as words completion threshold is not met still"
14589 );
14590 }
14591 });
14592
14593 cx.simulate_keystroke("w");
14594 cx.executor().run_until_parked();
14595 cx.update_editor(|editor, _, _| {
14596 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14597 {
14598 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14599 } else {
14600 panic!("expected completion menu to be open after the word completions threshold is met");
14601 }
14602 });
14603}
14604
14605#[gpui::test]
14606async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14607 init_test(cx, |language_settings| {
14608 language_settings.defaults.completions = Some(CompletionSettingsContent {
14609 words: Some(WordsCompletionMode::Enabled),
14610 words_min_length: Some(0),
14611 lsp_insert_mode: Some(LspInsertMode::Insert),
14612 ..Default::default()
14613 });
14614 });
14615
14616 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14617 cx.update_editor(|editor, _, _| {
14618 editor.disable_word_completions();
14619 });
14620 cx.set_state(indoc! {"ˇ
14621 wow
14622 wowen
14623 wowser
14624 "});
14625 cx.simulate_keystroke("w");
14626 cx.executor().run_until_parked();
14627 cx.update_editor(|editor, _, _| {
14628 if editor.context_menu.borrow_mut().is_some() {
14629 panic!(
14630 "expected completion menu to be hidden, as words completion are disabled for this editor"
14631 );
14632 }
14633 });
14634
14635 cx.update_editor(|editor, window, cx| {
14636 editor.show_word_completions(&ShowWordCompletions, window, cx);
14637 });
14638 cx.executor().run_until_parked();
14639 cx.update_editor(|editor, _, _| {
14640 if editor.context_menu.borrow_mut().is_some() {
14641 panic!(
14642 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14643 );
14644 }
14645 });
14646}
14647
14648fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14649 let position = || lsp::Position {
14650 line: params.text_document_position.position.line,
14651 character: params.text_document_position.position.character,
14652 };
14653 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14654 range: lsp::Range {
14655 start: position(),
14656 end: position(),
14657 },
14658 new_text: text.to_string(),
14659 }))
14660}
14661
14662#[gpui::test]
14663async fn test_multiline_completion(cx: &mut TestAppContext) {
14664 init_test(cx, |_| {});
14665
14666 let fs = FakeFs::new(cx.executor());
14667 fs.insert_tree(
14668 path!("/a"),
14669 json!({
14670 "main.ts": "a",
14671 }),
14672 )
14673 .await;
14674
14675 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14676 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14677 let typescript_language = Arc::new(Language::new(
14678 LanguageConfig {
14679 name: "TypeScript".into(),
14680 matcher: LanguageMatcher {
14681 path_suffixes: vec!["ts".to_string()],
14682 ..LanguageMatcher::default()
14683 },
14684 line_comments: vec!["// ".into()],
14685 ..LanguageConfig::default()
14686 },
14687 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14688 ));
14689 language_registry.add(typescript_language.clone());
14690 let mut fake_servers = language_registry.register_fake_lsp(
14691 "TypeScript",
14692 FakeLspAdapter {
14693 capabilities: lsp::ServerCapabilities {
14694 completion_provider: Some(lsp::CompletionOptions {
14695 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14696 ..lsp::CompletionOptions::default()
14697 }),
14698 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14699 ..lsp::ServerCapabilities::default()
14700 },
14701 // Emulate vtsls label generation
14702 label_for_completion: Some(Box::new(|item, _| {
14703 let text = if let Some(description) = item
14704 .label_details
14705 .as_ref()
14706 .and_then(|label_details| label_details.description.as_ref())
14707 {
14708 format!("{} {}", item.label, description)
14709 } else if let Some(detail) = &item.detail {
14710 format!("{} {}", item.label, detail)
14711 } else {
14712 item.label.clone()
14713 };
14714 let len = text.len();
14715 Some(language::CodeLabel {
14716 text,
14717 runs: Vec::new(),
14718 filter_range: 0..len,
14719 })
14720 })),
14721 ..FakeLspAdapter::default()
14722 },
14723 );
14724 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14725 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14726 let worktree_id = workspace
14727 .update(cx, |workspace, _window, cx| {
14728 workspace.project().update(cx, |project, cx| {
14729 project.worktrees(cx).next().unwrap().read(cx).id()
14730 })
14731 })
14732 .unwrap();
14733 let _buffer = project
14734 .update(cx, |project, cx| {
14735 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14736 })
14737 .await
14738 .unwrap();
14739 let editor = workspace
14740 .update(cx, |workspace, window, cx| {
14741 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14742 })
14743 .unwrap()
14744 .await
14745 .unwrap()
14746 .downcast::<Editor>()
14747 .unwrap();
14748 let fake_server = fake_servers.next().await.unwrap();
14749
14750 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14751 let multiline_label_2 = "a\nb\nc\n";
14752 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14753 let multiline_description = "d\ne\nf\n";
14754 let multiline_detail_2 = "g\nh\ni\n";
14755
14756 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14757 move |params, _| async move {
14758 Ok(Some(lsp::CompletionResponse::Array(vec![
14759 lsp::CompletionItem {
14760 label: multiline_label.to_string(),
14761 text_edit: gen_text_edit(¶ms, "new_text_1"),
14762 ..lsp::CompletionItem::default()
14763 },
14764 lsp::CompletionItem {
14765 label: "single line label 1".to_string(),
14766 detail: Some(multiline_detail.to_string()),
14767 text_edit: gen_text_edit(¶ms, "new_text_2"),
14768 ..lsp::CompletionItem::default()
14769 },
14770 lsp::CompletionItem {
14771 label: "single line label 2".to_string(),
14772 label_details: Some(lsp::CompletionItemLabelDetails {
14773 description: Some(multiline_description.to_string()),
14774 detail: None,
14775 }),
14776 text_edit: gen_text_edit(¶ms, "new_text_2"),
14777 ..lsp::CompletionItem::default()
14778 },
14779 lsp::CompletionItem {
14780 label: multiline_label_2.to_string(),
14781 detail: Some(multiline_detail_2.to_string()),
14782 text_edit: gen_text_edit(¶ms, "new_text_3"),
14783 ..lsp::CompletionItem::default()
14784 },
14785 lsp::CompletionItem {
14786 label: "Label with many spaces and \t but without newlines".to_string(),
14787 detail: Some(
14788 "Details with many spaces and \t but without newlines".to_string(),
14789 ),
14790 text_edit: gen_text_edit(¶ms, "new_text_4"),
14791 ..lsp::CompletionItem::default()
14792 },
14793 ])))
14794 },
14795 );
14796
14797 editor.update_in(cx, |editor, window, cx| {
14798 cx.focus_self(window);
14799 editor.move_to_end(&MoveToEnd, window, cx);
14800 editor.handle_input(".", window, cx);
14801 });
14802 cx.run_until_parked();
14803 completion_handle.next().await.unwrap();
14804
14805 editor.update(cx, |editor, _| {
14806 assert!(editor.context_menu_visible());
14807 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14808 {
14809 let completion_labels = menu
14810 .completions
14811 .borrow()
14812 .iter()
14813 .map(|c| c.label.text.clone())
14814 .collect::<Vec<_>>();
14815 assert_eq!(
14816 completion_labels,
14817 &[
14818 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14819 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14820 "single line label 2 d e f ",
14821 "a b c g h i ",
14822 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14823 ],
14824 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14825 );
14826
14827 for completion in menu
14828 .completions
14829 .borrow()
14830 .iter() {
14831 assert_eq!(
14832 completion.label.filter_range,
14833 0..completion.label.text.len(),
14834 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14835 );
14836 }
14837 } else {
14838 panic!("expected completion menu to be open");
14839 }
14840 });
14841}
14842
14843#[gpui::test]
14844async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14845 init_test(cx, |_| {});
14846 let mut cx = EditorLspTestContext::new_rust(
14847 lsp::ServerCapabilities {
14848 completion_provider: Some(lsp::CompletionOptions {
14849 trigger_characters: Some(vec![".".to_string()]),
14850 ..Default::default()
14851 }),
14852 ..Default::default()
14853 },
14854 cx,
14855 )
14856 .await;
14857 cx.lsp
14858 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14859 Ok(Some(lsp::CompletionResponse::Array(vec![
14860 lsp::CompletionItem {
14861 label: "first".into(),
14862 ..Default::default()
14863 },
14864 lsp::CompletionItem {
14865 label: "last".into(),
14866 ..Default::default()
14867 },
14868 ])))
14869 });
14870 cx.set_state("variableˇ");
14871 cx.simulate_keystroke(".");
14872 cx.executor().run_until_parked();
14873
14874 cx.update_editor(|editor, _, _| {
14875 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14876 {
14877 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14878 } else {
14879 panic!("expected completion menu to be open");
14880 }
14881 });
14882
14883 cx.update_editor(|editor, window, cx| {
14884 editor.move_page_down(&MovePageDown::default(), window, cx);
14885 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14886 {
14887 assert!(
14888 menu.selected_item == 1,
14889 "expected PageDown to select the last item from the context menu"
14890 );
14891 } else {
14892 panic!("expected completion menu to stay open after PageDown");
14893 }
14894 });
14895
14896 cx.update_editor(|editor, window, cx| {
14897 editor.move_page_up(&MovePageUp::default(), window, cx);
14898 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14899 {
14900 assert!(
14901 menu.selected_item == 0,
14902 "expected PageUp to select the first item from the context menu"
14903 );
14904 } else {
14905 panic!("expected completion menu to stay open after PageUp");
14906 }
14907 });
14908}
14909
14910#[gpui::test]
14911async fn test_as_is_completions(cx: &mut TestAppContext) {
14912 init_test(cx, |_| {});
14913 let mut cx = EditorLspTestContext::new_rust(
14914 lsp::ServerCapabilities {
14915 completion_provider: Some(lsp::CompletionOptions {
14916 ..Default::default()
14917 }),
14918 ..Default::default()
14919 },
14920 cx,
14921 )
14922 .await;
14923 cx.lsp
14924 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14925 Ok(Some(lsp::CompletionResponse::Array(vec![
14926 lsp::CompletionItem {
14927 label: "unsafe".into(),
14928 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14929 range: lsp::Range {
14930 start: lsp::Position {
14931 line: 1,
14932 character: 2,
14933 },
14934 end: lsp::Position {
14935 line: 1,
14936 character: 3,
14937 },
14938 },
14939 new_text: "unsafe".to_string(),
14940 })),
14941 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14942 ..Default::default()
14943 },
14944 ])))
14945 });
14946 cx.set_state("fn a() {}\n nˇ");
14947 cx.executor().run_until_parked();
14948 cx.update_editor(|editor, window, cx| {
14949 editor.show_completions(
14950 &ShowCompletions {
14951 trigger: Some("\n".into()),
14952 },
14953 window,
14954 cx,
14955 );
14956 });
14957 cx.executor().run_until_parked();
14958
14959 cx.update_editor(|editor, window, cx| {
14960 editor.confirm_completion(&Default::default(), window, cx)
14961 });
14962 cx.executor().run_until_parked();
14963 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14964}
14965
14966#[gpui::test]
14967async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14968 init_test(cx, |_| {});
14969 let language =
14970 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14971 let mut cx = EditorLspTestContext::new(
14972 language,
14973 lsp::ServerCapabilities {
14974 completion_provider: Some(lsp::CompletionOptions {
14975 ..lsp::CompletionOptions::default()
14976 }),
14977 ..lsp::ServerCapabilities::default()
14978 },
14979 cx,
14980 )
14981 .await;
14982
14983 cx.set_state(
14984 "#ifndef BAR_H
14985#define BAR_H
14986
14987#include <stdbool.h>
14988
14989int fn_branch(bool do_branch1, bool do_branch2);
14990
14991#endif // BAR_H
14992ˇ",
14993 );
14994 cx.executor().run_until_parked();
14995 cx.update_editor(|editor, window, cx| {
14996 editor.handle_input("#", window, cx);
14997 });
14998 cx.executor().run_until_parked();
14999 cx.update_editor(|editor, window, cx| {
15000 editor.handle_input("i", window, cx);
15001 });
15002 cx.executor().run_until_parked();
15003 cx.update_editor(|editor, window, cx| {
15004 editor.handle_input("n", window, cx);
15005 });
15006 cx.executor().run_until_parked();
15007 cx.assert_editor_state(
15008 "#ifndef BAR_H
15009#define BAR_H
15010
15011#include <stdbool.h>
15012
15013int fn_branch(bool do_branch1, bool do_branch2);
15014
15015#endif // BAR_H
15016#inˇ",
15017 );
15018
15019 cx.lsp
15020 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15021 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15022 is_incomplete: false,
15023 item_defaults: None,
15024 items: vec![lsp::CompletionItem {
15025 kind: Some(lsp::CompletionItemKind::SNIPPET),
15026 label_details: Some(lsp::CompletionItemLabelDetails {
15027 detail: Some("header".to_string()),
15028 description: None,
15029 }),
15030 label: " include".to_string(),
15031 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15032 range: lsp::Range {
15033 start: lsp::Position {
15034 line: 8,
15035 character: 1,
15036 },
15037 end: lsp::Position {
15038 line: 8,
15039 character: 1,
15040 },
15041 },
15042 new_text: "include \"$0\"".to_string(),
15043 })),
15044 sort_text: Some("40b67681include".to_string()),
15045 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15046 filter_text: Some("include".to_string()),
15047 insert_text: Some("include \"$0\"".to_string()),
15048 ..lsp::CompletionItem::default()
15049 }],
15050 })))
15051 });
15052 cx.update_editor(|editor, window, cx| {
15053 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15054 });
15055 cx.executor().run_until_parked();
15056 cx.update_editor(|editor, window, cx| {
15057 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15058 });
15059 cx.executor().run_until_parked();
15060 cx.assert_editor_state(
15061 "#ifndef BAR_H
15062#define BAR_H
15063
15064#include <stdbool.h>
15065
15066int fn_branch(bool do_branch1, bool do_branch2);
15067
15068#endif // BAR_H
15069#include \"ˇ\"",
15070 );
15071
15072 cx.lsp
15073 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15074 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15075 is_incomplete: true,
15076 item_defaults: None,
15077 items: vec![lsp::CompletionItem {
15078 kind: Some(lsp::CompletionItemKind::FILE),
15079 label: "AGL/".to_string(),
15080 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15081 range: lsp::Range {
15082 start: lsp::Position {
15083 line: 8,
15084 character: 10,
15085 },
15086 end: lsp::Position {
15087 line: 8,
15088 character: 11,
15089 },
15090 },
15091 new_text: "AGL/".to_string(),
15092 })),
15093 sort_text: Some("40b67681AGL/".to_string()),
15094 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15095 filter_text: Some("AGL/".to_string()),
15096 insert_text: Some("AGL/".to_string()),
15097 ..lsp::CompletionItem::default()
15098 }],
15099 })))
15100 });
15101 cx.update_editor(|editor, window, cx| {
15102 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15103 });
15104 cx.executor().run_until_parked();
15105 cx.update_editor(|editor, window, cx| {
15106 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15107 });
15108 cx.executor().run_until_parked();
15109 cx.assert_editor_state(
15110 r##"#ifndef BAR_H
15111#define BAR_H
15112
15113#include <stdbool.h>
15114
15115int fn_branch(bool do_branch1, bool do_branch2);
15116
15117#endif // BAR_H
15118#include "AGL/ˇ"##,
15119 );
15120
15121 cx.update_editor(|editor, window, cx| {
15122 editor.handle_input("\"", window, cx);
15123 });
15124 cx.executor().run_until_parked();
15125 cx.assert_editor_state(
15126 r##"#ifndef BAR_H
15127#define BAR_H
15128
15129#include <stdbool.h>
15130
15131int fn_branch(bool do_branch1, bool do_branch2);
15132
15133#endif // BAR_H
15134#include "AGL/"ˇ"##,
15135 );
15136}
15137
15138#[gpui::test]
15139async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15140 init_test(cx, |_| {});
15141
15142 let mut cx = EditorLspTestContext::new_rust(
15143 lsp::ServerCapabilities {
15144 completion_provider: Some(lsp::CompletionOptions {
15145 trigger_characters: Some(vec![".".to_string()]),
15146 resolve_provider: Some(true),
15147 ..Default::default()
15148 }),
15149 ..Default::default()
15150 },
15151 cx,
15152 )
15153 .await;
15154
15155 cx.set_state("fn main() { let a = 2ˇ; }");
15156 cx.simulate_keystroke(".");
15157 let completion_item = lsp::CompletionItem {
15158 label: "Some".into(),
15159 kind: Some(lsp::CompletionItemKind::SNIPPET),
15160 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15161 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15162 kind: lsp::MarkupKind::Markdown,
15163 value: "```rust\nSome(2)\n```".to_string(),
15164 })),
15165 deprecated: Some(false),
15166 sort_text: Some("Some".to_string()),
15167 filter_text: Some("Some".to_string()),
15168 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15169 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15170 range: lsp::Range {
15171 start: lsp::Position {
15172 line: 0,
15173 character: 22,
15174 },
15175 end: lsp::Position {
15176 line: 0,
15177 character: 22,
15178 },
15179 },
15180 new_text: "Some(2)".to_string(),
15181 })),
15182 additional_text_edits: Some(vec![lsp::TextEdit {
15183 range: lsp::Range {
15184 start: lsp::Position {
15185 line: 0,
15186 character: 20,
15187 },
15188 end: lsp::Position {
15189 line: 0,
15190 character: 22,
15191 },
15192 },
15193 new_text: "".to_string(),
15194 }]),
15195 ..Default::default()
15196 };
15197
15198 let closure_completion_item = completion_item.clone();
15199 let counter = Arc::new(AtomicUsize::new(0));
15200 let counter_clone = counter.clone();
15201 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15202 let task_completion_item = closure_completion_item.clone();
15203 counter_clone.fetch_add(1, atomic::Ordering::Release);
15204 async move {
15205 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15206 is_incomplete: true,
15207 item_defaults: None,
15208 items: vec![task_completion_item],
15209 })))
15210 }
15211 });
15212
15213 cx.condition(|editor, _| editor.context_menu_visible())
15214 .await;
15215 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15216 assert!(request.next().await.is_some());
15217 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15218
15219 cx.simulate_keystrokes("S o m");
15220 cx.condition(|editor, _| editor.context_menu_visible())
15221 .await;
15222 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15223 assert!(request.next().await.is_some());
15224 assert!(request.next().await.is_some());
15225 assert!(request.next().await.is_some());
15226 request.close();
15227 assert!(request.next().await.is_none());
15228 assert_eq!(
15229 counter.load(atomic::Ordering::Acquire),
15230 4,
15231 "With the completions menu open, only one LSP request should happen per input"
15232 );
15233}
15234
15235#[gpui::test]
15236async fn test_toggle_comment(cx: &mut TestAppContext) {
15237 init_test(cx, |_| {});
15238 let mut cx = EditorTestContext::new(cx).await;
15239 let language = Arc::new(Language::new(
15240 LanguageConfig {
15241 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15242 ..Default::default()
15243 },
15244 Some(tree_sitter_rust::LANGUAGE.into()),
15245 ));
15246 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15247
15248 // If multiple selections intersect a line, the line is only toggled once.
15249 cx.set_state(indoc! {"
15250 fn a() {
15251 «//b();
15252 ˇ»// «c();
15253 //ˇ» d();
15254 }
15255 "});
15256
15257 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15258
15259 cx.assert_editor_state(indoc! {"
15260 fn a() {
15261 «b();
15262 c();
15263 ˇ» d();
15264 }
15265 "});
15266
15267 // The comment prefix is inserted at the same column for every line in a
15268 // selection.
15269 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15270
15271 cx.assert_editor_state(indoc! {"
15272 fn a() {
15273 // «b();
15274 // c();
15275 ˇ»// d();
15276 }
15277 "});
15278
15279 // If a selection ends at the beginning of a line, that line is not toggled.
15280 cx.set_selections_state(indoc! {"
15281 fn a() {
15282 // b();
15283 «// c();
15284 ˇ» // d();
15285 }
15286 "});
15287
15288 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15289
15290 cx.assert_editor_state(indoc! {"
15291 fn a() {
15292 // b();
15293 «c();
15294 ˇ» // d();
15295 }
15296 "});
15297
15298 // If a selection span a single line and is empty, the line is toggled.
15299 cx.set_state(indoc! {"
15300 fn a() {
15301 a();
15302 b();
15303 ˇ
15304 }
15305 "});
15306
15307 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15308
15309 cx.assert_editor_state(indoc! {"
15310 fn a() {
15311 a();
15312 b();
15313 //•ˇ
15314 }
15315 "});
15316
15317 // If a selection span multiple lines, empty lines are not toggled.
15318 cx.set_state(indoc! {"
15319 fn a() {
15320 «a();
15321
15322 c();ˇ»
15323 }
15324 "});
15325
15326 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15327
15328 cx.assert_editor_state(indoc! {"
15329 fn a() {
15330 // «a();
15331
15332 // c();ˇ»
15333 }
15334 "});
15335
15336 // If a selection includes multiple comment prefixes, all lines are uncommented.
15337 cx.set_state(indoc! {"
15338 fn a() {
15339 «// a();
15340 /// b();
15341 //! c();ˇ»
15342 }
15343 "});
15344
15345 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15346
15347 cx.assert_editor_state(indoc! {"
15348 fn a() {
15349 «a();
15350 b();
15351 c();ˇ»
15352 }
15353 "});
15354}
15355
15356#[gpui::test]
15357async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15358 init_test(cx, |_| {});
15359 let mut cx = EditorTestContext::new(cx).await;
15360 let language = Arc::new(Language::new(
15361 LanguageConfig {
15362 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15363 ..Default::default()
15364 },
15365 Some(tree_sitter_rust::LANGUAGE.into()),
15366 ));
15367 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15368
15369 let toggle_comments = &ToggleComments {
15370 advance_downwards: false,
15371 ignore_indent: true,
15372 };
15373
15374 // If multiple selections intersect a line, the line is only toggled once.
15375 cx.set_state(indoc! {"
15376 fn a() {
15377 // «b();
15378 // c();
15379 // ˇ» d();
15380 }
15381 "});
15382
15383 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15384
15385 cx.assert_editor_state(indoc! {"
15386 fn a() {
15387 «b();
15388 c();
15389 ˇ» d();
15390 }
15391 "});
15392
15393 // The comment prefix is inserted at the beginning of each line
15394 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15395
15396 cx.assert_editor_state(indoc! {"
15397 fn a() {
15398 // «b();
15399 // c();
15400 // ˇ» d();
15401 }
15402 "});
15403
15404 // If a selection ends at the beginning of a line, that line is not toggled.
15405 cx.set_selections_state(indoc! {"
15406 fn a() {
15407 // b();
15408 // «c();
15409 ˇ»// d();
15410 }
15411 "});
15412
15413 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15414
15415 cx.assert_editor_state(indoc! {"
15416 fn a() {
15417 // b();
15418 «c();
15419 ˇ»// d();
15420 }
15421 "});
15422
15423 // If a selection span a single line and is empty, the line is toggled.
15424 cx.set_state(indoc! {"
15425 fn a() {
15426 a();
15427 b();
15428 ˇ
15429 }
15430 "});
15431
15432 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15433
15434 cx.assert_editor_state(indoc! {"
15435 fn a() {
15436 a();
15437 b();
15438 //ˇ
15439 }
15440 "});
15441
15442 // If a selection span multiple lines, empty lines are not toggled.
15443 cx.set_state(indoc! {"
15444 fn a() {
15445 «a();
15446
15447 c();ˇ»
15448 }
15449 "});
15450
15451 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15452
15453 cx.assert_editor_state(indoc! {"
15454 fn a() {
15455 // «a();
15456
15457 // c();ˇ»
15458 }
15459 "});
15460
15461 // If a selection includes multiple comment prefixes, all lines are uncommented.
15462 cx.set_state(indoc! {"
15463 fn a() {
15464 // «a();
15465 /// b();
15466 //! c();ˇ»
15467 }
15468 "});
15469
15470 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15471
15472 cx.assert_editor_state(indoc! {"
15473 fn a() {
15474 «a();
15475 b();
15476 c();ˇ»
15477 }
15478 "});
15479}
15480
15481#[gpui::test]
15482async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15483 init_test(cx, |_| {});
15484
15485 let language = Arc::new(Language::new(
15486 LanguageConfig {
15487 line_comments: vec!["// ".into()],
15488 ..Default::default()
15489 },
15490 Some(tree_sitter_rust::LANGUAGE.into()),
15491 ));
15492
15493 let mut cx = EditorTestContext::new(cx).await;
15494
15495 cx.language_registry().add(language.clone());
15496 cx.update_buffer(|buffer, cx| {
15497 buffer.set_language(Some(language), cx);
15498 });
15499
15500 let toggle_comments = &ToggleComments {
15501 advance_downwards: true,
15502 ignore_indent: false,
15503 };
15504
15505 // Single cursor on one line -> advance
15506 // Cursor moves horizontally 3 characters as well on non-blank line
15507 cx.set_state(indoc!(
15508 "fn a() {
15509 ˇdog();
15510 cat();
15511 }"
15512 ));
15513 cx.update_editor(|editor, window, cx| {
15514 editor.toggle_comments(toggle_comments, window, cx);
15515 });
15516 cx.assert_editor_state(indoc!(
15517 "fn a() {
15518 // dog();
15519 catˇ();
15520 }"
15521 ));
15522
15523 // Single selection on one line -> don't advance
15524 cx.set_state(indoc!(
15525 "fn a() {
15526 «dog()ˇ»;
15527 cat();
15528 }"
15529 ));
15530 cx.update_editor(|editor, window, cx| {
15531 editor.toggle_comments(toggle_comments, window, cx);
15532 });
15533 cx.assert_editor_state(indoc!(
15534 "fn a() {
15535 // «dog()ˇ»;
15536 cat();
15537 }"
15538 ));
15539
15540 // Multiple cursors on one line -> advance
15541 cx.set_state(indoc!(
15542 "fn a() {
15543 ˇdˇog();
15544 cat();
15545 }"
15546 ));
15547 cx.update_editor(|editor, window, cx| {
15548 editor.toggle_comments(toggle_comments, window, cx);
15549 });
15550 cx.assert_editor_state(indoc!(
15551 "fn a() {
15552 // dog();
15553 catˇ(ˇ);
15554 }"
15555 ));
15556
15557 // Multiple cursors on one line, with selection -> don't advance
15558 cx.set_state(indoc!(
15559 "fn a() {
15560 ˇdˇog«()ˇ»;
15561 cat();
15562 }"
15563 ));
15564 cx.update_editor(|editor, window, cx| {
15565 editor.toggle_comments(toggle_comments, window, cx);
15566 });
15567 cx.assert_editor_state(indoc!(
15568 "fn a() {
15569 // ˇdˇog«()ˇ»;
15570 cat();
15571 }"
15572 ));
15573
15574 // Single cursor on one line -> advance
15575 // Cursor moves to column 0 on blank line
15576 cx.set_state(indoc!(
15577 "fn a() {
15578 ˇdog();
15579
15580 cat();
15581 }"
15582 ));
15583 cx.update_editor(|editor, window, cx| {
15584 editor.toggle_comments(toggle_comments, window, cx);
15585 });
15586 cx.assert_editor_state(indoc!(
15587 "fn a() {
15588 // dog();
15589 ˇ
15590 cat();
15591 }"
15592 ));
15593
15594 // Single cursor on one line -> advance
15595 // Cursor starts and ends at column 0
15596 cx.set_state(indoc!(
15597 "fn a() {
15598 ˇ dog();
15599 cat();
15600 }"
15601 ));
15602 cx.update_editor(|editor, window, cx| {
15603 editor.toggle_comments(toggle_comments, window, cx);
15604 });
15605 cx.assert_editor_state(indoc!(
15606 "fn a() {
15607 // dog();
15608 ˇ cat();
15609 }"
15610 ));
15611}
15612
15613#[gpui::test]
15614async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15615 init_test(cx, |_| {});
15616
15617 let mut cx = EditorTestContext::new(cx).await;
15618
15619 let html_language = Arc::new(
15620 Language::new(
15621 LanguageConfig {
15622 name: "HTML".into(),
15623 block_comment: Some(BlockCommentConfig {
15624 start: "<!-- ".into(),
15625 prefix: "".into(),
15626 end: " -->".into(),
15627 tab_size: 0,
15628 }),
15629 ..Default::default()
15630 },
15631 Some(tree_sitter_html::LANGUAGE.into()),
15632 )
15633 .with_injection_query(
15634 r#"
15635 (script_element
15636 (raw_text) @injection.content
15637 (#set! injection.language "javascript"))
15638 "#,
15639 )
15640 .unwrap(),
15641 );
15642
15643 let javascript_language = Arc::new(Language::new(
15644 LanguageConfig {
15645 name: "JavaScript".into(),
15646 line_comments: vec!["// ".into()],
15647 ..Default::default()
15648 },
15649 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15650 ));
15651
15652 cx.language_registry().add(html_language.clone());
15653 cx.language_registry().add(javascript_language);
15654 cx.update_buffer(|buffer, cx| {
15655 buffer.set_language(Some(html_language), cx);
15656 });
15657
15658 // Toggle comments for empty selections
15659 cx.set_state(
15660 &r#"
15661 <p>A</p>ˇ
15662 <p>B</p>ˇ
15663 <p>C</p>ˇ
15664 "#
15665 .unindent(),
15666 );
15667 cx.update_editor(|editor, window, cx| {
15668 editor.toggle_comments(&ToggleComments::default(), window, cx)
15669 });
15670 cx.assert_editor_state(
15671 &r#"
15672 <!-- <p>A</p>ˇ -->
15673 <!-- <p>B</p>ˇ -->
15674 <!-- <p>C</p>ˇ -->
15675 "#
15676 .unindent(),
15677 );
15678 cx.update_editor(|editor, window, cx| {
15679 editor.toggle_comments(&ToggleComments::default(), window, cx)
15680 });
15681 cx.assert_editor_state(
15682 &r#"
15683 <p>A</p>ˇ
15684 <p>B</p>ˇ
15685 <p>C</p>ˇ
15686 "#
15687 .unindent(),
15688 );
15689
15690 // Toggle comments for mixture of empty and non-empty selections, where
15691 // multiple selections occupy a given line.
15692 cx.set_state(
15693 &r#"
15694 <p>A«</p>
15695 <p>ˇ»B</p>ˇ
15696 <p>C«</p>
15697 <p>ˇ»D</p>ˇ
15698 "#
15699 .unindent(),
15700 );
15701
15702 cx.update_editor(|editor, window, cx| {
15703 editor.toggle_comments(&ToggleComments::default(), window, cx)
15704 });
15705 cx.assert_editor_state(
15706 &r#"
15707 <!-- <p>A«</p>
15708 <p>ˇ»B</p>ˇ -->
15709 <!-- <p>C«</p>
15710 <p>ˇ»D</p>ˇ -->
15711 "#
15712 .unindent(),
15713 );
15714 cx.update_editor(|editor, window, cx| {
15715 editor.toggle_comments(&ToggleComments::default(), window, cx)
15716 });
15717 cx.assert_editor_state(
15718 &r#"
15719 <p>A«</p>
15720 <p>ˇ»B</p>ˇ
15721 <p>C«</p>
15722 <p>ˇ»D</p>ˇ
15723 "#
15724 .unindent(),
15725 );
15726
15727 // Toggle comments when different languages are active for different
15728 // selections.
15729 cx.set_state(
15730 &r#"
15731 ˇ<script>
15732 ˇvar x = new Y();
15733 ˇ</script>
15734 "#
15735 .unindent(),
15736 );
15737 cx.executor().run_until_parked();
15738 cx.update_editor(|editor, window, cx| {
15739 editor.toggle_comments(&ToggleComments::default(), window, cx)
15740 });
15741 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15742 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15743 cx.assert_editor_state(
15744 &r#"
15745 <!-- ˇ<script> -->
15746 // ˇvar x = new Y();
15747 <!-- ˇ</script> -->
15748 "#
15749 .unindent(),
15750 );
15751}
15752
15753#[gpui::test]
15754fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15755 init_test(cx, |_| {});
15756
15757 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15758 let multibuffer = cx.new(|cx| {
15759 let mut multibuffer = MultiBuffer::new(ReadWrite);
15760 multibuffer.push_excerpts(
15761 buffer.clone(),
15762 [
15763 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15764 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15765 ],
15766 cx,
15767 );
15768 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15769 multibuffer
15770 });
15771
15772 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15773 editor.update_in(cx, |editor, window, cx| {
15774 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15775 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15776 s.select_ranges([
15777 Point::new(0, 0)..Point::new(0, 0),
15778 Point::new(1, 0)..Point::new(1, 0),
15779 ])
15780 });
15781
15782 editor.handle_input("X", window, cx);
15783 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15784 assert_eq!(
15785 editor.selections.ranges(cx),
15786 [
15787 Point::new(0, 1)..Point::new(0, 1),
15788 Point::new(1, 1)..Point::new(1, 1),
15789 ]
15790 );
15791
15792 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15793 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15794 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15795 });
15796 editor.backspace(&Default::default(), window, cx);
15797 assert_eq!(editor.text(cx), "Xa\nbbb");
15798 assert_eq!(
15799 editor.selections.ranges(cx),
15800 [Point::new(1, 0)..Point::new(1, 0)]
15801 );
15802
15803 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15804 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15805 });
15806 editor.backspace(&Default::default(), window, cx);
15807 assert_eq!(editor.text(cx), "X\nbb");
15808 assert_eq!(
15809 editor.selections.ranges(cx),
15810 [Point::new(0, 1)..Point::new(0, 1)]
15811 );
15812 });
15813}
15814
15815#[gpui::test]
15816fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15817 init_test(cx, |_| {});
15818
15819 let markers = vec![('[', ']').into(), ('(', ')').into()];
15820 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15821 indoc! {"
15822 [aaaa
15823 (bbbb]
15824 cccc)",
15825 },
15826 markers.clone(),
15827 );
15828 let excerpt_ranges = markers.into_iter().map(|marker| {
15829 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15830 ExcerptRange::new(context)
15831 });
15832 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15833 let multibuffer = cx.new(|cx| {
15834 let mut multibuffer = MultiBuffer::new(ReadWrite);
15835 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15836 multibuffer
15837 });
15838
15839 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15840 editor.update_in(cx, |editor, window, cx| {
15841 let (expected_text, selection_ranges) = marked_text_ranges(
15842 indoc! {"
15843 aaaa
15844 bˇbbb
15845 bˇbbˇb
15846 cccc"
15847 },
15848 true,
15849 );
15850 assert_eq!(editor.text(cx), expected_text);
15851 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15852 s.select_ranges(selection_ranges)
15853 });
15854
15855 editor.handle_input("X", window, cx);
15856
15857 let (expected_text, expected_selections) = marked_text_ranges(
15858 indoc! {"
15859 aaaa
15860 bXˇbbXb
15861 bXˇbbXˇb
15862 cccc"
15863 },
15864 false,
15865 );
15866 assert_eq!(editor.text(cx), expected_text);
15867 assert_eq!(editor.selections.ranges(cx), expected_selections);
15868
15869 editor.newline(&Newline, window, cx);
15870 let (expected_text, expected_selections) = marked_text_ranges(
15871 indoc! {"
15872 aaaa
15873 bX
15874 ˇbbX
15875 b
15876 bX
15877 ˇbbX
15878 ˇb
15879 cccc"
15880 },
15881 false,
15882 );
15883 assert_eq!(editor.text(cx), expected_text);
15884 assert_eq!(editor.selections.ranges(cx), expected_selections);
15885 });
15886}
15887
15888#[gpui::test]
15889fn test_refresh_selections(cx: &mut TestAppContext) {
15890 init_test(cx, |_| {});
15891
15892 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15893 let mut excerpt1_id = None;
15894 let multibuffer = cx.new(|cx| {
15895 let mut multibuffer = MultiBuffer::new(ReadWrite);
15896 excerpt1_id = multibuffer
15897 .push_excerpts(
15898 buffer.clone(),
15899 [
15900 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15901 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15902 ],
15903 cx,
15904 )
15905 .into_iter()
15906 .next();
15907 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15908 multibuffer
15909 });
15910
15911 let editor = cx.add_window(|window, cx| {
15912 let mut editor = build_editor(multibuffer.clone(), window, cx);
15913 let snapshot = editor.snapshot(window, cx);
15914 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15915 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15916 });
15917 editor.begin_selection(
15918 Point::new(2, 1).to_display_point(&snapshot),
15919 true,
15920 1,
15921 window,
15922 cx,
15923 );
15924 assert_eq!(
15925 editor.selections.ranges(cx),
15926 [
15927 Point::new(1, 3)..Point::new(1, 3),
15928 Point::new(2, 1)..Point::new(2, 1),
15929 ]
15930 );
15931 editor
15932 });
15933
15934 // Refreshing selections is a no-op when excerpts haven't changed.
15935 _ = editor.update(cx, |editor, window, cx| {
15936 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15937 assert_eq!(
15938 editor.selections.ranges(cx),
15939 [
15940 Point::new(1, 3)..Point::new(1, 3),
15941 Point::new(2, 1)..Point::new(2, 1),
15942 ]
15943 );
15944 });
15945
15946 multibuffer.update(cx, |multibuffer, cx| {
15947 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15948 });
15949 _ = editor.update(cx, |editor, window, cx| {
15950 // Removing an excerpt causes the first selection to become degenerate.
15951 assert_eq!(
15952 editor.selections.ranges(cx),
15953 [
15954 Point::new(0, 0)..Point::new(0, 0),
15955 Point::new(0, 1)..Point::new(0, 1)
15956 ]
15957 );
15958
15959 // Refreshing selections will relocate the first selection to the original buffer
15960 // location.
15961 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15962 assert_eq!(
15963 editor.selections.ranges(cx),
15964 [
15965 Point::new(0, 1)..Point::new(0, 1),
15966 Point::new(0, 3)..Point::new(0, 3)
15967 ]
15968 );
15969 assert!(editor.selections.pending_anchor().is_some());
15970 });
15971}
15972
15973#[gpui::test]
15974fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15975 init_test(cx, |_| {});
15976
15977 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15978 let mut excerpt1_id = None;
15979 let multibuffer = cx.new(|cx| {
15980 let mut multibuffer = MultiBuffer::new(ReadWrite);
15981 excerpt1_id = multibuffer
15982 .push_excerpts(
15983 buffer.clone(),
15984 [
15985 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15986 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15987 ],
15988 cx,
15989 )
15990 .into_iter()
15991 .next();
15992 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15993 multibuffer
15994 });
15995
15996 let editor = cx.add_window(|window, cx| {
15997 let mut editor = build_editor(multibuffer.clone(), window, cx);
15998 let snapshot = editor.snapshot(window, cx);
15999 editor.begin_selection(
16000 Point::new(1, 3).to_display_point(&snapshot),
16001 false,
16002 1,
16003 window,
16004 cx,
16005 );
16006 assert_eq!(
16007 editor.selections.ranges(cx),
16008 [Point::new(1, 3)..Point::new(1, 3)]
16009 );
16010 editor
16011 });
16012
16013 multibuffer.update(cx, |multibuffer, cx| {
16014 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16015 });
16016 _ = editor.update(cx, |editor, window, cx| {
16017 assert_eq!(
16018 editor.selections.ranges(cx),
16019 [Point::new(0, 0)..Point::new(0, 0)]
16020 );
16021
16022 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16023 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16024 assert_eq!(
16025 editor.selections.ranges(cx),
16026 [Point::new(0, 3)..Point::new(0, 3)]
16027 );
16028 assert!(editor.selections.pending_anchor().is_some());
16029 });
16030}
16031
16032#[gpui::test]
16033async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16034 init_test(cx, |_| {});
16035
16036 let language = Arc::new(
16037 Language::new(
16038 LanguageConfig {
16039 brackets: BracketPairConfig {
16040 pairs: vec![
16041 BracketPair {
16042 start: "{".to_string(),
16043 end: "}".to_string(),
16044 close: true,
16045 surround: true,
16046 newline: true,
16047 },
16048 BracketPair {
16049 start: "/* ".to_string(),
16050 end: " */".to_string(),
16051 close: true,
16052 surround: true,
16053 newline: true,
16054 },
16055 ],
16056 ..Default::default()
16057 },
16058 ..Default::default()
16059 },
16060 Some(tree_sitter_rust::LANGUAGE.into()),
16061 )
16062 .with_indents_query("")
16063 .unwrap(),
16064 );
16065
16066 let text = concat!(
16067 "{ }\n", //
16068 " x\n", //
16069 " /* */\n", //
16070 "x\n", //
16071 "{{} }\n", //
16072 );
16073
16074 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16075 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16076 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16077 editor
16078 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16079 .await;
16080
16081 editor.update_in(cx, |editor, window, cx| {
16082 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16083 s.select_display_ranges([
16084 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16085 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16086 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16087 ])
16088 });
16089 editor.newline(&Newline, window, cx);
16090
16091 assert_eq!(
16092 editor.buffer().read(cx).read(cx).text(),
16093 concat!(
16094 "{ \n", // Suppress rustfmt
16095 "\n", //
16096 "}\n", //
16097 " x\n", //
16098 " /* \n", //
16099 " \n", //
16100 " */\n", //
16101 "x\n", //
16102 "{{} \n", //
16103 "}\n", //
16104 )
16105 );
16106 });
16107}
16108
16109#[gpui::test]
16110fn test_highlighted_ranges(cx: &mut TestAppContext) {
16111 init_test(cx, |_| {});
16112
16113 let editor = cx.add_window(|window, cx| {
16114 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16115 build_editor(buffer, window, cx)
16116 });
16117
16118 _ = editor.update(cx, |editor, window, cx| {
16119 struct Type1;
16120 struct Type2;
16121
16122 let buffer = editor.buffer.read(cx).snapshot(cx);
16123
16124 let anchor_range =
16125 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16126
16127 editor.highlight_background::<Type1>(
16128 &[
16129 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16130 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16131 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16132 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16133 ],
16134 |_| Hsla::red(),
16135 cx,
16136 );
16137 editor.highlight_background::<Type2>(
16138 &[
16139 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16140 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16141 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16142 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16143 ],
16144 |_| Hsla::green(),
16145 cx,
16146 );
16147
16148 let snapshot = editor.snapshot(window, cx);
16149 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16150 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16151 &snapshot,
16152 cx.theme(),
16153 );
16154 assert_eq!(
16155 highlighted_ranges,
16156 &[
16157 (
16158 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16159 Hsla::green(),
16160 ),
16161 (
16162 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16163 Hsla::red(),
16164 ),
16165 (
16166 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16167 Hsla::green(),
16168 ),
16169 (
16170 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16171 Hsla::red(),
16172 ),
16173 ]
16174 );
16175 assert_eq!(
16176 editor.sorted_background_highlights_in_range(
16177 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16178 &snapshot,
16179 cx.theme(),
16180 ),
16181 &[(
16182 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16183 Hsla::red(),
16184 )]
16185 );
16186 });
16187}
16188
16189#[gpui::test]
16190async fn test_following(cx: &mut TestAppContext) {
16191 init_test(cx, |_| {});
16192
16193 let fs = FakeFs::new(cx.executor());
16194 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16195
16196 let buffer = project.update(cx, |project, cx| {
16197 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16198 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16199 });
16200 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16201 let follower = cx.update(|cx| {
16202 cx.open_window(
16203 WindowOptions {
16204 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16205 gpui::Point::new(px(0.), px(0.)),
16206 gpui::Point::new(px(10.), px(80.)),
16207 ))),
16208 ..Default::default()
16209 },
16210 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16211 )
16212 .unwrap()
16213 });
16214
16215 let is_still_following = Rc::new(RefCell::new(true));
16216 let follower_edit_event_count = Rc::new(RefCell::new(0));
16217 let pending_update = Rc::new(RefCell::new(None));
16218 let leader_entity = leader.root(cx).unwrap();
16219 let follower_entity = follower.root(cx).unwrap();
16220 _ = follower.update(cx, {
16221 let update = pending_update.clone();
16222 let is_still_following = is_still_following.clone();
16223 let follower_edit_event_count = follower_edit_event_count.clone();
16224 |_, window, cx| {
16225 cx.subscribe_in(
16226 &leader_entity,
16227 window,
16228 move |_, leader, event, window, cx| {
16229 leader.read(cx).add_event_to_update_proto(
16230 event,
16231 &mut update.borrow_mut(),
16232 window,
16233 cx,
16234 );
16235 },
16236 )
16237 .detach();
16238
16239 cx.subscribe_in(
16240 &follower_entity,
16241 window,
16242 move |_, _, event: &EditorEvent, _window, _cx| {
16243 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16244 *is_still_following.borrow_mut() = false;
16245 }
16246
16247 if let EditorEvent::BufferEdited = event {
16248 *follower_edit_event_count.borrow_mut() += 1;
16249 }
16250 },
16251 )
16252 .detach();
16253 }
16254 });
16255
16256 // Update the selections only
16257 _ = leader.update(cx, |leader, window, cx| {
16258 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16259 s.select_ranges([1..1])
16260 });
16261 });
16262 follower
16263 .update(cx, |follower, window, cx| {
16264 follower.apply_update_proto(
16265 &project,
16266 pending_update.borrow_mut().take().unwrap(),
16267 window,
16268 cx,
16269 )
16270 })
16271 .unwrap()
16272 .await
16273 .unwrap();
16274 _ = follower.update(cx, |follower, _, cx| {
16275 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16276 });
16277 assert!(*is_still_following.borrow());
16278 assert_eq!(*follower_edit_event_count.borrow(), 0);
16279
16280 // Update the scroll position only
16281 _ = leader.update(cx, |leader, window, cx| {
16282 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16283 });
16284 follower
16285 .update(cx, |follower, window, cx| {
16286 follower.apply_update_proto(
16287 &project,
16288 pending_update.borrow_mut().take().unwrap(),
16289 window,
16290 cx,
16291 )
16292 })
16293 .unwrap()
16294 .await
16295 .unwrap();
16296 assert_eq!(
16297 follower
16298 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16299 .unwrap(),
16300 gpui::Point::new(1.5, 3.5)
16301 );
16302 assert!(*is_still_following.borrow());
16303 assert_eq!(*follower_edit_event_count.borrow(), 0);
16304
16305 // Update the selections and scroll position. The follower's scroll position is updated
16306 // via autoscroll, not via the leader's exact scroll position.
16307 _ = leader.update(cx, |leader, window, cx| {
16308 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16309 s.select_ranges([0..0])
16310 });
16311 leader.request_autoscroll(Autoscroll::newest(), cx);
16312 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16313 });
16314 follower
16315 .update(cx, |follower, window, cx| {
16316 follower.apply_update_proto(
16317 &project,
16318 pending_update.borrow_mut().take().unwrap(),
16319 window,
16320 cx,
16321 )
16322 })
16323 .unwrap()
16324 .await
16325 .unwrap();
16326 _ = follower.update(cx, |follower, _, cx| {
16327 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16328 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16329 });
16330 assert!(*is_still_following.borrow());
16331
16332 // Creating a pending selection that precedes another selection
16333 _ = leader.update(cx, |leader, window, cx| {
16334 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16335 s.select_ranges([1..1])
16336 });
16337 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16338 });
16339 follower
16340 .update(cx, |follower, window, cx| {
16341 follower.apply_update_proto(
16342 &project,
16343 pending_update.borrow_mut().take().unwrap(),
16344 window,
16345 cx,
16346 )
16347 })
16348 .unwrap()
16349 .await
16350 .unwrap();
16351 _ = follower.update(cx, |follower, _, cx| {
16352 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16353 });
16354 assert!(*is_still_following.borrow());
16355
16356 // Extend the pending selection so that it surrounds another selection
16357 _ = leader.update(cx, |leader, window, cx| {
16358 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16359 });
16360 follower
16361 .update(cx, |follower, window, cx| {
16362 follower.apply_update_proto(
16363 &project,
16364 pending_update.borrow_mut().take().unwrap(),
16365 window,
16366 cx,
16367 )
16368 })
16369 .unwrap()
16370 .await
16371 .unwrap();
16372 _ = follower.update(cx, |follower, _, cx| {
16373 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16374 });
16375
16376 // Scrolling locally breaks the follow
16377 _ = follower.update(cx, |follower, window, cx| {
16378 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16379 follower.set_scroll_anchor(
16380 ScrollAnchor {
16381 anchor: top_anchor,
16382 offset: gpui::Point::new(0.0, 0.5),
16383 },
16384 window,
16385 cx,
16386 );
16387 });
16388 assert!(!(*is_still_following.borrow()));
16389}
16390
16391#[gpui::test]
16392async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16393 init_test(cx, |_| {});
16394
16395 let fs = FakeFs::new(cx.executor());
16396 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16397 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16398 let pane = workspace
16399 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16400 .unwrap();
16401
16402 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16403
16404 let leader = pane.update_in(cx, |_, window, cx| {
16405 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16406 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16407 });
16408
16409 // Start following the editor when it has no excerpts.
16410 let mut state_message =
16411 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16412 let workspace_entity = workspace.root(cx).unwrap();
16413 let follower_1 = cx
16414 .update_window(*workspace.deref(), |_, window, cx| {
16415 Editor::from_state_proto(
16416 workspace_entity,
16417 ViewId {
16418 creator: CollaboratorId::PeerId(PeerId::default()),
16419 id: 0,
16420 },
16421 &mut state_message,
16422 window,
16423 cx,
16424 )
16425 })
16426 .unwrap()
16427 .unwrap()
16428 .await
16429 .unwrap();
16430
16431 let update_message = Rc::new(RefCell::new(None));
16432 follower_1.update_in(cx, {
16433 let update = update_message.clone();
16434 |_, window, cx| {
16435 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16436 leader.read(cx).add_event_to_update_proto(
16437 event,
16438 &mut update.borrow_mut(),
16439 window,
16440 cx,
16441 );
16442 })
16443 .detach();
16444 }
16445 });
16446
16447 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16448 (
16449 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16450 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16451 )
16452 });
16453
16454 // Insert some excerpts.
16455 leader.update(cx, |leader, cx| {
16456 leader.buffer.update(cx, |multibuffer, cx| {
16457 multibuffer.set_excerpts_for_path(
16458 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
16459 buffer_1.clone(),
16460 vec![
16461 Point::row_range(0..3),
16462 Point::row_range(1..6),
16463 Point::row_range(12..15),
16464 ],
16465 0,
16466 cx,
16467 );
16468 multibuffer.set_excerpts_for_path(
16469 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
16470 buffer_2.clone(),
16471 vec![Point::row_range(0..6), Point::row_range(8..12)],
16472 0,
16473 cx,
16474 );
16475 });
16476 });
16477
16478 // Apply the update of adding the excerpts.
16479 follower_1
16480 .update_in(cx, |follower, window, cx| {
16481 follower.apply_update_proto(
16482 &project,
16483 update_message.borrow().clone().unwrap(),
16484 window,
16485 cx,
16486 )
16487 })
16488 .await
16489 .unwrap();
16490 assert_eq!(
16491 follower_1.update(cx, |editor, cx| editor.text(cx)),
16492 leader.update(cx, |editor, cx| editor.text(cx))
16493 );
16494 update_message.borrow_mut().take();
16495
16496 // Start following separately after it already has excerpts.
16497 let mut state_message =
16498 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16499 let workspace_entity = workspace.root(cx).unwrap();
16500 let follower_2 = cx
16501 .update_window(*workspace.deref(), |_, window, cx| {
16502 Editor::from_state_proto(
16503 workspace_entity,
16504 ViewId {
16505 creator: CollaboratorId::PeerId(PeerId::default()),
16506 id: 0,
16507 },
16508 &mut state_message,
16509 window,
16510 cx,
16511 )
16512 })
16513 .unwrap()
16514 .unwrap()
16515 .await
16516 .unwrap();
16517 assert_eq!(
16518 follower_2.update(cx, |editor, cx| editor.text(cx)),
16519 leader.update(cx, |editor, cx| editor.text(cx))
16520 );
16521
16522 // Remove some excerpts.
16523 leader.update(cx, |leader, cx| {
16524 leader.buffer.update(cx, |multibuffer, cx| {
16525 let excerpt_ids = multibuffer.excerpt_ids();
16526 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16527 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16528 });
16529 });
16530
16531 // Apply the update of removing the excerpts.
16532 follower_1
16533 .update_in(cx, |follower, window, cx| {
16534 follower.apply_update_proto(
16535 &project,
16536 update_message.borrow().clone().unwrap(),
16537 window,
16538 cx,
16539 )
16540 })
16541 .await
16542 .unwrap();
16543 follower_2
16544 .update_in(cx, |follower, window, cx| {
16545 follower.apply_update_proto(
16546 &project,
16547 update_message.borrow().clone().unwrap(),
16548 window,
16549 cx,
16550 )
16551 })
16552 .await
16553 .unwrap();
16554 update_message.borrow_mut().take();
16555 assert_eq!(
16556 follower_1.update(cx, |editor, cx| editor.text(cx)),
16557 leader.update(cx, |editor, cx| editor.text(cx))
16558 );
16559}
16560
16561#[gpui::test]
16562async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16563 init_test(cx, |_| {});
16564
16565 let mut cx = EditorTestContext::new(cx).await;
16566 let lsp_store =
16567 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16568
16569 cx.set_state(indoc! {"
16570 ˇfn func(abc def: i32) -> u32 {
16571 }
16572 "});
16573
16574 cx.update(|_, cx| {
16575 lsp_store.update(cx, |lsp_store, cx| {
16576 lsp_store
16577 .update_diagnostics(
16578 LanguageServerId(0),
16579 lsp::PublishDiagnosticsParams {
16580 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16581 version: None,
16582 diagnostics: vec![
16583 lsp::Diagnostic {
16584 range: lsp::Range::new(
16585 lsp::Position::new(0, 11),
16586 lsp::Position::new(0, 12),
16587 ),
16588 severity: Some(lsp::DiagnosticSeverity::ERROR),
16589 ..Default::default()
16590 },
16591 lsp::Diagnostic {
16592 range: lsp::Range::new(
16593 lsp::Position::new(0, 12),
16594 lsp::Position::new(0, 15),
16595 ),
16596 severity: Some(lsp::DiagnosticSeverity::ERROR),
16597 ..Default::default()
16598 },
16599 lsp::Diagnostic {
16600 range: lsp::Range::new(
16601 lsp::Position::new(0, 25),
16602 lsp::Position::new(0, 28),
16603 ),
16604 severity: Some(lsp::DiagnosticSeverity::ERROR),
16605 ..Default::default()
16606 },
16607 ],
16608 },
16609 None,
16610 DiagnosticSourceKind::Pushed,
16611 &[],
16612 cx,
16613 )
16614 .unwrap()
16615 });
16616 });
16617
16618 executor.run_until_parked();
16619
16620 cx.update_editor(|editor, window, cx| {
16621 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16622 });
16623
16624 cx.assert_editor_state(indoc! {"
16625 fn func(abc def: i32) -> ˇu32 {
16626 }
16627 "});
16628
16629 cx.update_editor(|editor, window, cx| {
16630 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16631 });
16632
16633 cx.assert_editor_state(indoc! {"
16634 fn func(abc ˇdef: i32) -> u32 {
16635 }
16636 "});
16637
16638 cx.update_editor(|editor, window, cx| {
16639 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16640 });
16641
16642 cx.assert_editor_state(indoc! {"
16643 fn func(abcˇ def: i32) -> u32 {
16644 }
16645 "});
16646
16647 cx.update_editor(|editor, window, cx| {
16648 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16649 });
16650
16651 cx.assert_editor_state(indoc! {"
16652 fn func(abc def: i32) -> ˇu32 {
16653 }
16654 "});
16655}
16656
16657#[gpui::test]
16658async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16659 init_test(cx, |_| {});
16660
16661 let mut cx = EditorTestContext::new(cx).await;
16662
16663 let diff_base = r#"
16664 use some::mod;
16665
16666 const A: u32 = 42;
16667
16668 fn main() {
16669 println!("hello");
16670
16671 println!("world");
16672 }
16673 "#
16674 .unindent();
16675
16676 // Edits are modified, removed, modified, added
16677 cx.set_state(
16678 &r#"
16679 use some::modified;
16680
16681 ˇ
16682 fn main() {
16683 println!("hello there");
16684
16685 println!("around the");
16686 println!("world");
16687 }
16688 "#
16689 .unindent(),
16690 );
16691
16692 cx.set_head_text(&diff_base);
16693 executor.run_until_parked();
16694
16695 cx.update_editor(|editor, window, cx| {
16696 //Wrap around the bottom of the buffer
16697 for _ in 0..3 {
16698 editor.go_to_next_hunk(&GoToHunk, window, cx);
16699 }
16700 });
16701
16702 cx.assert_editor_state(
16703 &r#"
16704 ˇuse some::modified;
16705
16706
16707 fn main() {
16708 println!("hello there");
16709
16710 println!("around the");
16711 println!("world");
16712 }
16713 "#
16714 .unindent(),
16715 );
16716
16717 cx.update_editor(|editor, window, cx| {
16718 //Wrap around the top of the buffer
16719 for _ in 0..2 {
16720 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16721 }
16722 });
16723
16724 cx.assert_editor_state(
16725 &r#"
16726 use some::modified;
16727
16728
16729 fn main() {
16730 ˇ println!("hello there");
16731
16732 println!("around the");
16733 println!("world");
16734 }
16735 "#
16736 .unindent(),
16737 );
16738
16739 cx.update_editor(|editor, window, cx| {
16740 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16741 });
16742
16743 cx.assert_editor_state(
16744 &r#"
16745 use some::modified;
16746
16747 ˇ
16748 fn main() {
16749 println!("hello there");
16750
16751 println!("around the");
16752 println!("world");
16753 }
16754 "#
16755 .unindent(),
16756 );
16757
16758 cx.update_editor(|editor, window, cx| {
16759 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16760 });
16761
16762 cx.assert_editor_state(
16763 &r#"
16764 ˇuse some::modified;
16765
16766
16767 fn main() {
16768 println!("hello there");
16769
16770 println!("around the");
16771 println!("world");
16772 }
16773 "#
16774 .unindent(),
16775 );
16776
16777 cx.update_editor(|editor, window, cx| {
16778 for _ in 0..2 {
16779 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16780 }
16781 });
16782
16783 cx.assert_editor_state(
16784 &r#"
16785 use some::modified;
16786
16787
16788 fn main() {
16789 ˇ println!("hello there");
16790
16791 println!("around the");
16792 println!("world");
16793 }
16794 "#
16795 .unindent(),
16796 );
16797
16798 cx.update_editor(|editor, window, cx| {
16799 editor.fold(&Fold, window, cx);
16800 });
16801
16802 cx.update_editor(|editor, window, cx| {
16803 editor.go_to_next_hunk(&GoToHunk, window, cx);
16804 });
16805
16806 cx.assert_editor_state(
16807 &r#"
16808 ˇuse some::modified;
16809
16810
16811 fn main() {
16812 println!("hello there");
16813
16814 println!("around the");
16815 println!("world");
16816 }
16817 "#
16818 .unindent(),
16819 );
16820}
16821
16822#[test]
16823fn test_split_words() {
16824 fn split(text: &str) -> Vec<&str> {
16825 split_words(text).collect()
16826 }
16827
16828 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16829 assert_eq!(split("hello_world"), &["hello_", "world"]);
16830 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16831 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16832 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16833 assert_eq!(split("helloworld"), &["helloworld"]);
16834
16835 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16836}
16837
16838#[gpui::test]
16839async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16840 init_test(cx, |_| {});
16841
16842 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16843 let mut assert = |before, after| {
16844 let _state_context = cx.set_state(before);
16845 cx.run_until_parked();
16846 cx.update_editor(|editor, window, cx| {
16847 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16848 });
16849 cx.run_until_parked();
16850 cx.assert_editor_state(after);
16851 };
16852
16853 // Outside bracket jumps to outside of matching bracket
16854 assert("console.logˇ(var);", "console.log(var)ˇ;");
16855 assert("console.log(var)ˇ;", "console.logˇ(var);");
16856
16857 // Inside bracket jumps to inside of matching bracket
16858 assert("console.log(ˇvar);", "console.log(varˇ);");
16859 assert("console.log(varˇ);", "console.log(ˇvar);");
16860
16861 // When outside a bracket and inside, favor jumping to the inside bracket
16862 assert(
16863 "console.log('foo', [1, 2, 3]ˇ);",
16864 "console.log(ˇ'foo', [1, 2, 3]);",
16865 );
16866 assert(
16867 "console.log(ˇ'foo', [1, 2, 3]);",
16868 "console.log('foo', [1, 2, 3]ˇ);",
16869 );
16870
16871 // Bias forward if two options are equally likely
16872 assert(
16873 "let result = curried_fun()ˇ();",
16874 "let result = curried_fun()()ˇ;",
16875 );
16876
16877 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16878 assert(
16879 indoc! {"
16880 function test() {
16881 console.log('test')ˇ
16882 }"},
16883 indoc! {"
16884 function test() {
16885 console.logˇ('test')
16886 }"},
16887 );
16888}
16889
16890#[gpui::test]
16891async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16892 init_test(cx, |_| {});
16893
16894 let fs = FakeFs::new(cx.executor());
16895 fs.insert_tree(
16896 path!("/a"),
16897 json!({
16898 "main.rs": "fn main() { let a = 5; }",
16899 "other.rs": "// Test file",
16900 }),
16901 )
16902 .await;
16903 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16904
16905 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16906 language_registry.add(Arc::new(Language::new(
16907 LanguageConfig {
16908 name: "Rust".into(),
16909 matcher: LanguageMatcher {
16910 path_suffixes: vec!["rs".to_string()],
16911 ..Default::default()
16912 },
16913 brackets: BracketPairConfig {
16914 pairs: vec![BracketPair {
16915 start: "{".to_string(),
16916 end: "}".to_string(),
16917 close: true,
16918 surround: true,
16919 newline: true,
16920 }],
16921 disabled_scopes_by_bracket_ix: Vec::new(),
16922 },
16923 ..Default::default()
16924 },
16925 Some(tree_sitter_rust::LANGUAGE.into()),
16926 )));
16927 let mut fake_servers = language_registry.register_fake_lsp(
16928 "Rust",
16929 FakeLspAdapter {
16930 capabilities: lsp::ServerCapabilities {
16931 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16932 first_trigger_character: "{".to_string(),
16933 more_trigger_character: None,
16934 }),
16935 ..Default::default()
16936 },
16937 ..Default::default()
16938 },
16939 );
16940
16941 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16942
16943 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16944
16945 let worktree_id = workspace
16946 .update(cx, |workspace, _, cx| {
16947 workspace.project().update(cx, |project, cx| {
16948 project.worktrees(cx).next().unwrap().read(cx).id()
16949 })
16950 })
16951 .unwrap();
16952
16953 let buffer = project
16954 .update(cx, |project, cx| {
16955 project.open_local_buffer(path!("/a/main.rs"), cx)
16956 })
16957 .await
16958 .unwrap();
16959 let editor_handle = workspace
16960 .update(cx, |workspace, window, cx| {
16961 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16962 })
16963 .unwrap()
16964 .await
16965 .unwrap()
16966 .downcast::<Editor>()
16967 .unwrap();
16968
16969 cx.executor().start_waiting();
16970 let fake_server = fake_servers.next().await.unwrap();
16971
16972 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16973 |params, _| async move {
16974 assert_eq!(
16975 params.text_document_position.text_document.uri,
16976 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16977 );
16978 assert_eq!(
16979 params.text_document_position.position,
16980 lsp::Position::new(0, 21),
16981 );
16982
16983 Ok(Some(vec![lsp::TextEdit {
16984 new_text: "]".to_string(),
16985 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16986 }]))
16987 },
16988 );
16989
16990 editor_handle.update_in(cx, |editor, window, cx| {
16991 window.focus(&editor.focus_handle(cx));
16992 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16993 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16994 });
16995 editor.handle_input("{", window, cx);
16996 });
16997
16998 cx.executor().run_until_parked();
16999
17000 buffer.update(cx, |buffer, _| {
17001 assert_eq!(
17002 buffer.text(),
17003 "fn main() { let a = {5}; }",
17004 "No extra braces from on type formatting should appear in the buffer"
17005 )
17006 });
17007}
17008
17009#[gpui::test(iterations = 20, seeds(31))]
17010async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17011 init_test(cx, |_| {});
17012
17013 let mut cx = EditorLspTestContext::new_rust(
17014 lsp::ServerCapabilities {
17015 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17016 first_trigger_character: ".".to_string(),
17017 more_trigger_character: None,
17018 }),
17019 ..Default::default()
17020 },
17021 cx,
17022 )
17023 .await;
17024
17025 cx.update_buffer(|buffer, _| {
17026 // This causes autoindent to be async.
17027 buffer.set_sync_parse_timeout(Duration::ZERO)
17028 });
17029
17030 cx.set_state("fn c() {\n d()ˇ\n}\n");
17031 cx.simulate_keystroke("\n");
17032 cx.run_until_parked();
17033
17034 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17035 let mut request =
17036 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17037 let buffer_cloned = buffer_cloned.clone();
17038 async move {
17039 buffer_cloned.update(&mut cx, |buffer, _| {
17040 assert_eq!(
17041 buffer.text(),
17042 "fn c() {\n d()\n .\n}\n",
17043 "OnTypeFormatting should triggered after autoindent applied"
17044 )
17045 })?;
17046
17047 Ok(Some(vec![]))
17048 }
17049 });
17050
17051 cx.simulate_keystroke(".");
17052 cx.run_until_parked();
17053
17054 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17055 assert!(request.next().await.is_some());
17056 request.close();
17057 assert!(request.next().await.is_none());
17058}
17059
17060#[gpui::test]
17061async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17062 init_test(cx, |_| {});
17063
17064 let fs = FakeFs::new(cx.executor());
17065 fs.insert_tree(
17066 path!("/a"),
17067 json!({
17068 "main.rs": "fn main() { let a = 5; }",
17069 "other.rs": "// Test file",
17070 }),
17071 )
17072 .await;
17073
17074 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17075
17076 let server_restarts = Arc::new(AtomicUsize::new(0));
17077 let closure_restarts = Arc::clone(&server_restarts);
17078 let language_server_name = "test language server";
17079 let language_name: LanguageName = "Rust".into();
17080
17081 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17082 language_registry.add(Arc::new(Language::new(
17083 LanguageConfig {
17084 name: language_name.clone(),
17085 matcher: LanguageMatcher {
17086 path_suffixes: vec!["rs".to_string()],
17087 ..Default::default()
17088 },
17089 ..Default::default()
17090 },
17091 Some(tree_sitter_rust::LANGUAGE.into()),
17092 )));
17093 let mut fake_servers = language_registry.register_fake_lsp(
17094 "Rust",
17095 FakeLspAdapter {
17096 name: language_server_name,
17097 initialization_options: Some(json!({
17098 "testOptionValue": true
17099 })),
17100 initializer: Some(Box::new(move |fake_server| {
17101 let task_restarts = Arc::clone(&closure_restarts);
17102 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17103 task_restarts.fetch_add(1, atomic::Ordering::Release);
17104 futures::future::ready(Ok(()))
17105 });
17106 })),
17107 ..Default::default()
17108 },
17109 );
17110
17111 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17112 let _buffer = project
17113 .update(cx, |project, cx| {
17114 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17115 })
17116 .await
17117 .unwrap();
17118 let _fake_server = fake_servers.next().await.unwrap();
17119 update_test_language_settings(cx, |language_settings| {
17120 language_settings.languages.0.insert(
17121 language_name.clone().0,
17122 LanguageSettingsContent {
17123 tab_size: NonZeroU32::new(8),
17124 ..Default::default()
17125 },
17126 );
17127 });
17128 cx.executor().run_until_parked();
17129 assert_eq!(
17130 server_restarts.load(atomic::Ordering::Acquire),
17131 0,
17132 "Should not restart LSP server on an unrelated change"
17133 );
17134
17135 update_test_project_settings(cx, |project_settings| {
17136 project_settings.lsp.insert(
17137 "Some other server name".into(),
17138 LspSettings {
17139 binary: None,
17140 settings: None,
17141 initialization_options: Some(json!({
17142 "some other init value": false
17143 })),
17144 enable_lsp_tasks: false,
17145 fetch: None,
17146 },
17147 );
17148 });
17149 cx.executor().run_until_parked();
17150 assert_eq!(
17151 server_restarts.load(atomic::Ordering::Acquire),
17152 0,
17153 "Should not restart LSP server on an unrelated LSP settings change"
17154 );
17155
17156 update_test_project_settings(cx, |project_settings| {
17157 project_settings.lsp.insert(
17158 language_server_name.into(),
17159 LspSettings {
17160 binary: None,
17161 settings: None,
17162 initialization_options: Some(json!({
17163 "anotherInitValue": false
17164 })),
17165 enable_lsp_tasks: false,
17166 fetch: None,
17167 },
17168 );
17169 });
17170 cx.executor().run_until_parked();
17171 assert_eq!(
17172 server_restarts.load(atomic::Ordering::Acquire),
17173 1,
17174 "Should restart LSP server on a related LSP settings change"
17175 );
17176
17177 update_test_project_settings(cx, |project_settings| {
17178 project_settings.lsp.insert(
17179 language_server_name.into(),
17180 LspSettings {
17181 binary: None,
17182 settings: None,
17183 initialization_options: Some(json!({
17184 "anotherInitValue": false
17185 })),
17186 enable_lsp_tasks: false,
17187 fetch: None,
17188 },
17189 );
17190 });
17191 cx.executor().run_until_parked();
17192 assert_eq!(
17193 server_restarts.load(atomic::Ordering::Acquire),
17194 1,
17195 "Should not restart LSP server on a related LSP settings change that is the same"
17196 );
17197
17198 update_test_project_settings(cx, |project_settings| {
17199 project_settings.lsp.insert(
17200 language_server_name.into(),
17201 LspSettings {
17202 binary: None,
17203 settings: None,
17204 initialization_options: None,
17205 enable_lsp_tasks: false,
17206 fetch: None,
17207 },
17208 );
17209 });
17210 cx.executor().run_until_parked();
17211 assert_eq!(
17212 server_restarts.load(atomic::Ordering::Acquire),
17213 2,
17214 "Should restart LSP server on another related LSP settings change"
17215 );
17216}
17217
17218#[gpui::test]
17219async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17220 init_test(cx, |_| {});
17221
17222 let mut cx = EditorLspTestContext::new_rust(
17223 lsp::ServerCapabilities {
17224 completion_provider: Some(lsp::CompletionOptions {
17225 trigger_characters: Some(vec![".".to_string()]),
17226 resolve_provider: Some(true),
17227 ..Default::default()
17228 }),
17229 ..Default::default()
17230 },
17231 cx,
17232 )
17233 .await;
17234
17235 cx.set_state("fn main() { let a = 2ˇ; }");
17236 cx.simulate_keystroke(".");
17237 let completion_item = lsp::CompletionItem {
17238 label: "some".into(),
17239 kind: Some(lsp::CompletionItemKind::SNIPPET),
17240 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17241 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17242 kind: lsp::MarkupKind::Markdown,
17243 value: "```rust\nSome(2)\n```".to_string(),
17244 })),
17245 deprecated: Some(false),
17246 sort_text: Some("fffffff2".to_string()),
17247 filter_text: Some("some".to_string()),
17248 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17249 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17250 range: lsp::Range {
17251 start: lsp::Position {
17252 line: 0,
17253 character: 22,
17254 },
17255 end: lsp::Position {
17256 line: 0,
17257 character: 22,
17258 },
17259 },
17260 new_text: "Some(2)".to_string(),
17261 })),
17262 additional_text_edits: Some(vec![lsp::TextEdit {
17263 range: lsp::Range {
17264 start: lsp::Position {
17265 line: 0,
17266 character: 20,
17267 },
17268 end: lsp::Position {
17269 line: 0,
17270 character: 22,
17271 },
17272 },
17273 new_text: "".to_string(),
17274 }]),
17275 ..Default::default()
17276 };
17277
17278 let closure_completion_item = completion_item.clone();
17279 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17280 let task_completion_item = closure_completion_item.clone();
17281 async move {
17282 Ok(Some(lsp::CompletionResponse::Array(vec![
17283 task_completion_item,
17284 ])))
17285 }
17286 });
17287
17288 request.next().await;
17289
17290 cx.condition(|editor, _| editor.context_menu_visible())
17291 .await;
17292 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17293 editor
17294 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17295 .unwrap()
17296 });
17297 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17298
17299 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17300 let task_completion_item = completion_item.clone();
17301 async move { Ok(task_completion_item) }
17302 })
17303 .next()
17304 .await
17305 .unwrap();
17306 apply_additional_edits.await.unwrap();
17307 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17308}
17309
17310#[gpui::test]
17311async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17312 init_test(cx, |_| {});
17313
17314 let mut cx = EditorLspTestContext::new_rust(
17315 lsp::ServerCapabilities {
17316 completion_provider: Some(lsp::CompletionOptions {
17317 trigger_characters: Some(vec![".".to_string()]),
17318 resolve_provider: Some(true),
17319 ..Default::default()
17320 }),
17321 ..Default::default()
17322 },
17323 cx,
17324 )
17325 .await;
17326
17327 cx.set_state("fn main() { let a = 2ˇ; }");
17328 cx.simulate_keystroke(".");
17329
17330 let item1 = lsp::CompletionItem {
17331 label: "method id()".to_string(),
17332 filter_text: Some("id".to_string()),
17333 detail: None,
17334 documentation: None,
17335 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17336 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17337 new_text: ".id".to_string(),
17338 })),
17339 ..lsp::CompletionItem::default()
17340 };
17341
17342 let item2 = lsp::CompletionItem {
17343 label: "other".to_string(),
17344 filter_text: Some("other".to_string()),
17345 detail: None,
17346 documentation: None,
17347 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17348 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17349 new_text: ".other".to_string(),
17350 })),
17351 ..lsp::CompletionItem::default()
17352 };
17353
17354 let item1 = item1.clone();
17355 cx.set_request_handler::<lsp::request::Completion, _, _>({
17356 let item1 = item1.clone();
17357 move |_, _, _| {
17358 let item1 = item1.clone();
17359 let item2 = item2.clone();
17360 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17361 }
17362 })
17363 .next()
17364 .await;
17365
17366 cx.condition(|editor, _| editor.context_menu_visible())
17367 .await;
17368 cx.update_editor(|editor, _, _| {
17369 let context_menu = editor.context_menu.borrow_mut();
17370 let context_menu = context_menu
17371 .as_ref()
17372 .expect("Should have the context menu deployed");
17373 match context_menu {
17374 CodeContextMenu::Completions(completions_menu) => {
17375 let completions = completions_menu.completions.borrow_mut();
17376 assert_eq!(
17377 completions
17378 .iter()
17379 .map(|completion| &completion.label.text)
17380 .collect::<Vec<_>>(),
17381 vec!["method id()", "other"]
17382 )
17383 }
17384 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17385 }
17386 });
17387
17388 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17389 let item1 = item1.clone();
17390 move |_, item_to_resolve, _| {
17391 let item1 = item1.clone();
17392 async move {
17393 if item1 == item_to_resolve {
17394 Ok(lsp::CompletionItem {
17395 label: "method id()".to_string(),
17396 filter_text: Some("id".to_string()),
17397 detail: Some("Now resolved!".to_string()),
17398 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17399 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17400 range: lsp::Range::new(
17401 lsp::Position::new(0, 22),
17402 lsp::Position::new(0, 22),
17403 ),
17404 new_text: ".id".to_string(),
17405 })),
17406 ..lsp::CompletionItem::default()
17407 })
17408 } else {
17409 Ok(item_to_resolve)
17410 }
17411 }
17412 }
17413 })
17414 .next()
17415 .await
17416 .unwrap();
17417 cx.run_until_parked();
17418
17419 cx.update_editor(|editor, window, cx| {
17420 editor.context_menu_next(&Default::default(), window, cx);
17421 });
17422
17423 cx.update_editor(|editor, _, _| {
17424 let context_menu = editor.context_menu.borrow_mut();
17425 let context_menu = context_menu
17426 .as_ref()
17427 .expect("Should have the context menu deployed");
17428 match context_menu {
17429 CodeContextMenu::Completions(completions_menu) => {
17430 let completions = completions_menu.completions.borrow_mut();
17431 assert_eq!(
17432 completions
17433 .iter()
17434 .map(|completion| &completion.label.text)
17435 .collect::<Vec<_>>(),
17436 vec!["method id() Now resolved!", "other"],
17437 "Should update first completion label, but not second as the filter text did not match."
17438 );
17439 }
17440 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17441 }
17442 });
17443}
17444
17445#[gpui::test]
17446async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17447 init_test(cx, |_| {});
17448 let mut cx = EditorLspTestContext::new_rust(
17449 lsp::ServerCapabilities {
17450 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17451 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17452 completion_provider: Some(lsp::CompletionOptions {
17453 resolve_provider: Some(true),
17454 ..Default::default()
17455 }),
17456 ..Default::default()
17457 },
17458 cx,
17459 )
17460 .await;
17461 cx.set_state(indoc! {"
17462 struct TestStruct {
17463 field: i32
17464 }
17465
17466 fn mainˇ() {
17467 let unused_var = 42;
17468 let test_struct = TestStruct { field: 42 };
17469 }
17470 "});
17471 let symbol_range = cx.lsp_range(indoc! {"
17472 struct TestStruct {
17473 field: i32
17474 }
17475
17476 «fn main»() {
17477 let unused_var = 42;
17478 let test_struct = TestStruct { field: 42 };
17479 }
17480 "});
17481 let mut hover_requests =
17482 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17483 Ok(Some(lsp::Hover {
17484 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17485 kind: lsp::MarkupKind::Markdown,
17486 value: "Function documentation".to_string(),
17487 }),
17488 range: Some(symbol_range),
17489 }))
17490 });
17491
17492 // Case 1: Test that code action menu hide hover popover
17493 cx.dispatch_action(Hover);
17494 hover_requests.next().await;
17495 cx.condition(|editor, _| editor.hover_state.visible()).await;
17496 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17497 move |_, _, _| async move {
17498 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17499 lsp::CodeAction {
17500 title: "Remove unused variable".to_string(),
17501 kind: Some(CodeActionKind::QUICKFIX),
17502 edit: Some(lsp::WorkspaceEdit {
17503 changes: Some(
17504 [(
17505 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17506 vec![lsp::TextEdit {
17507 range: lsp::Range::new(
17508 lsp::Position::new(5, 4),
17509 lsp::Position::new(5, 27),
17510 ),
17511 new_text: "".to_string(),
17512 }],
17513 )]
17514 .into_iter()
17515 .collect(),
17516 ),
17517 ..Default::default()
17518 }),
17519 ..Default::default()
17520 },
17521 )]))
17522 },
17523 );
17524 cx.update_editor(|editor, window, cx| {
17525 editor.toggle_code_actions(
17526 &ToggleCodeActions {
17527 deployed_from: None,
17528 quick_launch: false,
17529 },
17530 window,
17531 cx,
17532 );
17533 });
17534 code_action_requests.next().await;
17535 cx.run_until_parked();
17536 cx.condition(|editor, _| editor.context_menu_visible())
17537 .await;
17538 cx.update_editor(|editor, _, _| {
17539 assert!(
17540 !editor.hover_state.visible(),
17541 "Hover popover should be hidden when code action menu is shown"
17542 );
17543 // Hide code actions
17544 editor.context_menu.take();
17545 });
17546
17547 // Case 2: Test that code completions hide hover popover
17548 cx.dispatch_action(Hover);
17549 hover_requests.next().await;
17550 cx.condition(|editor, _| editor.hover_state.visible()).await;
17551 let counter = Arc::new(AtomicUsize::new(0));
17552 let mut completion_requests =
17553 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17554 let counter = counter.clone();
17555 async move {
17556 counter.fetch_add(1, atomic::Ordering::Release);
17557 Ok(Some(lsp::CompletionResponse::Array(vec![
17558 lsp::CompletionItem {
17559 label: "main".into(),
17560 kind: Some(lsp::CompletionItemKind::FUNCTION),
17561 detail: Some("() -> ()".to_string()),
17562 ..Default::default()
17563 },
17564 lsp::CompletionItem {
17565 label: "TestStruct".into(),
17566 kind: Some(lsp::CompletionItemKind::STRUCT),
17567 detail: Some("struct TestStruct".to_string()),
17568 ..Default::default()
17569 },
17570 ])))
17571 }
17572 });
17573 cx.update_editor(|editor, window, cx| {
17574 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17575 });
17576 completion_requests.next().await;
17577 cx.condition(|editor, _| editor.context_menu_visible())
17578 .await;
17579 cx.update_editor(|editor, _, _| {
17580 assert!(
17581 !editor.hover_state.visible(),
17582 "Hover popover should be hidden when completion menu is shown"
17583 );
17584 });
17585}
17586
17587#[gpui::test]
17588async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17589 init_test(cx, |_| {});
17590
17591 let mut cx = EditorLspTestContext::new_rust(
17592 lsp::ServerCapabilities {
17593 completion_provider: Some(lsp::CompletionOptions {
17594 trigger_characters: Some(vec![".".to_string()]),
17595 resolve_provider: Some(true),
17596 ..Default::default()
17597 }),
17598 ..Default::default()
17599 },
17600 cx,
17601 )
17602 .await;
17603
17604 cx.set_state("fn main() { let a = 2ˇ; }");
17605 cx.simulate_keystroke(".");
17606
17607 let unresolved_item_1 = lsp::CompletionItem {
17608 label: "id".to_string(),
17609 filter_text: Some("id".to_string()),
17610 detail: None,
17611 documentation: None,
17612 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17613 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17614 new_text: ".id".to_string(),
17615 })),
17616 ..lsp::CompletionItem::default()
17617 };
17618 let resolved_item_1 = lsp::CompletionItem {
17619 additional_text_edits: Some(vec![lsp::TextEdit {
17620 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17621 new_text: "!!".to_string(),
17622 }]),
17623 ..unresolved_item_1.clone()
17624 };
17625 let unresolved_item_2 = lsp::CompletionItem {
17626 label: "other".to_string(),
17627 filter_text: Some("other".to_string()),
17628 detail: None,
17629 documentation: None,
17630 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17631 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17632 new_text: ".other".to_string(),
17633 })),
17634 ..lsp::CompletionItem::default()
17635 };
17636 let resolved_item_2 = lsp::CompletionItem {
17637 additional_text_edits: Some(vec![lsp::TextEdit {
17638 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17639 new_text: "??".to_string(),
17640 }]),
17641 ..unresolved_item_2.clone()
17642 };
17643
17644 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17645 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17646 cx.lsp
17647 .server
17648 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17649 let unresolved_item_1 = unresolved_item_1.clone();
17650 let resolved_item_1 = resolved_item_1.clone();
17651 let unresolved_item_2 = unresolved_item_2.clone();
17652 let resolved_item_2 = resolved_item_2.clone();
17653 let resolve_requests_1 = resolve_requests_1.clone();
17654 let resolve_requests_2 = resolve_requests_2.clone();
17655 move |unresolved_request, _| {
17656 let unresolved_item_1 = unresolved_item_1.clone();
17657 let resolved_item_1 = resolved_item_1.clone();
17658 let unresolved_item_2 = unresolved_item_2.clone();
17659 let resolved_item_2 = resolved_item_2.clone();
17660 let resolve_requests_1 = resolve_requests_1.clone();
17661 let resolve_requests_2 = resolve_requests_2.clone();
17662 async move {
17663 if unresolved_request == unresolved_item_1 {
17664 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17665 Ok(resolved_item_1.clone())
17666 } else if unresolved_request == unresolved_item_2 {
17667 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17668 Ok(resolved_item_2.clone())
17669 } else {
17670 panic!("Unexpected completion item {unresolved_request:?}")
17671 }
17672 }
17673 }
17674 })
17675 .detach();
17676
17677 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17678 let unresolved_item_1 = unresolved_item_1.clone();
17679 let unresolved_item_2 = unresolved_item_2.clone();
17680 async move {
17681 Ok(Some(lsp::CompletionResponse::Array(vec![
17682 unresolved_item_1,
17683 unresolved_item_2,
17684 ])))
17685 }
17686 })
17687 .next()
17688 .await;
17689
17690 cx.condition(|editor, _| editor.context_menu_visible())
17691 .await;
17692 cx.update_editor(|editor, _, _| {
17693 let context_menu = editor.context_menu.borrow_mut();
17694 let context_menu = context_menu
17695 .as_ref()
17696 .expect("Should have the context menu deployed");
17697 match context_menu {
17698 CodeContextMenu::Completions(completions_menu) => {
17699 let completions = completions_menu.completions.borrow_mut();
17700 assert_eq!(
17701 completions
17702 .iter()
17703 .map(|completion| &completion.label.text)
17704 .collect::<Vec<_>>(),
17705 vec!["id", "other"]
17706 )
17707 }
17708 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17709 }
17710 });
17711 cx.run_until_parked();
17712
17713 cx.update_editor(|editor, window, cx| {
17714 editor.context_menu_next(&ContextMenuNext, window, cx);
17715 });
17716 cx.run_until_parked();
17717 cx.update_editor(|editor, window, cx| {
17718 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17719 });
17720 cx.run_until_parked();
17721 cx.update_editor(|editor, window, cx| {
17722 editor.context_menu_next(&ContextMenuNext, window, cx);
17723 });
17724 cx.run_until_parked();
17725 cx.update_editor(|editor, window, cx| {
17726 editor
17727 .compose_completion(&ComposeCompletion::default(), window, cx)
17728 .expect("No task returned")
17729 })
17730 .await
17731 .expect("Completion failed");
17732 cx.run_until_parked();
17733
17734 cx.update_editor(|editor, _, cx| {
17735 assert_eq!(
17736 resolve_requests_1.load(atomic::Ordering::Acquire),
17737 1,
17738 "Should always resolve once despite multiple selections"
17739 );
17740 assert_eq!(
17741 resolve_requests_2.load(atomic::Ordering::Acquire),
17742 1,
17743 "Should always resolve once after multiple selections and applying the completion"
17744 );
17745 assert_eq!(
17746 editor.text(cx),
17747 "fn main() { let a = ??.other; }",
17748 "Should use resolved data when applying the completion"
17749 );
17750 });
17751}
17752
17753#[gpui::test]
17754async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17755 init_test(cx, |_| {});
17756
17757 let item_0 = lsp::CompletionItem {
17758 label: "abs".into(),
17759 insert_text: Some("abs".into()),
17760 data: Some(json!({ "very": "special"})),
17761 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17762 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17763 lsp::InsertReplaceEdit {
17764 new_text: "abs".to_string(),
17765 insert: lsp::Range::default(),
17766 replace: lsp::Range::default(),
17767 },
17768 )),
17769 ..lsp::CompletionItem::default()
17770 };
17771 let items = iter::once(item_0.clone())
17772 .chain((11..51).map(|i| lsp::CompletionItem {
17773 label: format!("item_{}", i),
17774 insert_text: Some(format!("item_{}", i)),
17775 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17776 ..lsp::CompletionItem::default()
17777 }))
17778 .collect::<Vec<_>>();
17779
17780 let default_commit_characters = vec!["?".to_string()];
17781 let default_data = json!({ "default": "data"});
17782 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17783 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17784 let default_edit_range = lsp::Range {
17785 start: lsp::Position {
17786 line: 0,
17787 character: 5,
17788 },
17789 end: lsp::Position {
17790 line: 0,
17791 character: 5,
17792 },
17793 };
17794
17795 let mut cx = EditorLspTestContext::new_rust(
17796 lsp::ServerCapabilities {
17797 completion_provider: Some(lsp::CompletionOptions {
17798 trigger_characters: Some(vec![".".to_string()]),
17799 resolve_provider: Some(true),
17800 ..Default::default()
17801 }),
17802 ..Default::default()
17803 },
17804 cx,
17805 )
17806 .await;
17807
17808 cx.set_state("fn main() { let a = 2ˇ; }");
17809 cx.simulate_keystroke(".");
17810
17811 let completion_data = default_data.clone();
17812 let completion_characters = default_commit_characters.clone();
17813 let completion_items = items.clone();
17814 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17815 let default_data = completion_data.clone();
17816 let default_commit_characters = completion_characters.clone();
17817 let items = completion_items.clone();
17818 async move {
17819 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17820 items,
17821 item_defaults: Some(lsp::CompletionListItemDefaults {
17822 data: Some(default_data.clone()),
17823 commit_characters: Some(default_commit_characters.clone()),
17824 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17825 default_edit_range,
17826 )),
17827 insert_text_format: Some(default_insert_text_format),
17828 insert_text_mode: Some(default_insert_text_mode),
17829 }),
17830 ..lsp::CompletionList::default()
17831 })))
17832 }
17833 })
17834 .next()
17835 .await;
17836
17837 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17838 cx.lsp
17839 .server
17840 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17841 let closure_resolved_items = resolved_items.clone();
17842 move |item_to_resolve, _| {
17843 let closure_resolved_items = closure_resolved_items.clone();
17844 async move {
17845 closure_resolved_items.lock().push(item_to_resolve.clone());
17846 Ok(item_to_resolve)
17847 }
17848 }
17849 })
17850 .detach();
17851
17852 cx.condition(|editor, _| editor.context_menu_visible())
17853 .await;
17854 cx.run_until_parked();
17855 cx.update_editor(|editor, _, _| {
17856 let menu = editor.context_menu.borrow_mut();
17857 match menu.as_ref().expect("should have the completions menu") {
17858 CodeContextMenu::Completions(completions_menu) => {
17859 assert_eq!(
17860 completions_menu
17861 .entries
17862 .borrow()
17863 .iter()
17864 .map(|mat| mat.string.clone())
17865 .collect::<Vec<String>>(),
17866 items
17867 .iter()
17868 .map(|completion| completion.label.clone())
17869 .collect::<Vec<String>>()
17870 );
17871 }
17872 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17873 }
17874 });
17875 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17876 // with 4 from the end.
17877 assert_eq!(
17878 *resolved_items.lock(),
17879 [&items[0..16], &items[items.len() - 4..items.len()]]
17880 .concat()
17881 .iter()
17882 .cloned()
17883 .map(|mut item| {
17884 if item.data.is_none() {
17885 item.data = Some(default_data.clone());
17886 }
17887 item
17888 })
17889 .collect::<Vec<lsp::CompletionItem>>(),
17890 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17891 );
17892 resolved_items.lock().clear();
17893
17894 cx.update_editor(|editor, window, cx| {
17895 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17896 });
17897 cx.run_until_parked();
17898 // Completions that have already been resolved are skipped.
17899 assert_eq!(
17900 *resolved_items.lock(),
17901 items[items.len() - 17..items.len() - 4]
17902 .iter()
17903 .cloned()
17904 .map(|mut item| {
17905 if item.data.is_none() {
17906 item.data = Some(default_data.clone());
17907 }
17908 item
17909 })
17910 .collect::<Vec<lsp::CompletionItem>>()
17911 );
17912 resolved_items.lock().clear();
17913}
17914
17915#[gpui::test]
17916async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17917 init_test(cx, |_| {});
17918
17919 let mut cx = EditorLspTestContext::new(
17920 Language::new(
17921 LanguageConfig {
17922 matcher: LanguageMatcher {
17923 path_suffixes: vec!["jsx".into()],
17924 ..Default::default()
17925 },
17926 overrides: [(
17927 "element".into(),
17928 LanguageConfigOverride {
17929 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17930 ..Default::default()
17931 },
17932 )]
17933 .into_iter()
17934 .collect(),
17935 ..Default::default()
17936 },
17937 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17938 )
17939 .with_override_query("(jsx_self_closing_element) @element")
17940 .unwrap(),
17941 lsp::ServerCapabilities {
17942 completion_provider: Some(lsp::CompletionOptions {
17943 trigger_characters: Some(vec![":".to_string()]),
17944 ..Default::default()
17945 }),
17946 ..Default::default()
17947 },
17948 cx,
17949 )
17950 .await;
17951
17952 cx.lsp
17953 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17954 Ok(Some(lsp::CompletionResponse::Array(vec![
17955 lsp::CompletionItem {
17956 label: "bg-blue".into(),
17957 ..Default::default()
17958 },
17959 lsp::CompletionItem {
17960 label: "bg-red".into(),
17961 ..Default::default()
17962 },
17963 lsp::CompletionItem {
17964 label: "bg-yellow".into(),
17965 ..Default::default()
17966 },
17967 ])))
17968 });
17969
17970 cx.set_state(r#"<p class="bgˇ" />"#);
17971
17972 // Trigger completion when typing a dash, because the dash is an extra
17973 // word character in the 'element' scope, which contains the cursor.
17974 cx.simulate_keystroke("-");
17975 cx.executor().run_until_parked();
17976 cx.update_editor(|editor, _, _| {
17977 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17978 {
17979 assert_eq!(
17980 completion_menu_entries(menu),
17981 &["bg-blue", "bg-red", "bg-yellow"]
17982 );
17983 } else {
17984 panic!("expected completion menu to be open");
17985 }
17986 });
17987
17988 cx.simulate_keystroke("l");
17989 cx.executor().run_until_parked();
17990 cx.update_editor(|editor, _, _| {
17991 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17992 {
17993 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17994 } else {
17995 panic!("expected completion menu to be open");
17996 }
17997 });
17998
17999 // When filtering completions, consider the character after the '-' to
18000 // be the start of a subword.
18001 cx.set_state(r#"<p class="yelˇ" />"#);
18002 cx.simulate_keystroke("l");
18003 cx.executor().run_until_parked();
18004 cx.update_editor(|editor, _, _| {
18005 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18006 {
18007 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18008 } else {
18009 panic!("expected completion menu to be open");
18010 }
18011 });
18012}
18013
18014fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18015 let entries = menu.entries.borrow();
18016 entries.iter().map(|mat| mat.string.clone()).collect()
18017}
18018
18019#[gpui::test]
18020async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18021 init_test(cx, |settings| {
18022 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
18023 Formatter::Prettier,
18024 )))
18025 });
18026
18027 let fs = FakeFs::new(cx.executor());
18028 fs.insert_file(path!("/file.ts"), Default::default()).await;
18029
18030 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18031 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18032
18033 language_registry.add(Arc::new(Language::new(
18034 LanguageConfig {
18035 name: "TypeScript".into(),
18036 matcher: LanguageMatcher {
18037 path_suffixes: vec!["ts".to_string()],
18038 ..Default::default()
18039 },
18040 ..Default::default()
18041 },
18042 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18043 )));
18044 update_test_language_settings(cx, |settings| {
18045 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18046 });
18047
18048 let test_plugin = "test_plugin";
18049 let _ = language_registry.register_fake_lsp(
18050 "TypeScript",
18051 FakeLspAdapter {
18052 prettier_plugins: vec![test_plugin],
18053 ..Default::default()
18054 },
18055 );
18056
18057 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18058 let buffer = project
18059 .update(cx, |project, cx| {
18060 project.open_local_buffer(path!("/file.ts"), cx)
18061 })
18062 .await
18063 .unwrap();
18064
18065 let buffer_text = "one\ntwo\nthree\n";
18066 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18067 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18068 editor.update_in(cx, |editor, window, cx| {
18069 editor.set_text(buffer_text, window, cx)
18070 });
18071
18072 editor
18073 .update_in(cx, |editor, window, cx| {
18074 editor.perform_format(
18075 project.clone(),
18076 FormatTrigger::Manual,
18077 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18078 window,
18079 cx,
18080 )
18081 })
18082 .unwrap()
18083 .await;
18084 assert_eq!(
18085 editor.update(cx, |editor, cx| editor.text(cx)),
18086 buffer_text.to_string() + prettier_format_suffix,
18087 "Test prettier formatting was not applied to the original buffer text",
18088 );
18089
18090 update_test_language_settings(cx, |settings| {
18091 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18092 });
18093 let format = editor.update_in(cx, |editor, window, cx| {
18094 editor.perform_format(
18095 project.clone(),
18096 FormatTrigger::Manual,
18097 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18098 window,
18099 cx,
18100 )
18101 });
18102 format.await.unwrap();
18103 assert_eq!(
18104 editor.update(cx, |editor, cx| editor.text(cx)),
18105 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18106 "Autoformatting (via test prettier) was not applied to the original buffer text",
18107 );
18108}
18109
18110#[gpui::test]
18111async fn test_addition_reverts(cx: &mut TestAppContext) {
18112 init_test(cx, |_| {});
18113 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18114 let base_text = indoc! {r#"
18115 struct Row;
18116 struct Row1;
18117 struct Row2;
18118
18119 struct Row4;
18120 struct Row5;
18121 struct Row6;
18122
18123 struct Row8;
18124 struct Row9;
18125 struct Row10;"#};
18126
18127 // When addition hunks are not adjacent to carets, no hunk revert is performed
18128 assert_hunk_revert(
18129 indoc! {r#"struct Row;
18130 struct Row1;
18131 struct Row1.1;
18132 struct Row1.2;
18133 struct Row2;ˇ
18134
18135 struct Row4;
18136 struct Row5;
18137 struct Row6;
18138
18139 struct Row8;
18140 ˇstruct Row9;
18141 struct Row9.1;
18142 struct Row9.2;
18143 struct Row9.3;
18144 struct Row10;"#},
18145 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18146 indoc! {r#"struct Row;
18147 struct Row1;
18148 struct Row1.1;
18149 struct Row1.2;
18150 struct Row2;ˇ
18151
18152 struct Row4;
18153 struct Row5;
18154 struct Row6;
18155
18156 struct Row8;
18157 ˇstruct Row9;
18158 struct Row9.1;
18159 struct Row9.2;
18160 struct Row9.3;
18161 struct Row10;"#},
18162 base_text,
18163 &mut cx,
18164 );
18165 // Same for selections
18166 assert_hunk_revert(
18167 indoc! {r#"struct Row;
18168 struct Row1;
18169 struct Row2;
18170 struct Row2.1;
18171 struct Row2.2;
18172 «ˇ
18173 struct Row4;
18174 struct» Row5;
18175 «struct Row6;
18176 ˇ»
18177 struct Row9.1;
18178 struct Row9.2;
18179 struct Row9.3;
18180 struct Row8;
18181 struct Row9;
18182 struct Row10;"#},
18183 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18184 indoc! {r#"struct Row;
18185 struct Row1;
18186 struct Row2;
18187 struct Row2.1;
18188 struct Row2.2;
18189 «ˇ
18190 struct Row4;
18191 struct» Row5;
18192 «struct Row6;
18193 ˇ»
18194 struct Row9.1;
18195 struct Row9.2;
18196 struct Row9.3;
18197 struct Row8;
18198 struct Row9;
18199 struct Row10;"#},
18200 base_text,
18201 &mut cx,
18202 );
18203
18204 // When carets and selections intersect the addition hunks, those are reverted.
18205 // Adjacent carets got merged.
18206 assert_hunk_revert(
18207 indoc! {r#"struct Row;
18208 ˇ// something on the top
18209 struct Row1;
18210 struct Row2;
18211 struct Roˇw3.1;
18212 struct Row2.2;
18213 struct Row2.3;ˇ
18214
18215 struct Row4;
18216 struct ˇRow5.1;
18217 struct Row5.2;
18218 struct «Rowˇ»5.3;
18219 struct Row5;
18220 struct Row6;
18221 ˇ
18222 struct Row9.1;
18223 struct «Rowˇ»9.2;
18224 struct «ˇRow»9.3;
18225 struct Row8;
18226 struct Row9;
18227 «ˇ// something on bottom»
18228 struct Row10;"#},
18229 vec![
18230 DiffHunkStatusKind::Added,
18231 DiffHunkStatusKind::Added,
18232 DiffHunkStatusKind::Added,
18233 DiffHunkStatusKind::Added,
18234 DiffHunkStatusKind::Added,
18235 ],
18236 indoc! {r#"struct Row;
18237 ˇstruct Row1;
18238 struct Row2;
18239 ˇ
18240 struct Row4;
18241 ˇstruct Row5;
18242 struct Row6;
18243 ˇ
18244 ˇstruct Row8;
18245 struct Row9;
18246 ˇstruct Row10;"#},
18247 base_text,
18248 &mut cx,
18249 );
18250}
18251
18252#[gpui::test]
18253async fn test_modification_reverts(cx: &mut TestAppContext) {
18254 init_test(cx, |_| {});
18255 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18256 let base_text = indoc! {r#"
18257 struct Row;
18258 struct Row1;
18259 struct Row2;
18260
18261 struct Row4;
18262 struct Row5;
18263 struct Row6;
18264
18265 struct Row8;
18266 struct Row9;
18267 struct Row10;"#};
18268
18269 // Modification hunks behave the same as the addition ones.
18270 assert_hunk_revert(
18271 indoc! {r#"struct Row;
18272 struct Row1;
18273 struct Row33;
18274 ˇ
18275 struct Row4;
18276 struct Row5;
18277 struct Row6;
18278 ˇ
18279 struct Row99;
18280 struct Row9;
18281 struct Row10;"#},
18282 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18283 indoc! {r#"struct Row;
18284 struct Row1;
18285 struct Row33;
18286 ˇ
18287 struct Row4;
18288 struct Row5;
18289 struct Row6;
18290 ˇ
18291 struct Row99;
18292 struct Row9;
18293 struct Row10;"#},
18294 base_text,
18295 &mut cx,
18296 );
18297 assert_hunk_revert(
18298 indoc! {r#"struct Row;
18299 struct Row1;
18300 struct Row33;
18301 «ˇ
18302 struct Row4;
18303 struct» Row5;
18304 «struct Row6;
18305 ˇ»
18306 struct Row99;
18307 struct Row9;
18308 struct Row10;"#},
18309 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18310 indoc! {r#"struct Row;
18311 struct Row1;
18312 struct Row33;
18313 «ˇ
18314 struct Row4;
18315 struct» Row5;
18316 «struct Row6;
18317 ˇ»
18318 struct Row99;
18319 struct Row9;
18320 struct Row10;"#},
18321 base_text,
18322 &mut cx,
18323 );
18324
18325 assert_hunk_revert(
18326 indoc! {r#"ˇstruct Row1.1;
18327 struct Row1;
18328 «ˇstr»uct Row22;
18329
18330 struct ˇRow44;
18331 struct Row5;
18332 struct «Rˇ»ow66;ˇ
18333
18334 «struˇ»ct Row88;
18335 struct Row9;
18336 struct Row1011;ˇ"#},
18337 vec![
18338 DiffHunkStatusKind::Modified,
18339 DiffHunkStatusKind::Modified,
18340 DiffHunkStatusKind::Modified,
18341 DiffHunkStatusKind::Modified,
18342 DiffHunkStatusKind::Modified,
18343 DiffHunkStatusKind::Modified,
18344 ],
18345 indoc! {r#"struct Row;
18346 ˇstruct Row1;
18347 struct Row2;
18348 ˇ
18349 struct Row4;
18350 ˇstruct Row5;
18351 struct Row6;
18352 ˇ
18353 struct Row8;
18354 ˇstruct Row9;
18355 struct Row10;ˇ"#},
18356 base_text,
18357 &mut cx,
18358 );
18359}
18360
18361#[gpui::test]
18362async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18363 init_test(cx, |_| {});
18364 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18365 let base_text = indoc! {r#"
18366 one
18367
18368 two
18369 three
18370 "#};
18371
18372 cx.set_head_text(base_text);
18373 cx.set_state("\nˇ\n");
18374 cx.executor().run_until_parked();
18375 cx.update_editor(|editor, _window, cx| {
18376 editor.expand_selected_diff_hunks(cx);
18377 });
18378 cx.executor().run_until_parked();
18379 cx.update_editor(|editor, window, cx| {
18380 editor.backspace(&Default::default(), window, cx);
18381 });
18382 cx.run_until_parked();
18383 cx.assert_state_with_diff(
18384 indoc! {r#"
18385
18386 - two
18387 - threeˇ
18388 +
18389 "#}
18390 .to_string(),
18391 );
18392}
18393
18394#[gpui::test]
18395async fn test_deletion_reverts(cx: &mut TestAppContext) {
18396 init_test(cx, |_| {});
18397 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18398 let base_text = indoc! {r#"struct Row;
18399struct Row1;
18400struct Row2;
18401
18402struct Row4;
18403struct Row5;
18404struct Row6;
18405
18406struct Row8;
18407struct Row9;
18408struct Row10;"#};
18409
18410 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18411 assert_hunk_revert(
18412 indoc! {r#"struct Row;
18413 struct Row2;
18414
18415 ˇstruct Row4;
18416 struct Row5;
18417 struct Row6;
18418 ˇ
18419 struct Row8;
18420 struct Row10;"#},
18421 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18422 indoc! {r#"struct Row;
18423 struct Row2;
18424
18425 ˇstruct Row4;
18426 struct Row5;
18427 struct Row6;
18428 ˇ
18429 struct Row8;
18430 struct Row10;"#},
18431 base_text,
18432 &mut cx,
18433 );
18434 assert_hunk_revert(
18435 indoc! {r#"struct Row;
18436 struct Row2;
18437
18438 «ˇstruct Row4;
18439 struct» Row5;
18440 «struct Row6;
18441 ˇ»
18442 struct Row8;
18443 struct Row10;"#},
18444 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18445 indoc! {r#"struct Row;
18446 struct Row2;
18447
18448 «ˇstruct Row4;
18449 struct» Row5;
18450 «struct Row6;
18451 ˇ»
18452 struct Row8;
18453 struct Row10;"#},
18454 base_text,
18455 &mut cx,
18456 );
18457
18458 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18459 assert_hunk_revert(
18460 indoc! {r#"struct Row;
18461 ˇstruct Row2;
18462
18463 struct Row4;
18464 struct Row5;
18465 struct Row6;
18466
18467 struct Row8;ˇ
18468 struct Row10;"#},
18469 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18470 indoc! {r#"struct Row;
18471 struct Row1;
18472 ˇstruct Row2;
18473
18474 struct Row4;
18475 struct Row5;
18476 struct Row6;
18477
18478 struct Row8;ˇ
18479 struct Row9;
18480 struct Row10;"#},
18481 base_text,
18482 &mut cx,
18483 );
18484 assert_hunk_revert(
18485 indoc! {r#"struct Row;
18486 struct Row2«ˇ;
18487 struct Row4;
18488 struct» Row5;
18489 «struct Row6;
18490
18491 struct Row8;ˇ»
18492 struct Row10;"#},
18493 vec![
18494 DiffHunkStatusKind::Deleted,
18495 DiffHunkStatusKind::Deleted,
18496 DiffHunkStatusKind::Deleted,
18497 ],
18498 indoc! {r#"struct Row;
18499 struct Row1;
18500 struct Row2«ˇ;
18501
18502 struct Row4;
18503 struct» Row5;
18504 «struct Row6;
18505
18506 struct Row8;ˇ»
18507 struct Row9;
18508 struct Row10;"#},
18509 base_text,
18510 &mut cx,
18511 );
18512}
18513
18514#[gpui::test]
18515async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18516 init_test(cx, |_| {});
18517
18518 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18519 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18520 let base_text_3 =
18521 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18522
18523 let text_1 = edit_first_char_of_every_line(base_text_1);
18524 let text_2 = edit_first_char_of_every_line(base_text_2);
18525 let text_3 = edit_first_char_of_every_line(base_text_3);
18526
18527 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18528 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18529 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18530
18531 let multibuffer = cx.new(|cx| {
18532 let mut multibuffer = MultiBuffer::new(ReadWrite);
18533 multibuffer.push_excerpts(
18534 buffer_1.clone(),
18535 [
18536 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18537 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18538 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18539 ],
18540 cx,
18541 );
18542 multibuffer.push_excerpts(
18543 buffer_2.clone(),
18544 [
18545 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18546 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18547 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18548 ],
18549 cx,
18550 );
18551 multibuffer.push_excerpts(
18552 buffer_3.clone(),
18553 [
18554 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18555 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18556 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18557 ],
18558 cx,
18559 );
18560 multibuffer
18561 });
18562
18563 let fs = FakeFs::new(cx.executor());
18564 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18565 let (editor, cx) = cx
18566 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18567 editor.update_in(cx, |editor, _window, cx| {
18568 for (buffer, diff_base) in [
18569 (buffer_1.clone(), base_text_1),
18570 (buffer_2.clone(), base_text_2),
18571 (buffer_3.clone(), base_text_3),
18572 ] {
18573 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18574 editor
18575 .buffer
18576 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18577 }
18578 });
18579 cx.executor().run_until_parked();
18580
18581 editor.update_in(cx, |editor, window, cx| {
18582 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}");
18583 editor.select_all(&SelectAll, window, cx);
18584 editor.git_restore(&Default::default(), window, cx);
18585 });
18586 cx.executor().run_until_parked();
18587
18588 // When all ranges are selected, all buffer hunks are reverted.
18589 editor.update(cx, |editor, cx| {
18590 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");
18591 });
18592 buffer_1.update(cx, |buffer, _| {
18593 assert_eq!(buffer.text(), base_text_1);
18594 });
18595 buffer_2.update(cx, |buffer, _| {
18596 assert_eq!(buffer.text(), base_text_2);
18597 });
18598 buffer_3.update(cx, |buffer, _| {
18599 assert_eq!(buffer.text(), base_text_3);
18600 });
18601
18602 editor.update_in(cx, |editor, window, cx| {
18603 editor.undo(&Default::default(), window, cx);
18604 });
18605
18606 editor.update_in(cx, |editor, window, cx| {
18607 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18608 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18609 });
18610 editor.git_restore(&Default::default(), window, cx);
18611 });
18612
18613 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18614 // but not affect buffer_2 and its related excerpts.
18615 editor.update(cx, |editor, cx| {
18616 assert_eq!(
18617 editor.text(cx),
18618 "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}"
18619 );
18620 });
18621 buffer_1.update(cx, |buffer, _| {
18622 assert_eq!(buffer.text(), base_text_1);
18623 });
18624 buffer_2.update(cx, |buffer, _| {
18625 assert_eq!(
18626 buffer.text(),
18627 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18628 );
18629 });
18630 buffer_3.update(cx, |buffer, _| {
18631 assert_eq!(
18632 buffer.text(),
18633 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18634 );
18635 });
18636
18637 fn edit_first_char_of_every_line(text: &str) -> String {
18638 text.split('\n')
18639 .map(|line| format!("X{}", &line[1..]))
18640 .collect::<Vec<_>>()
18641 .join("\n")
18642 }
18643}
18644
18645#[gpui::test]
18646async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18647 init_test(cx, |_| {});
18648
18649 let cols = 4;
18650 let rows = 10;
18651 let sample_text_1 = sample_text(rows, cols, 'a');
18652 assert_eq!(
18653 sample_text_1,
18654 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18655 );
18656 let sample_text_2 = sample_text(rows, cols, 'l');
18657 assert_eq!(
18658 sample_text_2,
18659 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18660 );
18661 let sample_text_3 = sample_text(rows, cols, 'v');
18662 assert_eq!(
18663 sample_text_3,
18664 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18665 );
18666
18667 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18668 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18669 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18670
18671 let multi_buffer = cx.new(|cx| {
18672 let mut multibuffer = MultiBuffer::new(ReadWrite);
18673 multibuffer.push_excerpts(
18674 buffer_1.clone(),
18675 [
18676 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18677 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18678 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18679 ],
18680 cx,
18681 );
18682 multibuffer.push_excerpts(
18683 buffer_2.clone(),
18684 [
18685 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18686 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18687 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18688 ],
18689 cx,
18690 );
18691 multibuffer.push_excerpts(
18692 buffer_3.clone(),
18693 [
18694 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18695 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18696 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18697 ],
18698 cx,
18699 );
18700 multibuffer
18701 });
18702
18703 let fs = FakeFs::new(cx.executor());
18704 fs.insert_tree(
18705 "/a",
18706 json!({
18707 "main.rs": sample_text_1,
18708 "other.rs": sample_text_2,
18709 "lib.rs": sample_text_3,
18710 }),
18711 )
18712 .await;
18713 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18714 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18715 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18716 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18717 Editor::new(
18718 EditorMode::full(),
18719 multi_buffer,
18720 Some(project.clone()),
18721 window,
18722 cx,
18723 )
18724 });
18725 let multibuffer_item_id = workspace
18726 .update(cx, |workspace, window, cx| {
18727 assert!(
18728 workspace.active_item(cx).is_none(),
18729 "active item should be None before the first item is added"
18730 );
18731 workspace.add_item_to_active_pane(
18732 Box::new(multi_buffer_editor.clone()),
18733 None,
18734 true,
18735 window,
18736 cx,
18737 );
18738 let active_item = workspace
18739 .active_item(cx)
18740 .expect("should have an active item after adding the multi buffer");
18741 assert!(
18742 !active_item.is_singleton(cx),
18743 "A multi buffer was expected to active after adding"
18744 );
18745 active_item.item_id()
18746 })
18747 .unwrap();
18748 cx.executor().run_until_parked();
18749
18750 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18751 editor.change_selections(
18752 SelectionEffects::scroll(Autoscroll::Next),
18753 window,
18754 cx,
18755 |s| s.select_ranges(Some(1..2)),
18756 );
18757 editor.open_excerpts(&OpenExcerpts, window, cx);
18758 });
18759 cx.executor().run_until_parked();
18760 let first_item_id = workspace
18761 .update(cx, |workspace, window, cx| {
18762 let active_item = workspace
18763 .active_item(cx)
18764 .expect("should have an active item after navigating into the 1st buffer");
18765 let first_item_id = active_item.item_id();
18766 assert_ne!(
18767 first_item_id, multibuffer_item_id,
18768 "Should navigate into the 1st buffer and activate it"
18769 );
18770 assert!(
18771 active_item.is_singleton(cx),
18772 "New active item should be a singleton buffer"
18773 );
18774 assert_eq!(
18775 active_item
18776 .act_as::<Editor>(cx)
18777 .expect("should have navigated into an editor for the 1st buffer")
18778 .read(cx)
18779 .text(cx),
18780 sample_text_1
18781 );
18782
18783 workspace
18784 .go_back(workspace.active_pane().downgrade(), window, cx)
18785 .detach_and_log_err(cx);
18786
18787 first_item_id
18788 })
18789 .unwrap();
18790 cx.executor().run_until_parked();
18791 workspace
18792 .update(cx, |workspace, _, cx| {
18793 let active_item = workspace
18794 .active_item(cx)
18795 .expect("should have an active item after navigating back");
18796 assert_eq!(
18797 active_item.item_id(),
18798 multibuffer_item_id,
18799 "Should navigate back to the multi buffer"
18800 );
18801 assert!(!active_item.is_singleton(cx));
18802 })
18803 .unwrap();
18804
18805 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18806 editor.change_selections(
18807 SelectionEffects::scroll(Autoscroll::Next),
18808 window,
18809 cx,
18810 |s| s.select_ranges(Some(39..40)),
18811 );
18812 editor.open_excerpts(&OpenExcerpts, window, cx);
18813 });
18814 cx.executor().run_until_parked();
18815 let second_item_id = workspace
18816 .update(cx, |workspace, window, cx| {
18817 let active_item = workspace
18818 .active_item(cx)
18819 .expect("should have an active item after navigating into the 2nd buffer");
18820 let second_item_id = active_item.item_id();
18821 assert_ne!(
18822 second_item_id, multibuffer_item_id,
18823 "Should navigate away from the multibuffer"
18824 );
18825 assert_ne!(
18826 second_item_id, first_item_id,
18827 "Should navigate into the 2nd buffer and activate it"
18828 );
18829 assert!(
18830 active_item.is_singleton(cx),
18831 "New active item should be a singleton buffer"
18832 );
18833 assert_eq!(
18834 active_item
18835 .act_as::<Editor>(cx)
18836 .expect("should have navigated into an editor")
18837 .read(cx)
18838 .text(cx),
18839 sample_text_2
18840 );
18841
18842 workspace
18843 .go_back(workspace.active_pane().downgrade(), window, cx)
18844 .detach_and_log_err(cx);
18845
18846 second_item_id
18847 })
18848 .unwrap();
18849 cx.executor().run_until_parked();
18850 workspace
18851 .update(cx, |workspace, _, cx| {
18852 let active_item = workspace
18853 .active_item(cx)
18854 .expect("should have an active item after navigating back from the 2nd buffer");
18855 assert_eq!(
18856 active_item.item_id(),
18857 multibuffer_item_id,
18858 "Should navigate back from the 2nd buffer to the multi buffer"
18859 );
18860 assert!(!active_item.is_singleton(cx));
18861 })
18862 .unwrap();
18863
18864 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18865 editor.change_selections(
18866 SelectionEffects::scroll(Autoscroll::Next),
18867 window,
18868 cx,
18869 |s| s.select_ranges(Some(70..70)),
18870 );
18871 editor.open_excerpts(&OpenExcerpts, window, cx);
18872 });
18873 cx.executor().run_until_parked();
18874 workspace
18875 .update(cx, |workspace, window, cx| {
18876 let active_item = workspace
18877 .active_item(cx)
18878 .expect("should have an active item after navigating into the 3rd buffer");
18879 let third_item_id = active_item.item_id();
18880 assert_ne!(
18881 third_item_id, multibuffer_item_id,
18882 "Should navigate into the 3rd buffer and activate it"
18883 );
18884 assert_ne!(third_item_id, first_item_id);
18885 assert_ne!(third_item_id, second_item_id);
18886 assert!(
18887 active_item.is_singleton(cx),
18888 "New active item should be a singleton buffer"
18889 );
18890 assert_eq!(
18891 active_item
18892 .act_as::<Editor>(cx)
18893 .expect("should have navigated into an editor")
18894 .read(cx)
18895 .text(cx),
18896 sample_text_3
18897 );
18898
18899 workspace
18900 .go_back(workspace.active_pane().downgrade(), window, cx)
18901 .detach_and_log_err(cx);
18902 })
18903 .unwrap();
18904 cx.executor().run_until_parked();
18905 workspace
18906 .update(cx, |workspace, _, cx| {
18907 let active_item = workspace
18908 .active_item(cx)
18909 .expect("should have an active item after navigating back from the 3rd buffer");
18910 assert_eq!(
18911 active_item.item_id(),
18912 multibuffer_item_id,
18913 "Should navigate back from the 3rd buffer to the multi buffer"
18914 );
18915 assert!(!active_item.is_singleton(cx));
18916 })
18917 .unwrap();
18918}
18919
18920#[gpui::test]
18921async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18922 init_test(cx, |_| {});
18923
18924 let mut cx = EditorTestContext::new(cx).await;
18925
18926 let diff_base = r#"
18927 use some::mod;
18928
18929 const A: u32 = 42;
18930
18931 fn main() {
18932 println!("hello");
18933
18934 println!("world");
18935 }
18936 "#
18937 .unindent();
18938
18939 cx.set_state(
18940 &r#"
18941 use some::modified;
18942
18943 ˇ
18944 fn main() {
18945 println!("hello there");
18946
18947 println!("around the");
18948 println!("world");
18949 }
18950 "#
18951 .unindent(),
18952 );
18953
18954 cx.set_head_text(&diff_base);
18955 executor.run_until_parked();
18956
18957 cx.update_editor(|editor, window, cx| {
18958 editor.go_to_next_hunk(&GoToHunk, window, cx);
18959 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18960 });
18961 executor.run_until_parked();
18962 cx.assert_state_with_diff(
18963 r#"
18964 use some::modified;
18965
18966
18967 fn main() {
18968 - println!("hello");
18969 + ˇ println!("hello there");
18970
18971 println!("around the");
18972 println!("world");
18973 }
18974 "#
18975 .unindent(),
18976 );
18977
18978 cx.update_editor(|editor, window, cx| {
18979 for _ in 0..2 {
18980 editor.go_to_next_hunk(&GoToHunk, window, cx);
18981 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18982 }
18983 });
18984 executor.run_until_parked();
18985 cx.assert_state_with_diff(
18986 r#"
18987 - use some::mod;
18988 + ˇuse some::modified;
18989
18990
18991 fn main() {
18992 - println!("hello");
18993 + println!("hello there");
18994
18995 + println!("around the");
18996 println!("world");
18997 }
18998 "#
18999 .unindent(),
19000 );
19001
19002 cx.update_editor(|editor, window, cx| {
19003 editor.go_to_next_hunk(&GoToHunk, window, cx);
19004 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19005 });
19006 executor.run_until_parked();
19007 cx.assert_state_with_diff(
19008 r#"
19009 - use some::mod;
19010 + use some::modified;
19011
19012 - const A: u32 = 42;
19013 ˇ
19014 fn main() {
19015 - println!("hello");
19016 + println!("hello there");
19017
19018 + println!("around the");
19019 println!("world");
19020 }
19021 "#
19022 .unindent(),
19023 );
19024
19025 cx.update_editor(|editor, window, cx| {
19026 editor.cancel(&Cancel, window, cx);
19027 });
19028
19029 cx.assert_state_with_diff(
19030 r#"
19031 use some::modified;
19032
19033 ˇ
19034 fn main() {
19035 println!("hello there");
19036
19037 println!("around the");
19038 println!("world");
19039 }
19040 "#
19041 .unindent(),
19042 );
19043}
19044
19045#[gpui::test]
19046async fn test_diff_base_change_with_expanded_diff_hunks(
19047 executor: BackgroundExecutor,
19048 cx: &mut TestAppContext,
19049) {
19050 init_test(cx, |_| {});
19051
19052 let mut cx = EditorTestContext::new(cx).await;
19053
19054 let diff_base = r#"
19055 use some::mod1;
19056 use some::mod2;
19057
19058 const A: u32 = 42;
19059 const B: u32 = 42;
19060 const C: u32 = 42;
19061
19062 fn main() {
19063 println!("hello");
19064
19065 println!("world");
19066 }
19067 "#
19068 .unindent();
19069
19070 cx.set_state(
19071 &r#"
19072 use some::mod2;
19073
19074 const A: u32 = 42;
19075 const C: u32 = 42;
19076
19077 fn main(ˇ) {
19078 //println!("hello");
19079
19080 println!("world");
19081 //
19082 //
19083 }
19084 "#
19085 .unindent(),
19086 );
19087
19088 cx.set_head_text(&diff_base);
19089 executor.run_until_parked();
19090
19091 cx.update_editor(|editor, window, cx| {
19092 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19093 });
19094 executor.run_until_parked();
19095 cx.assert_state_with_diff(
19096 r#"
19097 - use some::mod1;
19098 use some::mod2;
19099
19100 const A: u32 = 42;
19101 - const B: u32 = 42;
19102 const C: u32 = 42;
19103
19104 fn main(ˇ) {
19105 - println!("hello");
19106 + //println!("hello");
19107
19108 println!("world");
19109 + //
19110 + //
19111 }
19112 "#
19113 .unindent(),
19114 );
19115
19116 cx.set_head_text("new diff base!");
19117 executor.run_until_parked();
19118 cx.assert_state_with_diff(
19119 r#"
19120 - new diff base!
19121 + use some::mod2;
19122 +
19123 + const A: u32 = 42;
19124 + const C: u32 = 42;
19125 +
19126 + fn main(ˇ) {
19127 + //println!("hello");
19128 +
19129 + println!("world");
19130 + //
19131 + //
19132 + }
19133 "#
19134 .unindent(),
19135 );
19136}
19137
19138#[gpui::test]
19139async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19140 init_test(cx, |_| {});
19141
19142 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19143 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19144 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19145 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19146 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19147 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19148
19149 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19150 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19151 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19152
19153 let multi_buffer = cx.new(|cx| {
19154 let mut multibuffer = MultiBuffer::new(ReadWrite);
19155 multibuffer.push_excerpts(
19156 buffer_1.clone(),
19157 [
19158 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19159 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19160 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19161 ],
19162 cx,
19163 );
19164 multibuffer.push_excerpts(
19165 buffer_2.clone(),
19166 [
19167 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19168 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19169 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19170 ],
19171 cx,
19172 );
19173 multibuffer.push_excerpts(
19174 buffer_3.clone(),
19175 [
19176 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19177 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19178 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19179 ],
19180 cx,
19181 );
19182 multibuffer
19183 });
19184
19185 let editor =
19186 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19187 editor
19188 .update(cx, |editor, _window, cx| {
19189 for (buffer, diff_base) in [
19190 (buffer_1.clone(), file_1_old),
19191 (buffer_2.clone(), file_2_old),
19192 (buffer_3.clone(), file_3_old),
19193 ] {
19194 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19195 editor
19196 .buffer
19197 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19198 }
19199 })
19200 .unwrap();
19201
19202 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19203 cx.run_until_parked();
19204
19205 cx.assert_editor_state(
19206 &"
19207 ˇaaa
19208 ccc
19209 ddd
19210
19211 ggg
19212 hhh
19213
19214
19215 lll
19216 mmm
19217 NNN
19218
19219 qqq
19220 rrr
19221
19222 uuu
19223 111
19224 222
19225 333
19226
19227 666
19228 777
19229
19230 000
19231 !!!"
19232 .unindent(),
19233 );
19234
19235 cx.update_editor(|editor, window, cx| {
19236 editor.select_all(&SelectAll, window, cx);
19237 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19238 });
19239 cx.executor().run_until_parked();
19240
19241 cx.assert_state_with_diff(
19242 "
19243 «aaa
19244 - bbb
19245 ccc
19246 ddd
19247
19248 ggg
19249 hhh
19250
19251
19252 lll
19253 mmm
19254 - nnn
19255 + NNN
19256
19257 qqq
19258 rrr
19259
19260 uuu
19261 111
19262 222
19263 333
19264
19265 + 666
19266 777
19267
19268 000
19269 !!!ˇ»"
19270 .unindent(),
19271 );
19272}
19273
19274#[gpui::test]
19275async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19276 init_test(cx, |_| {});
19277
19278 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19279 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19280
19281 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19282 let multi_buffer = cx.new(|cx| {
19283 let mut multibuffer = MultiBuffer::new(ReadWrite);
19284 multibuffer.push_excerpts(
19285 buffer.clone(),
19286 [
19287 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19288 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19289 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19290 ],
19291 cx,
19292 );
19293 multibuffer
19294 });
19295
19296 let editor =
19297 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19298 editor
19299 .update(cx, |editor, _window, cx| {
19300 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19301 editor
19302 .buffer
19303 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19304 })
19305 .unwrap();
19306
19307 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19308 cx.run_until_parked();
19309
19310 cx.update_editor(|editor, window, cx| {
19311 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19312 });
19313 cx.executor().run_until_parked();
19314
19315 // When the start of a hunk coincides with the start of its excerpt,
19316 // the hunk is expanded. When the start of a hunk is earlier than
19317 // the start of its excerpt, the hunk is not expanded.
19318 cx.assert_state_with_diff(
19319 "
19320 ˇaaa
19321 - bbb
19322 + BBB
19323
19324 - ddd
19325 - eee
19326 + DDD
19327 + EEE
19328 fff
19329
19330 iii
19331 "
19332 .unindent(),
19333 );
19334}
19335
19336#[gpui::test]
19337async fn test_edits_around_expanded_insertion_hunks(
19338 executor: BackgroundExecutor,
19339 cx: &mut TestAppContext,
19340) {
19341 init_test(cx, |_| {});
19342
19343 let mut cx = EditorTestContext::new(cx).await;
19344
19345 let diff_base = r#"
19346 use some::mod1;
19347 use some::mod2;
19348
19349 const A: u32 = 42;
19350
19351 fn main() {
19352 println!("hello");
19353
19354 println!("world");
19355 }
19356 "#
19357 .unindent();
19358 executor.run_until_parked();
19359 cx.set_state(
19360 &r#"
19361 use some::mod1;
19362 use some::mod2;
19363
19364 const A: u32 = 42;
19365 const B: u32 = 42;
19366 const C: u32 = 42;
19367 ˇ
19368
19369 fn main() {
19370 println!("hello");
19371
19372 println!("world");
19373 }
19374 "#
19375 .unindent(),
19376 );
19377
19378 cx.set_head_text(&diff_base);
19379 executor.run_until_parked();
19380
19381 cx.update_editor(|editor, window, cx| {
19382 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19383 });
19384 executor.run_until_parked();
19385
19386 cx.assert_state_with_diff(
19387 r#"
19388 use some::mod1;
19389 use some::mod2;
19390
19391 const A: u32 = 42;
19392 + const B: u32 = 42;
19393 + const C: u32 = 42;
19394 + ˇ
19395
19396 fn main() {
19397 println!("hello");
19398
19399 println!("world");
19400 }
19401 "#
19402 .unindent(),
19403 );
19404
19405 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19406 executor.run_until_parked();
19407
19408 cx.assert_state_with_diff(
19409 r#"
19410 use some::mod1;
19411 use some::mod2;
19412
19413 const A: u32 = 42;
19414 + const B: u32 = 42;
19415 + const C: u32 = 42;
19416 + const D: u32 = 42;
19417 + ˇ
19418
19419 fn main() {
19420 println!("hello");
19421
19422 println!("world");
19423 }
19424 "#
19425 .unindent(),
19426 );
19427
19428 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19429 executor.run_until_parked();
19430
19431 cx.assert_state_with_diff(
19432 r#"
19433 use some::mod1;
19434 use some::mod2;
19435
19436 const A: u32 = 42;
19437 + const B: u32 = 42;
19438 + const C: u32 = 42;
19439 + const D: u32 = 42;
19440 + const E: u32 = 42;
19441 + ˇ
19442
19443 fn main() {
19444 println!("hello");
19445
19446 println!("world");
19447 }
19448 "#
19449 .unindent(),
19450 );
19451
19452 cx.update_editor(|editor, window, cx| {
19453 editor.delete_line(&DeleteLine, window, cx);
19454 });
19455 executor.run_until_parked();
19456
19457 cx.assert_state_with_diff(
19458 r#"
19459 use some::mod1;
19460 use some::mod2;
19461
19462 const A: u32 = 42;
19463 + const B: u32 = 42;
19464 + const C: u32 = 42;
19465 + const D: u32 = 42;
19466 + const E: u32 = 42;
19467 ˇ
19468 fn main() {
19469 println!("hello");
19470
19471 println!("world");
19472 }
19473 "#
19474 .unindent(),
19475 );
19476
19477 cx.update_editor(|editor, window, cx| {
19478 editor.move_up(&MoveUp, window, cx);
19479 editor.delete_line(&DeleteLine, window, cx);
19480 editor.move_up(&MoveUp, window, cx);
19481 editor.delete_line(&DeleteLine, window, cx);
19482 editor.move_up(&MoveUp, window, cx);
19483 editor.delete_line(&DeleteLine, window, cx);
19484 });
19485 executor.run_until_parked();
19486 cx.assert_state_with_diff(
19487 r#"
19488 use some::mod1;
19489 use some::mod2;
19490
19491 const A: u32 = 42;
19492 + const B: u32 = 42;
19493 ˇ
19494 fn main() {
19495 println!("hello");
19496
19497 println!("world");
19498 }
19499 "#
19500 .unindent(),
19501 );
19502
19503 cx.update_editor(|editor, window, cx| {
19504 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19505 editor.delete_line(&DeleteLine, window, cx);
19506 });
19507 executor.run_until_parked();
19508 cx.assert_state_with_diff(
19509 r#"
19510 ˇ
19511 fn main() {
19512 println!("hello");
19513
19514 println!("world");
19515 }
19516 "#
19517 .unindent(),
19518 );
19519}
19520
19521#[gpui::test]
19522async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19523 init_test(cx, |_| {});
19524
19525 let mut cx = EditorTestContext::new(cx).await;
19526 cx.set_head_text(indoc! { "
19527 one
19528 two
19529 three
19530 four
19531 five
19532 "
19533 });
19534 cx.set_state(indoc! { "
19535 one
19536 ˇthree
19537 five
19538 "});
19539 cx.run_until_parked();
19540 cx.update_editor(|editor, window, cx| {
19541 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19542 });
19543 cx.assert_state_with_diff(
19544 indoc! { "
19545 one
19546 - two
19547 ˇthree
19548 - four
19549 five
19550 "}
19551 .to_string(),
19552 );
19553 cx.update_editor(|editor, window, cx| {
19554 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19555 });
19556
19557 cx.assert_state_with_diff(
19558 indoc! { "
19559 one
19560 ˇthree
19561 five
19562 "}
19563 .to_string(),
19564 );
19565
19566 cx.set_state(indoc! { "
19567 one
19568 ˇTWO
19569 three
19570 four
19571 five
19572 "});
19573 cx.run_until_parked();
19574 cx.update_editor(|editor, window, cx| {
19575 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19576 });
19577
19578 cx.assert_state_with_diff(
19579 indoc! { "
19580 one
19581 - two
19582 + ˇTWO
19583 three
19584 four
19585 five
19586 "}
19587 .to_string(),
19588 );
19589 cx.update_editor(|editor, window, cx| {
19590 editor.move_up(&Default::default(), window, cx);
19591 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19592 });
19593 cx.assert_state_with_diff(
19594 indoc! { "
19595 one
19596 ˇTWO
19597 three
19598 four
19599 five
19600 "}
19601 .to_string(),
19602 );
19603}
19604
19605#[gpui::test]
19606async fn test_edits_around_expanded_deletion_hunks(
19607 executor: BackgroundExecutor,
19608 cx: &mut TestAppContext,
19609) {
19610 init_test(cx, |_| {});
19611
19612 let mut cx = EditorTestContext::new(cx).await;
19613
19614 let diff_base = r#"
19615 use some::mod1;
19616 use some::mod2;
19617
19618 const A: u32 = 42;
19619 const B: u32 = 42;
19620 const C: u32 = 42;
19621
19622
19623 fn main() {
19624 println!("hello");
19625
19626 println!("world");
19627 }
19628 "#
19629 .unindent();
19630 executor.run_until_parked();
19631 cx.set_state(
19632 &r#"
19633 use some::mod1;
19634 use some::mod2;
19635
19636 ˇconst B: u32 = 42;
19637 const C: u32 = 42;
19638
19639
19640 fn main() {
19641 println!("hello");
19642
19643 println!("world");
19644 }
19645 "#
19646 .unindent(),
19647 );
19648
19649 cx.set_head_text(&diff_base);
19650 executor.run_until_parked();
19651
19652 cx.update_editor(|editor, window, cx| {
19653 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19654 });
19655 executor.run_until_parked();
19656
19657 cx.assert_state_with_diff(
19658 r#"
19659 use some::mod1;
19660 use some::mod2;
19661
19662 - const A: u32 = 42;
19663 ˇconst B: u32 = 42;
19664 const C: u32 = 42;
19665
19666
19667 fn main() {
19668 println!("hello");
19669
19670 println!("world");
19671 }
19672 "#
19673 .unindent(),
19674 );
19675
19676 cx.update_editor(|editor, window, cx| {
19677 editor.delete_line(&DeleteLine, window, cx);
19678 });
19679 executor.run_until_parked();
19680 cx.assert_state_with_diff(
19681 r#"
19682 use some::mod1;
19683 use some::mod2;
19684
19685 - const A: u32 = 42;
19686 - const B: u32 = 42;
19687 ˇconst C: u32 = 42;
19688
19689
19690 fn main() {
19691 println!("hello");
19692
19693 println!("world");
19694 }
19695 "#
19696 .unindent(),
19697 );
19698
19699 cx.update_editor(|editor, window, cx| {
19700 editor.delete_line(&DeleteLine, window, cx);
19701 });
19702 executor.run_until_parked();
19703 cx.assert_state_with_diff(
19704 r#"
19705 use some::mod1;
19706 use some::mod2;
19707
19708 - const A: u32 = 42;
19709 - const B: u32 = 42;
19710 - const C: u32 = 42;
19711 ˇ
19712
19713 fn main() {
19714 println!("hello");
19715
19716 println!("world");
19717 }
19718 "#
19719 .unindent(),
19720 );
19721
19722 cx.update_editor(|editor, window, cx| {
19723 editor.handle_input("replacement", window, cx);
19724 });
19725 executor.run_until_parked();
19726 cx.assert_state_with_diff(
19727 r#"
19728 use some::mod1;
19729 use some::mod2;
19730
19731 - const A: u32 = 42;
19732 - const B: u32 = 42;
19733 - const C: u32 = 42;
19734 -
19735 + replacementˇ
19736
19737 fn main() {
19738 println!("hello");
19739
19740 println!("world");
19741 }
19742 "#
19743 .unindent(),
19744 );
19745}
19746
19747#[gpui::test]
19748async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19749 init_test(cx, |_| {});
19750
19751 let mut cx = EditorTestContext::new(cx).await;
19752
19753 let base_text = r#"
19754 one
19755 two
19756 three
19757 four
19758 five
19759 "#
19760 .unindent();
19761 executor.run_until_parked();
19762 cx.set_state(
19763 &r#"
19764 one
19765 two
19766 fˇour
19767 five
19768 "#
19769 .unindent(),
19770 );
19771
19772 cx.set_head_text(&base_text);
19773 executor.run_until_parked();
19774
19775 cx.update_editor(|editor, window, cx| {
19776 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19777 });
19778 executor.run_until_parked();
19779
19780 cx.assert_state_with_diff(
19781 r#"
19782 one
19783 two
19784 - three
19785 fˇour
19786 five
19787 "#
19788 .unindent(),
19789 );
19790
19791 cx.update_editor(|editor, window, cx| {
19792 editor.backspace(&Backspace, window, cx);
19793 editor.backspace(&Backspace, window, cx);
19794 });
19795 executor.run_until_parked();
19796 cx.assert_state_with_diff(
19797 r#"
19798 one
19799 two
19800 - threeˇ
19801 - four
19802 + our
19803 five
19804 "#
19805 .unindent(),
19806 );
19807}
19808
19809#[gpui::test]
19810async fn test_edit_after_expanded_modification_hunk(
19811 executor: BackgroundExecutor,
19812 cx: &mut TestAppContext,
19813) {
19814 init_test(cx, |_| {});
19815
19816 let mut cx = EditorTestContext::new(cx).await;
19817
19818 let diff_base = r#"
19819 use some::mod1;
19820 use some::mod2;
19821
19822 const A: u32 = 42;
19823 const B: u32 = 42;
19824 const C: u32 = 42;
19825 const D: u32 = 42;
19826
19827
19828 fn main() {
19829 println!("hello");
19830
19831 println!("world");
19832 }"#
19833 .unindent();
19834
19835 cx.set_state(
19836 &r#"
19837 use some::mod1;
19838 use some::mod2;
19839
19840 const A: u32 = 42;
19841 const B: u32 = 42;
19842 const C: u32 = 43ˇ
19843 const D: u32 = 42;
19844
19845
19846 fn main() {
19847 println!("hello");
19848
19849 println!("world");
19850 }"#
19851 .unindent(),
19852 );
19853
19854 cx.set_head_text(&diff_base);
19855 executor.run_until_parked();
19856 cx.update_editor(|editor, window, cx| {
19857 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19858 });
19859 executor.run_until_parked();
19860
19861 cx.assert_state_with_diff(
19862 r#"
19863 use some::mod1;
19864 use some::mod2;
19865
19866 const A: u32 = 42;
19867 const B: u32 = 42;
19868 - const C: u32 = 42;
19869 + const C: u32 = 43ˇ
19870 const D: u32 = 42;
19871
19872
19873 fn main() {
19874 println!("hello");
19875
19876 println!("world");
19877 }"#
19878 .unindent(),
19879 );
19880
19881 cx.update_editor(|editor, window, cx| {
19882 editor.handle_input("\nnew_line\n", window, cx);
19883 });
19884 executor.run_until_parked();
19885
19886 cx.assert_state_with_diff(
19887 r#"
19888 use some::mod1;
19889 use some::mod2;
19890
19891 const A: u32 = 42;
19892 const B: u32 = 42;
19893 - const C: u32 = 42;
19894 + const C: u32 = 43
19895 + new_line
19896 + ˇ
19897 const D: u32 = 42;
19898
19899
19900 fn main() {
19901 println!("hello");
19902
19903 println!("world");
19904 }"#
19905 .unindent(),
19906 );
19907}
19908
19909#[gpui::test]
19910async fn test_stage_and_unstage_added_file_hunk(
19911 executor: BackgroundExecutor,
19912 cx: &mut TestAppContext,
19913) {
19914 init_test(cx, |_| {});
19915
19916 let mut cx = EditorTestContext::new(cx).await;
19917 cx.update_editor(|editor, _, cx| {
19918 editor.set_expand_all_diff_hunks(cx);
19919 });
19920
19921 let working_copy = r#"
19922 ˇfn main() {
19923 println!("hello, world!");
19924 }
19925 "#
19926 .unindent();
19927
19928 cx.set_state(&working_copy);
19929 executor.run_until_parked();
19930
19931 cx.assert_state_with_diff(
19932 r#"
19933 + ˇfn main() {
19934 + println!("hello, world!");
19935 + }
19936 "#
19937 .unindent(),
19938 );
19939 cx.assert_index_text(None);
19940
19941 cx.update_editor(|editor, window, cx| {
19942 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19943 });
19944 executor.run_until_parked();
19945 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19946 cx.assert_state_with_diff(
19947 r#"
19948 + ˇfn main() {
19949 + println!("hello, world!");
19950 + }
19951 "#
19952 .unindent(),
19953 );
19954
19955 cx.update_editor(|editor, window, cx| {
19956 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19957 });
19958 executor.run_until_parked();
19959 cx.assert_index_text(None);
19960}
19961
19962async fn setup_indent_guides_editor(
19963 text: &str,
19964 cx: &mut TestAppContext,
19965) -> (BufferId, EditorTestContext) {
19966 init_test(cx, |_| {});
19967
19968 let mut cx = EditorTestContext::new(cx).await;
19969
19970 let buffer_id = cx.update_editor(|editor, window, cx| {
19971 editor.set_text(text, window, cx);
19972 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19973
19974 buffer_ids[0]
19975 });
19976
19977 (buffer_id, cx)
19978}
19979
19980fn assert_indent_guides(
19981 range: Range<u32>,
19982 expected: Vec<IndentGuide>,
19983 active_indices: Option<Vec<usize>>,
19984 cx: &mut EditorTestContext,
19985) {
19986 let indent_guides = cx.update_editor(|editor, window, cx| {
19987 let snapshot = editor.snapshot(window, cx).display_snapshot;
19988 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19989 editor,
19990 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19991 true,
19992 &snapshot,
19993 cx,
19994 );
19995
19996 indent_guides.sort_by(|a, b| {
19997 a.depth.cmp(&b.depth).then(
19998 a.start_row
19999 .cmp(&b.start_row)
20000 .then(a.end_row.cmp(&b.end_row)),
20001 )
20002 });
20003 indent_guides
20004 });
20005
20006 if let Some(expected) = active_indices {
20007 let active_indices = cx.update_editor(|editor, window, cx| {
20008 let snapshot = editor.snapshot(window, cx).display_snapshot;
20009 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20010 });
20011
20012 assert_eq!(
20013 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20014 expected,
20015 "Active indent guide indices do not match"
20016 );
20017 }
20018
20019 assert_eq!(indent_guides, expected, "Indent guides do not match");
20020}
20021
20022fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20023 IndentGuide {
20024 buffer_id,
20025 start_row: MultiBufferRow(start_row),
20026 end_row: MultiBufferRow(end_row),
20027 depth,
20028 tab_size: 4,
20029 settings: IndentGuideSettings {
20030 enabled: true,
20031 line_width: 1,
20032 active_line_width: 1,
20033 coloring: IndentGuideColoring::default(),
20034 background_coloring: IndentGuideBackgroundColoring::default(),
20035 },
20036 }
20037}
20038
20039#[gpui::test]
20040async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20041 let (buffer_id, mut cx) = setup_indent_guides_editor(
20042 &"
20043 fn main() {
20044 let a = 1;
20045 }"
20046 .unindent(),
20047 cx,
20048 )
20049 .await;
20050
20051 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20052}
20053
20054#[gpui::test]
20055async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20056 let (buffer_id, mut cx) = setup_indent_guides_editor(
20057 &"
20058 fn main() {
20059 let a = 1;
20060 let b = 2;
20061 }"
20062 .unindent(),
20063 cx,
20064 )
20065 .await;
20066
20067 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20068}
20069
20070#[gpui::test]
20071async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20072 let (buffer_id, mut cx) = setup_indent_guides_editor(
20073 &"
20074 fn main() {
20075 let a = 1;
20076 if a == 3 {
20077 let b = 2;
20078 } else {
20079 let c = 3;
20080 }
20081 }"
20082 .unindent(),
20083 cx,
20084 )
20085 .await;
20086
20087 assert_indent_guides(
20088 0..8,
20089 vec![
20090 indent_guide(buffer_id, 1, 6, 0),
20091 indent_guide(buffer_id, 3, 3, 1),
20092 indent_guide(buffer_id, 5, 5, 1),
20093 ],
20094 None,
20095 &mut cx,
20096 );
20097}
20098
20099#[gpui::test]
20100async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20101 let (buffer_id, mut cx) = setup_indent_guides_editor(
20102 &"
20103 fn main() {
20104 let a = 1;
20105 let b = 2;
20106 let c = 3;
20107 }"
20108 .unindent(),
20109 cx,
20110 )
20111 .await;
20112
20113 assert_indent_guides(
20114 0..5,
20115 vec![
20116 indent_guide(buffer_id, 1, 3, 0),
20117 indent_guide(buffer_id, 2, 2, 1),
20118 ],
20119 None,
20120 &mut cx,
20121 );
20122}
20123
20124#[gpui::test]
20125async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20126 let (buffer_id, mut cx) = setup_indent_guides_editor(
20127 &"
20128 fn main() {
20129 let a = 1;
20130
20131 let c = 3;
20132 }"
20133 .unindent(),
20134 cx,
20135 )
20136 .await;
20137
20138 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20139}
20140
20141#[gpui::test]
20142async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20143 let (buffer_id, mut cx) = setup_indent_guides_editor(
20144 &"
20145 fn main() {
20146 let a = 1;
20147
20148 let c = 3;
20149
20150 if a == 3 {
20151 let b = 2;
20152 } else {
20153 let c = 3;
20154 }
20155 }"
20156 .unindent(),
20157 cx,
20158 )
20159 .await;
20160
20161 assert_indent_guides(
20162 0..11,
20163 vec![
20164 indent_guide(buffer_id, 1, 9, 0),
20165 indent_guide(buffer_id, 6, 6, 1),
20166 indent_guide(buffer_id, 8, 8, 1),
20167 ],
20168 None,
20169 &mut cx,
20170 );
20171}
20172
20173#[gpui::test]
20174async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20175 let (buffer_id, mut cx) = setup_indent_guides_editor(
20176 &"
20177 fn main() {
20178 let a = 1;
20179
20180 let c = 3;
20181
20182 if a == 3 {
20183 let b = 2;
20184 } else {
20185 let c = 3;
20186 }
20187 }"
20188 .unindent(),
20189 cx,
20190 )
20191 .await;
20192
20193 assert_indent_guides(
20194 1..11,
20195 vec![
20196 indent_guide(buffer_id, 1, 9, 0),
20197 indent_guide(buffer_id, 6, 6, 1),
20198 indent_guide(buffer_id, 8, 8, 1),
20199 ],
20200 None,
20201 &mut cx,
20202 );
20203}
20204
20205#[gpui::test]
20206async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20207 let (buffer_id, mut cx) = setup_indent_guides_editor(
20208 &"
20209 fn main() {
20210 let a = 1;
20211
20212 let c = 3;
20213
20214 if a == 3 {
20215 let b = 2;
20216 } else {
20217 let c = 3;
20218 }
20219 }"
20220 .unindent(),
20221 cx,
20222 )
20223 .await;
20224
20225 assert_indent_guides(
20226 1..10,
20227 vec![
20228 indent_guide(buffer_id, 1, 9, 0),
20229 indent_guide(buffer_id, 6, 6, 1),
20230 indent_guide(buffer_id, 8, 8, 1),
20231 ],
20232 None,
20233 &mut cx,
20234 );
20235}
20236
20237#[gpui::test]
20238async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20239 let (buffer_id, mut cx) = setup_indent_guides_editor(
20240 &"
20241 fn main() {
20242 if a {
20243 b(
20244 c,
20245 d,
20246 )
20247 } else {
20248 e(
20249 f
20250 )
20251 }
20252 }"
20253 .unindent(),
20254 cx,
20255 )
20256 .await;
20257
20258 assert_indent_guides(
20259 0..11,
20260 vec![
20261 indent_guide(buffer_id, 1, 10, 0),
20262 indent_guide(buffer_id, 2, 5, 1),
20263 indent_guide(buffer_id, 7, 9, 1),
20264 indent_guide(buffer_id, 3, 4, 2),
20265 indent_guide(buffer_id, 8, 8, 2),
20266 ],
20267 None,
20268 &mut cx,
20269 );
20270
20271 cx.update_editor(|editor, window, cx| {
20272 editor.fold_at(MultiBufferRow(2), window, cx);
20273 assert_eq!(
20274 editor.display_text(cx),
20275 "
20276 fn main() {
20277 if a {
20278 b(⋯
20279 )
20280 } else {
20281 e(
20282 f
20283 )
20284 }
20285 }"
20286 .unindent()
20287 );
20288 });
20289
20290 assert_indent_guides(
20291 0..11,
20292 vec![
20293 indent_guide(buffer_id, 1, 10, 0),
20294 indent_guide(buffer_id, 2, 5, 1),
20295 indent_guide(buffer_id, 7, 9, 1),
20296 indent_guide(buffer_id, 8, 8, 2),
20297 ],
20298 None,
20299 &mut cx,
20300 );
20301}
20302
20303#[gpui::test]
20304async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20305 let (buffer_id, mut cx) = setup_indent_guides_editor(
20306 &"
20307 block1
20308 block2
20309 block3
20310 block4
20311 block2
20312 block1
20313 block1"
20314 .unindent(),
20315 cx,
20316 )
20317 .await;
20318
20319 assert_indent_guides(
20320 1..10,
20321 vec![
20322 indent_guide(buffer_id, 1, 4, 0),
20323 indent_guide(buffer_id, 2, 3, 1),
20324 indent_guide(buffer_id, 3, 3, 2),
20325 ],
20326 None,
20327 &mut cx,
20328 );
20329}
20330
20331#[gpui::test]
20332async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20333 let (buffer_id, mut cx) = setup_indent_guides_editor(
20334 &"
20335 block1
20336 block2
20337 block3
20338
20339 block1
20340 block1"
20341 .unindent(),
20342 cx,
20343 )
20344 .await;
20345
20346 assert_indent_guides(
20347 0..6,
20348 vec![
20349 indent_guide(buffer_id, 1, 2, 0),
20350 indent_guide(buffer_id, 2, 2, 1),
20351 ],
20352 None,
20353 &mut cx,
20354 );
20355}
20356
20357#[gpui::test]
20358async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20359 let (buffer_id, mut cx) = setup_indent_guides_editor(
20360 &"
20361 function component() {
20362 \treturn (
20363 \t\t\t
20364 \t\t<div>
20365 \t\t\t<abc></abc>
20366 \t\t</div>
20367 \t)
20368 }"
20369 .unindent(),
20370 cx,
20371 )
20372 .await;
20373
20374 assert_indent_guides(
20375 0..8,
20376 vec![
20377 indent_guide(buffer_id, 1, 6, 0),
20378 indent_guide(buffer_id, 2, 5, 1),
20379 indent_guide(buffer_id, 4, 4, 2),
20380 ],
20381 None,
20382 &mut cx,
20383 );
20384}
20385
20386#[gpui::test]
20387async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20388 let (buffer_id, mut cx) = setup_indent_guides_editor(
20389 &"
20390 function component() {
20391 \treturn (
20392 \t
20393 \t\t<div>
20394 \t\t\t<abc></abc>
20395 \t\t</div>
20396 \t)
20397 }"
20398 .unindent(),
20399 cx,
20400 )
20401 .await;
20402
20403 assert_indent_guides(
20404 0..8,
20405 vec![
20406 indent_guide(buffer_id, 1, 6, 0),
20407 indent_guide(buffer_id, 2, 5, 1),
20408 indent_guide(buffer_id, 4, 4, 2),
20409 ],
20410 None,
20411 &mut cx,
20412 );
20413}
20414
20415#[gpui::test]
20416async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20417 let (buffer_id, mut cx) = setup_indent_guides_editor(
20418 &"
20419 block1
20420
20421
20422
20423 block2
20424 "
20425 .unindent(),
20426 cx,
20427 )
20428 .await;
20429
20430 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20431}
20432
20433#[gpui::test]
20434async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20435 let (buffer_id, mut cx) = setup_indent_guides_editor(
20436 &"
20437 def a:
20438 \tb = 3
20439 \tif True:
20440 \t\tc = 4
20441 \t\td = 5
20442 \tprint(b)
20443 "
20444 .unindent(),
20445 cx,
20446 )
20447 .await;
20448
20449 assert_indent_guides(
20450 0..6,
20451 vec![
20452 indent_guide(buffer_id, 1, 5, 0),
20453 indent_guide(buffer_id, 3, 4, 1),
20454 ],
20455 None,
20456 &mut cx,
20457 );
20458}
20459
20460#[gpui::test]
20461async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20462 let (buffer_id, mut cx) = setup_indent_guides_editor(
20463 &"
20464 fn main() {
20465 let a = 1;
20466 }"
20467 .unindent(),
20468 cx,
20469 )
20470 .await;
20471
20472 cx.update_editor(|editor, window, cx| {
20473 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20474 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20475 });
20476 });
20477
20478 assert_indent_guides(
20479 0..3,
20480 vec![indent_guide(buffer_id, 1, 1, 0)],
20481 Some(vec![0]),
20482 &mut cx,
20483 );
20484}
20485
20486#[gpui::test]
20487async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20488 let (buffer_id, mut cx) = setup_indent_guides_editor(
20489 &"
20490 fn main() {
20491 if 1 == 2 {
20492 let a = 1;
20493 }
20494 }"
20495 .unindent(),
20496 cx,
20497 )
20498 .await;
20499
20500 cx.update_editor(|editor, window, cx| {
20501 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20502 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20503 });
20504 });
20505
20506 assert_indent_guides(
20507 0..4,
20508 vec![
20509 indent_guide(buffer_id, 1, 3, 0),
20510 indent_guide(buffer_id, 2, 2, 1),
20511 ],
20512 Some(vec![1]),
20513 &mut cx,
20514 );
20515
20516 cx.update_editor(|editor, window, cx| {
20517 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20518 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20519 });
20520 });
20521
20522 assert_indent_guides(
20523 0..4,
20524 vec![
20525 indent_guide(buffer_id, 1, 3, 0),
20526 indent_guide(buffer_id, 2, 2, 1),
20527 ],
20528 Some(vec![1]),
20529 &mut cx,
20530 );
20531
20532 cx.update_editor(|editor, window, cx| {
20533 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20534 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20535 });
20536 });
20537
20538 assert_indent_guides(
20539 0..4,
20540 vec![
20541 indent_guide(buffer_id, 1, 3, 0),
20542 indent_guide(buffer_id, 2, 2, 1),
20543 ],
20544 Some(vec![0]),
20545 &mut cx,
20546 );
20547}
20548
20549#[gpui::test]
20550async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20551 let (buffer_id, mut cx) = setup_indent_guides_editor(
20552 &"
20553 fn main() {
20554 let a = 1;
20555
20556 let b = 2;
20557 }"
20558 .unindent(),
20559 cx,
20560 )
20561 .await;
20562
20563 cx.update_editor(|editor, window, cx| {
20564 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20565 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20566 });
20567 });
20568
20569 assert_indent_guides(
20570 0..5,
20571 vec![indent_guide(buffer_id, 1, 3, 0)],
20572 Some(vec![0]),
20573 &mut cx,
20574 );
20575}
20576
20577#[gpui::test]
20578async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20579 let (buffer_id, mut cx) = setup_indent_guides_editor(
20580 &"
20581 def m:
20582 a = 1
20583 pass"
20584 .unindent(),
20585 cx,
20586 )
20587 .await;
20588
20589 cx.update_editor(|editor, window, cx| {
20590 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20591 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20592 });
20593 });
20594
20595 assert_indent_guides(
20596 0..3,
20597 vec![indent_guide(buffer_id, 1, 2, 0)],
20598 Some(vec![0]),
20599 &mut cx,
20600 );
20601}
20602
20603#[gpui::test]
20604async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20605 init_test(cx, |_| {});
20606 let mut cx = EditorTestContext::new(cx).await;
20607 let text = indoc! {
20608 "
20609 impl A {
20610 fn b() {
20611 0;
20612 3;
20613 5;
20614 6;
20615 7;
20616 }
20617 }
20618 "
20619 };
20620 let base_text = indoc! {
20621 "
20622 impl A {
20623 fn b() {
20624 0;
20625 1;
20626 2;
20627 3;
20628 4;
20629 }
20630 fn c() {
20631 5;
20632 6;
20633 7;
20634 }
20635 }
20636 "
20637 };
20638
20639 cx.update_editor(|editor, window, cx| {
20640 editor.set_text(text, window, cx);
20641
20642 editor.buffer().update(cx, |multibuffer, cx| {
20643 let buffer = multibuffer.as_singleton().unwrap();
20644 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20645
20646 multibuffer.set_all_diff_hunks_expanded(cx);
20647 multibuffer.add_diff(diff, cx);
20648
20649 buffer.read(cx).remote_id()
20650 })
20651 });
20652 cx.run_until_parked();
20653
20654 cx.assert_state_with_diff(
20655 indoc! { "
20656 impl A {
20657 fn b() {
20658 0;
20659 - 1;
20660 - 2;
20661 3;
20662 - 4;
20663 - }
20664 - fn c() {
20665 5;
20666 6;
20667 7;
20668 }
20669 }
20670 ˇ"
20671 }
20672 .to_string(),
20673 );
20674
20675 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20676 editor
20677 .snapshot(window, cx)
20678 .buffer_snapshot
20679 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20680 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20681 .collect::<Vec<_>>()
20682 });
20683 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20684 assert_eq!(
20685 actual_guides,
20686 vec![
20687 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20688 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20689 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20690 ]
20691 );
20692}
20693
20694#[gpui::test]
20695async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20696 init_test(cx, |_| {});
20697 let mut cx = EditorTestContext::new(cx).await;
20698
20699 let diff_base = r#"
20700 a
20701 b
20702 c
20703 "#
20704 .unindent();
20705
20706 cx.set_state(
20707 &r#"
20708 ˇA
20709 b
20710 C
20711 "#
20712 .unindent(),
20713 );
20714 cx.set_head_text(&diff_base);
20715 cx.update_editor(|editor, window, cx| {
20716 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20717 });
20718 executor.run_until_parked();
20719
20720 let both_hunks_expanded = r#"
20721 - a
20722 + ˇA
20723 b
20724 - c
20725 + C
20726 "#
20727 .unindent();
20728
20729 cx.assert_state_with_diff(both_hunks_expanded.clone());
20730
20731 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20732 let snapshot = editor.snapshot(window, cx);
20733 let hunks = editor
20734 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20735 .collect::<Vec<_>>();
20736 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20737 let buffer_id = hunks[0].buffer_id;
20738 hunks
20739 .into_iter()
20740 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20741 .collect::<Vec<_>>()
20742 });
20743 assert_eq!(hunk_ranges.len(), 2);
20744
20745 cx.update_editor(|editor, _, cx| {
20746 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20747 });
20748 executor.run_until_parked();
20749
20750 let second_hunk_expanded = r#"
20751 ˇA
20752 b
20753 - c
20754 + C
20755 "#
20756 .unindent();
20757
20758 cx.assert_state_with_diff(second_hunk_expanded);
20759
20760 cx.update_editor(|editor, _, cx| {
20761 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20762 });
20763 executor.run_until_parked();
20764
20765 cx.assert_state_with_diff(both_hunks_expanded.clone());
20766
20767 cx.update_editor(|editor, _, cx| {
20768 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20769 });
20770 executor.run_until_parked();
20771
20772 let first_hunk_expanded = r#"
20773 - a
20774 + ˇA
20775 b
20776 C
20777 "#
20778 .unindent();
20779
20780 cx.assert_state_with_diff(first_hunk_expanded);
20781
20782 cx.update_editor(|editor, _, cx| {
20783 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20784 });
20785 executor.run_until_parked();
20786
20787 cx.assert_state_with_diff(both_hunks_expanded);
20788
20789 cx.set_state(
20790 &r#"
20791 ˇA
20792 b
20793 "#
20794 .unindent(),
20795 );
20796 cx.run_until_parked();
20797
20798 // TODO this cursor position seems bad
20799 cx.assert_state_with_diff(
20800 r#"
20801 - ˇa
20802 + A
20803 b
20804 "#
20805 .unindent(),
20806 );
20807
20808 cx.update_editor(|editor, window, cx| {
20809 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20810 });
20811
20812 cx.assert_state_with_diff(
20813 r#"
20814 - ˇa
20815 + A
20816 b
20817 - c
20818 "#
20819 .unindent(),
20820 );
20821
20822 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20823 let snapshot = editor.snapshot(window, cx);
20824 let hunks = editor
20825 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20826 .collect::<Vec<_>>();
20827 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20828 let buffer_id = hunks[0].buffer_id;
20829 hunks
20830 .into_iter()
20831 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20832 .collect::<Vec<_>>()
20833 });
20834 assert_eq!(hunk_ranges.len(), 2);
20835
20836 cx.update_editor(|editor, _, cx| {
20837 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20838 });
20839 executor.run_until_parked();
20840
20841 cx.assert_state_with_diff(
20842 r#"
20843 - ˇa
20844 + A
20845 b
20846 "#
20847 .unindent(),
20848 );
20849}
20850
20851#[gpui::test]
20852async fn test_toggle_deletion_hunk_at_start_of_file(
20853 executor: BackgroundExecutor,
20854 cx: &mut TestAppContext,
20855) {
20856 init_test(cx, |_| {});
20857 let mut cx = EditorTestContext::new(cx).await;
20858
20859 let diff_base = r#"
20860 a
20861 b
20862 c
20863 "#
20864 .unindent();
20865
20866 cx.set_state(
20867 &r#"
20868 ˇb
20869 c
20870 "#
20871 .unindent(),
20872 );
20873 cx.set_head_text(&diff_base);
20874 cx.update_editor(|editor, window, cx| {
20875 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20876 });
20877 executor.run_until_parked();
20878
20879 let hunk_expanded = r#"
20880 - a
20881 ˇb
20882 c
20883 "#
20884 .unindent();
20885
20886 cx.assert_state_with_diff(hunk_expanded.clone());
20887
20888 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20889 let snapshot = editor.snapshot(window, cx);
20890 let hunks = editor
20891 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20892 .collect::<Vec<_>>();
20893 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20894 let buffer_id = hunks[0].buffer_id;
20895 hunks
20896 .into_iter()
20897 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20898 .collect::<Vec<_>>()
20899 });
20900 assert_eq!(hunk_ranges.len(), 1);
20901
20902 cx.update_editor(|editor, _, cx| {
20903 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20904 });
20905 executor.run_until_parked();
20906
20907 let hunk_collapsed = r#"
20908 ˇb
20909 c
20910 "#
20911 .unindent();
20912
20913 cx.assert_state_with_diff(hunk_collapsed);
20914
20915 cx.update_editor(|editor, _, cx| {
20916 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20917 });
20918 executor.run_until_parked();
20919
20920 cx.assert_state_with_diff(hunk_expanded);
20921}
20922
20923#[gpui::test]
20924async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20925 init_test(cx, |_| {});
20926
20927 let fs = FakeFs::new(cx.executor());
20928 fs.insert_tree(
20929 path!("/test"),
20930 json!({
20931 ".git": {},
20932 "file-1": "ONE\n",
20933 "file-2": "TWO\n",
20934 "file-3": "THREE\n",
20935 }),
20936 )
20937 .await;
20938
20939 fs.set_head_for_repo(
20940 path!("/test/.git").as_ref(),
20941 &[
20942 ("file-1".into(), "one\n".into()),
20943 ("file-2".into(), "two\n".into()),
20944 ("file-3".into(), "three\n".into()),
20945 ],
20946 "deadbeef",
20947 );
20948
20949 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20950 let mut buffers = vec![];
20951 for i in 1..=3 {
20952 let buffer = project
20953 .update(cx, |project, cx| {
20954 let path = format!(path!("/test/file-{}"), i);
20955 project.open_local_buffer(path, cx)
20956 })
20957 .await
20958 .unwrap();
20959 buffers.push(buffer);
20960 }
20961
20962 let multibuffer = cx.new(|cx| {
20963 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20964 multibuffer.set_all_diff_hunks_expanded(cx);
20965 for buffer in &buffers {
20966 let snapshot = buffer.read(cx).snapshot();
20967 multibuffer.set_excerpts_for_path(
20968 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20969 buffer.clone(),
20970 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20971 2,
20972 cx,
20973 );
20974 }
20975 multibuffer
20976 });
20977
20978 let editor = cx.add_window(|window, cx| {
20979 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20980 });
20981 cx.run_until_parked();
20982
20983 let snapshot = editor
20984 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20985 .unwrap();
20986 let hunks = snapshot
20987 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20988 .map(|hunk| match hunk {
20989 DisplayDiffHunk::Unfolded {
20990 display_row_range, ..
20991 } => display_row_range,
20992 DisplayDiffHunk::Folded { .. } => unreachable!(),
20993 })
20994 .collect::<Vec<_>>();
20995 assert_eq!(
20996 hunks,
20997 [
20998 DisplayRow(2)..DisplayRow(4),
20999 DisplayRow(7)..DisplayRow(9),
21000 DisplayRow(12)..DisplayRow(14),
21001 ]
21002 );
21003}
21004
21005#[gpui::test]
21006async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21007 init_test(cx, |_| {});
21008
21009 let mut cx = EditorTestContext::new(cx).await;
21010 cx.set_head_text(indoc! { "
21011 one
21012 two
21013 three
21014 four
21015 five
21016 "
21017 });
21018 cx.set_index_text(indoc! { "
21019 one
21020 two
21021 three
21022 four
21023 five
21024 "
21025 });
21026 cx.set_state(indoc! {"
21027 one
21028 TWO
21029 ˇTHREE
21030 FOUR
21031 five
21032 "});
21033 cx.run_until_parked();
21034 cx.update_editor(|editor, window, cx| {
21035 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21036 });
21037 cx.run_until_parked();
21038 cx.assert_index_text(Some(indoc! {"
21039 one
21040 TWO
21041 THREE
21042 FOUR
21043 five
21044 "}));
21045 cx.set_state(indoc! { "
21046 one
21047 TWO
21048 ˇTHREE-HUNDRED
21049 FOUR
21050 five
21051 "});
21052 cx.run_until_parked();
21053 cx.update_editor(|editor, window, cx| {
21054 let snapshot = editor.snapshot(window, cx);
21055 let hunks = editor
21056 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
21057 .collect::<Vec<_>>();
21058 assert_eq!(hunks.len(), 1);
21059 assert_eq!(
21060 hunks[0].status(),
21061 DiffHunkStatus {
21062 kind: DiffHunkStatusKind::Modified,
21063 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21064 }
21065 );
21066
21067 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21068 });
21069 cx.run_until_parked();
21070 cx.assert_index_text(Some(indoc! {"
21071 one
21072 TWO
21073 THREE-HUNDRED
21074 FOUR
21075 five
21076 "}));
21077}
21078
21079#[gpui::test]
21080fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21081 init_test(cx, |_| {});
21082
21083 let editor = cx.add_window(|window, cx| {
21084 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21085 build_editor(buffer, window, cx)
21086 });
21087
21088 let render_args = Arc::new(Mutex::new(None));
21089 let snapshot = editor
21090 .update(cx, |editor, window, cx| {
21091 let snapshot = editor.buffer().read(cx).snapshot(cx);
21092 let range =
21093 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21094
21095 struct RenderArgs {
21096 row: MultiBufferRow,
21097 folded: bool,
21098 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21099 }
21100
21101 let crease = Crease::inline(
21102 range,
21103 FoldPlaceholder::test(),
21104 {
21105 let toggle_callback = render_args.clone();
21106 move |row, folded, callback, _window, _cx| {
21107 *toggle_callback.lock() = Some(RenderArgs {
21108 row,
21109 folded,
21110 callback,
21111 });
21112 div()
21113 }
21114 },
21115 |_row, _folded, _window, _cx| div(),
21116 );
21117
21118 editor.insert_creases(Some(crease), cx);
21119 let snapshot = editor.snapshot(window, cx);
21120 let _div =
21121 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21122 snapshot
21123 })
21124 .unwrap();
21125
21126 let render_args = render_args.lock().take().unwrap();
21127 assert_eq!(render_args.row, MultiBufferRow(1));
21128 assert!(!render_args.folded);
21129 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21130
21131 cx.update_window(*editor, |_, window, cx| {
21132 (render_args.callback)(true, window, cx)
21133 })
21134 .unwrap();
21135 let snapshot = editor
21136 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21137 .unwrap();
21138 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21139
21140 cx.update_window(*editor, |_, window, cx| {
21141 (render_args.callback)(false, window, cx)
21142 })
21143 .unwrap();
21144 let snapshot = editor
21145 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21146 .unwrap();
21147 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21148}
21149
21150#[gpui::test]
21151async fn test_input_text(cx: &mut TestAppContext) {
21152 init_test(cx, |_| {});
21153 let mut cx = EditorTestContext::new(cx).await;
21154
21155 cx.set_state(
21156 &r#"ˇone
21157 two
21158
21159 three
21160 fourˇ
21161 five
21162
21163 siˇx"#
21164 .unindent(),
21165 );
21166
21167 cx.dispatch_action(HandleInput(String::new()));
21168 cx.assert_editor_state(
21169 &r#"ˇone
21170 two
21171
21172 three
21173 fourˇ
21174 five
21175
21176 siˇx"#
21177 .unindent(),
21178 );
21179
21180 cx.dispatch_action(HandleInput("AAAA".to_string()));
21181 cx.assert_editor_state(
21182 &r#"AAAAˇone
21183 two
21184
21185 three
21186 fourAAAAˇ
21187 five
21188
21189 siAAAAˇx"#
21190 .unindent(),
21191 );
21192}
21193
21194#[gpui::test]
21195async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21196 init_test(cx, |_| {});
21197
21198 let mut cx = EditorTestContext::new(cx).await;
21199 cx.set_state(
21200 r#"let foo = 1;
21201let foo = 2;
21202let foo = 3;
21203let fooˇ = 4;
21204let foo = 5;
21205let foo = 6;
21206let foo = 7;
21207let foo = 8;
21208let foo = 9;
21209let foo = 10;
21210let foo = 11;
21211let foo = 12;
21212let foo = 13;
21213let foo = 14;
21214let foo = 15;"#,
21215 );
21216
21217 cx.update_editor(|e, window, cx| {
21218 assert_eq!(
21219 e.next_scroll_position,
21220 NextScrollCursorCenterTopBottom::Center,
21221 "Default next scroll direction is center",
21222 );
21223
21224 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21225 assert_eq!(
21226 e.next_scroll_position,
21227 NextScrollCursorCenterTopBottom::Top,
21228 "After center, next scroll direction should be top",
21229 );
21230
21231 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21232 assert_eq!(
21233 e.next_scroll_position,
21234 NextScrollCursorCenterTopBottom::Bottom,
21235 "After top, next scroll direction should be bottom",
21236 );
21237
21238 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21239 assert_eq!(
21240 e.next_scroll_position,
21241 NextScrollCursorCenterTopBottom::Center,
21242 "After bottom, scrolling should start over",
21243 );
21244
21245 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21246 assert_eq!(
21247 e.next_scroll_position,
21248 NextScrollCursorCenterTopBottom::Top,
21249 "Scrolling continues if retriggered fast enough"
21250 );
21251 });
21252
21253 cx.executor()
21254 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21255 cx.executor().run_until_parked();
21256 cx.update_editor(|e, _, _| {
21257 assert_eq!(
21258 e.next_scroll_position,
21259 NextScrollCursorCenterTopBottom::Center,
21260 "If scrolling is not triggered fast enough, it should reset"
21261 );
21262 });
21263}
21264
21265#[gpui::test]
21266async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21267 init_test(cx, |_| {});
21268 let mut cx = EditorLspTestContext::new_rust(
21269 lsp::ServerCapabilities {
21270 definition_provider: Some(lsp::OneOf::Left(true)),
21271 references_provider: Some(lsp::OneOf::Left(true)),
21272 ..lsp::ServerCapabilities::default()
21273 },
21274 cx,
21275 )
21276 .await;
21277
21278 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21279 let go_to_definition = cx
21280 .lsp
21281 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21282 move |params, _| async move {
21283 if empty_go_to_definition {
21284 Ok(None)
21285 } else {
21286 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21287 uri: params.text_document_position_params.text_document.uri,
21288 range: lsp::Range::new(
21289 lsp::Position::new(4, 3),
21290 lsp::Position::new(4, 6),
21291 ),
21292 })))
21293 }
21294 },
21295 );
21296 let references = cx
21297 .lsp
21298 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21299 Ok(Some(vec![lsp::Location {
21300 uri: params.text_document_position.text_document.uri,
21301 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21302 }]))
21303 });
21304 (go_to_definition, references)
21305 };
21306
21307 cx.set_state(
21308 &r#"fn one() {
21309 let mut a = ˇtwo();
21310 }
21311
21312 fn two() {}"#
21313 .unindent(),
21314 );
21315 set_up_lsp_handlers(false, &mut cx);
21316 let navigated = cx
21317 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21318 .await
21319 .expect("Failed to navigate to definition");
21320 assert_eq!(
21321 navigated,
21322 Navigated::Yes,
21323 "Should have navigated to definition from the GetDefinition response"
21324 );
21325 cx.assert_editor_state(
21326 &r#"fn one() {
21327 let mut a = two();
21328 }
21329
21330 fn «twoˇ»() {}"#
21331 .unindent(),
21332 );
21333
21334 let editors = cx.update_workspace(|workspace, _, cx| {
21335 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21336 });
21337 cx.update_editor(|_, _, test_editor_cx| {
21338 assert_eq!(
21339 editors.len(),
21340 1,
21341 "Initially, only one, test, editor should be open in the workspace"
21342 );
21343 assert_eq!(
21344 test_editor_cx.entity(),
21345 editors.last().expect("Asserted len is 1").clone()
21346 );
21347 });
21348
21349 set_up_lsp_handlers(true, &mut cx);
21350 let navigated = cx
21351 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21352 .await
21353 .expect("Failed to navigate to lookup references");
21354 assert_eq!(
21355 navigated,
21356 Navigated::Yes,
21357 "Should have navigated to references as a fallback after empty GoToDefinition response"
21358 );
21359 // We should not change the selections in the existing file,
21360 // if opening another milti buffer with the references
21361 cx.assert_editor_state(
21362 &r#"fn one() {
21363 let mut a = two();
21364 }
21365
21366 fn «twoˇ»() {}"#
21367 .unindent(),
21368 );
21369 let editors = cx.update_workspace(|workspace, _, cx| {
21370 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21371 });
21372 cx.update_editor(|_, _, test_editor_cx| {
21373 assert_eq!(
21374 editors.len(),
21375 2,
21376 "After falling back to references search, we open a new editor with the results"
21377 );
21378 let references_fallback_text = editors
21379 .into_iter()
21380 .find(|new_editor| *new_editor != test_editor_cx.entity())
21381 .expect("Should have one non-test editor now")
21382 .read(test_editor_cx)
21383 .text(test_editor_cx);
21384 assert_eq!(
21385 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21386 "Should use the range from the references response and not the GoToDefinition one"
21387 );
21388 });
21389}
21390
21391#[gpui::test]
21392async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21393 init_test(cx, |_| {});
21394 cx.update(|cx| {
21395 let mut editor_settings = EditorSettings::get_global(cx).clone();
21396 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21397 EditorSettings::override_global(editor_settings, cx);
21398 });
21399 let mut cx = EditorLspTestContext::new_rust(
21400 lsp::ServerCapabilities {
21401 definition_provider: Some(lsp::OneOf::Left(true)),
21402 references_provider: Some(lsp::OneOf::Left(true)),
21403 ..lsp::ServerCapabilities::default()
21404 },
21405 cx,
21406 )
21407 .await;
21408 let original_state = r#"fn one() {
21409 let mut a = ˇtwo();
21410 }
21411
21412 fn two() {}"#
21413 .unindent();
21414 cx.set_state(&original_state);
21415
21416 let mut go_to_definition = cx
21417 .lsp
21418 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21419 move |_, _| async move { Ok(None) },
21420 );
21421 let _references = cx
21422 .lsp
21423 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21424 panic!("Should not call for references with no go to definition fallback")
21425 });
21426
21427 let navigated = cx
21428 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21429 .await
21430 .expect("Failed to navigate to lookup references");
21431 go_to_definition
21432 .next()
21433 .await
21434 .expect("Should have called the go_to_definition handler");
21435
21436 assert_eq!(
21437 navigated,
21438 Navigated::No,
21439 "Should have navigated to references as a fallback after empty GoToDefinition response"
21440 );
21441 cx.assert_editor_state(&original_state);
21442 let editors = cx.update_workspace(|workspace, _, cx| {
21443 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21444 });
21445 cx.update_editor(|_, _, _| {
21446 assert_eq!(
21447 editors.len(),
21448 1,
21449 "After unsuccessful fallback, no other editor should have been opened"
21450 );
21451 });
21452}
21453
21454#[gpui::test]
21455async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21456 init_test(cx, |_| {});
21457 let mut cx = EditorLspTestContext::new_rust(
21458 lsp::ServerCapabilities {
21459 references_provider: Some(lsp::OneOf::Left(true)),
21460 ..lsp::ServerCapabilities::default()
21461 },
21462 cx,
21463 )
21464 .await;
21465
21466 cx.set_state(
21467 &r#"
21468 fn one() {
21469 let mut a = two();
21470 }
21471
21472 fn ˇtwo() {}"#
21473 .unindent(),
21474 );
21475 cx.lsp
21476 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21477 Ok(Some(vec![
21478 lsp::Location {
21479 uri: params.text_document_position.text_document.uri.clone(),
21480 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21481 },
21482 lsp::Location {
21483 uri: params.text_document_position.text_document.uri,
21484 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21485 },
21486 ]))
21487 });
21488 let navigated = cx
21489 .update_editor(|editor, window, cx| {
21490 editor.find_all_references(&FindAllReferences, window, cx)
21491 })
21492 .unwrap()
21493 .await
21494 .expect("Failed to navigate to references");
21495 assert_eq!(
21496 navigated,
21497 Navigated::Yes,
21498 "Should have navigated to references from the FindAllReferences response"
21499 );
21500 cx.assert_editor_state(
21501 &r#"fn one() {
21502 let mut a = two();
21503 }
21504
21505 fn ˇtwo() {}"#
21506 .unindent(),
21507 );
21508
21509 let editors = cx.update_workspace(|workspace, _, cx| {
21510 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21511 });
21512 cx.update_editor(|_, _, _| {
21513 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21514 });
21515
21516 cx.set_state(
21517 &r#"fn one() {
21518 let mut a = ˇtwo();
21519 }
21520
21521 fn two() {}"#
21522 .unindent(),
21523 );
21524 let navigated = cx
21525 .update_editor(|editor, window, cx| {
21526 editor.find_all_references(&FindAllReferences, window, cx)
21527 })
21528 .unwrap()
21529 .await
21530 .expect("Failed to navigate to references");
21531 assert_eq!(
21532 navigated,
21533 Navigated::Yes,
21534 "Should have navigated to references from the FindAllReferences response"
21535 );
21536 cx.assert_editor_state(
21537 &r#"fn one() {
21538 let mut a = ˇtwo();
21539 }
21540
21541 fn two() {}"#
21542 .unindent(),
21543 );
21544 let editors = cx.update_workspace(|workspace, _, cx| {
21545 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21546 });
21547 cx.update_editor(|_, _, _| {
21548 assert_eq!(
21549 editors.len(),
21550 2,
21551 "should have re-used the previous multibuffer"
21552 );
21553 });
21554
21555 cx.set_state(
21556 &r#"fn one() {
21557 let mut a = ˇtwo();
21558 }
21559 fn three() {}
21560 fn two() {}"#
21561 .unindent(),
21562 );
21563 cx.lsp
21564 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21565 Ok(Some(vec![
21566 lsp::Location {
21567 uri: params.text_document_position.text_document.uri.clone(),
21568 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21569 },
21570 lsp::Location {
21571 uri: params.text_document_position.text_document.uri,
21572 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21573 },
21574 ]))
21575 });
21576 let navigated = cx
21577 .update_editor(|editor, window, cx| {
21578 editor.find_all_references(&FindAllReferences, window, cx)
21579 })
21580 .unwrap()
21581 .await
21582 .expect("Failed to navigate to references");
21583 assert_eq!(
21584 navigated,
21585 Navigated::Yes,
21586 "Should have navigated to references from the FindAllReferences response"
21587 );
21588 cx.assert_editor_state(
21589 &r#"fn one() {
21590 let mut a = ˇtwo();
21591 }
21592 fn three() {}
21593 fn two() {}"#
21594 .unindent(),
21595 );
21596 let editors = cx.update_workspace(|workspace, _, cx| {
21597 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21598 });
21599 cx.update_editor(|_, _, _| {
21600 assert_eq!(
21601 editors.len(),
21602 3,
21603 "should have used a new multibuffer as offsets changed"
21604 );
21605 });
21606}
21607#[gpui::test]
21608async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21609 init_test(cx, |_| {});
21610
21611 let language = Arc::new(Language::new(
21612 LanguageConfig::default(),
21613 Some(tree_sitter_rust::LANGUAGE.into()),
21614 ));
21615
21616 let text = r#"
21617 #[cfg(test)]
21618 mod tests() {
21619 #[test]
21620 fn runnable_1() {
21621 let a = 1;
21622 }
21623
21624 #[test]
21625 fn runnable_2() {
21626 let a = 1;
21627 let b = 2;
21628 }
21629 }
21630 "#
21631 .unindent();
21632
21633 let fs = FakeFs::new(cx.executor());
21634 fs.insert_file("/file.rs", Default::default()).await;
21635
21636 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21637 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21638 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21639 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21640 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21641
21642 let editor = cx.new_window_entity(|window, cx| {
21643 Editor::new(
21644 EditorMode::full(),
21645 multi_buffer,
21646 Some(project.clone()),
21647 window,
21648 cx,
21649 )
21650 });
21651
21652 editor.update_in(cx, |editor, window, cx| {
21653 let snapshot = editor.buffer().read(cx).snapshot(cx);
21654 editor.tasks.insert(
21655 (buffer.read(cx).remote_id(), 3),
21656 RunnableTasks {
21657 templates: vec![],
21658 offset: snapshot.anchor_before(43),
21659 column: 0,
21660 extra_variables: HashMap::default(),
21661 context_range: BufferOffset(43)..BufferOffset(85),
21662 },
21663 );
21664 editor.tasks.insert(
21665 (buffer.read(cx).remote_id(), 8),
21666 RunnableTasks {
21667 templates: vec![],
21668 offset: snapshot.anchor_before(86),
21669 column: 0,
21670 extra_variables: HashMap::default(),
21671 context_range: BufferOffset(86)..BufferOffset(191),
21672 },
21673 );
21674
21675 // Test finding task when cursor is inside function body
21676 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21677 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21678 });
21679 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21680 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21681
21682 // Test finding task when cursor is on function name
21683 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21684 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21685 });
21686 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21687 assert_eq!(row, 8, "Should find task when cursor is on function name");
21688 });
21689}
21690
21691#[gpui::test]
21692async fn test_folding_buffers(cx: &mut TestAppContext) {
21693 init_test(cx, |_| {});
21694
21695 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21696 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21697 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21698
21699 let fs = FakeFs::new(cx.executor());
21700 fs.insert_tree(
21701 path!("/a"),
21702 json!({
21703 "first.rs": sample_text_1,
21704 "second.rs": sample_text_2,
21705 "third.rs": sample_text_3,
21706 }),
21707 )
21708 .await;
21709 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21710 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21711 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21712 let worktree = project.update(cx, |project, cx| {
21713 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21714 assert_eq!(worktrees.len(), 1);
21715 worktrees.pop().unwrap()
21716 });
21717 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21718
21719 let buffer_1 = project
21720 .update(cx, |project, cx| {
21721 project.open_buffer((worktree_id, "first.rs"), cx)
21722 })
21723 .await
21724 .unwrap();
21725 let buffer_2 = project
21726 .update(cx, |project, cx| {
21727 project.open_buffer((worktree_id, "second.rs"), cx)
21728 })
21729 .await
21730 .unwrap();
21731 let buffer_3 = project
21732 .update(cx, |project, cx| {
21733 project.open_buffer((worktree_id, "third.rs"), cx)
21734 })
21735 .await
21736 .unwrap();
21737
21738 let multi_buffer = cx.new(|cx| {
21739 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21740 multi_buffer.push_excerpts(
21741 buffer_1.clone(),
21742 [
21743 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21744 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21745 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21746 ],
21747 cx,
21748 );
21749 multi_buffer.push_excerpts(
21750 buffer_2.clone(),
21751 [
21752 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21753 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21754 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21755 ],
21756 cx,
21757 );
21758 multi_buffer.push_excerpts(
21759 buffer_3.clone(),
21760 [
21761 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21762 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21763 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21764 ],
21765 cx,
21766 );
21767 multi_buffer
21768 });
21769 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21770 Editor::new(
21771 EditorMode::full(),
21772 multi_buffer.clone(),
21773 Some(project.clone()),
21774 window,
21775 cx,
21776 )
21777 });
21778
21779 assert_eq!(
21780 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21781 "\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",
21782 );
21783
21784 multi_buffer_editor.update(cx, |editor, cx| {
21785 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21786 });
21787 assert_eq!(
21788 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21789 "\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",
21790 "After folding the first buffer, its text should not be displayed"
21791 );
21792
21793 multi_buffer_editor.update(cx, |editor, cx| {
21794 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21795 });
21796 assert_eq!(
21797 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21798 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21799 "After folding the second buffer, its text should not be displayed"
21800 );
21801
21802 multi_buffer_editor.update(cx, |editor, cx| {
21803 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21804 });
21805 assert_eq!(
21806 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21807 "\n\n\n\n\n",
21808 "After folding the third buffer, its text should not be displayed"
21809 );
21810
21811 // Emulate selection inside the fold logic, that should work
21812 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21813 editor
21814 .snapshot(window, cx)
21815 .next_line_boundary(Point::new(0, 4));
21816 });
21817
21818 multi_buffer_editor.update(cx, |editor, cx| {
21819 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21820 });
21821 assert_eq!(
21822 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21823 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21824 "After unfolding the second buffer, its text should be displayed"
21825 );
21826
21827 // Typing inside of buffer 1 causes that buffer to be unfolded.
21828 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21829 assert_eq!(
21830 multi_buffer
21831 .read(cx)
21832 .snapshot(cx)
21833 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21834 .collect::<String>(),
21835 "bbbb"
21836 );
21837 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21838 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21839 });
21840 editor.handle_input("B", window, cx);
21841 });
21842
21843 assert_eq!(
21844 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21845 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21846 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21847 );
21848
21849 multi_buffer_editor.update(cx, |editor, cx| {
21850 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21851 });
21852 assert_eq!(
21853 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21854 "\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",
21855 "After unfolding the all buffers, all original text should be displayed"
21856 );
21857}
21858
21859#[gpui::test]
21860async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21861 init_test(cx, |_| {});
21862
21863 let sample_text_1 = "1111\n2222\n3333".to_string();
21864 let sample_text_2 = "4444\n5555\n6666".to_string();
21865 let sample_text_3 = "7777\n8888\n9999".to_string();
21866
21867 let fs = FakeFs::new(cx.executor());
21868 fs.insert_tree(
21869 path!("/a"),
21870 json!({
21871 "first.rs": sample_text_1,
21872 "second.rs": sample_text_2,
21873 "third.rs": sample_text_3,
21874 }),
21875 )
21876 .await;
21877 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21878 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21879 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21880 let worktree = project.update(cx, |project, cx| {
21881 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21882 assert_eq!(worktrees.len(), 1);
21883 worktrees.pop().unwrap()
21884 });
21885 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21886
21887 let buffer_1 = project
21888 .update(cx, |project, cx| {
21889 project.open_buffer((worktree_id, "first.rs"), cx)
21890 })
21891 .await
21892 .unwrap();
21893 let buffer_2 = project
21894 .update(cx, |project, cx| {
21895 project.open_buffer((worktree_id, "second.rs"), cx)
21896 })
21897 .await
21898 .unwrap();
21899 let buffer_3 = project
21900 .update(cx, |project, cx| {
21901 project.open_buffer((worktree_id, "third.rs"), cx)
21902 })
21903 .await
21904 .unwrap();
21905
21906 let multi_buffer = cx.new(|cx| {
21907 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21908 multi_buffer.push_excerpts(
21909 buffer_1.clone(),
21910 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21911 cx,
21912 );
21913 multi_buffer.push_excerpts(
21914 buffer_2.clone(),
21915 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21916 cx,
21917 );
21918 multi_buffer.push_excerpts(
21919 buffer_3.clone(),
21920 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21921 cx,
21922 );
21923 multi_buffer
21924 });
21925
21926 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21927 Editor::new(
21928 EditorMode::full(),
21929 multi_buffer,
21930 Some(project.clone()),
21931 window,
21932 cx,
21933 )
21934 });
21935
21936 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21937 assert_eq!(
21938 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21939 full_text,
21940 );
21941
21942 multi_buffer_editor.update(cx, |editor, cx| {
21943 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21944 });
21945 assert_eq!(
21946 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21947 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21948 "After folding the first buffer, its text should not be displayed"
21949 );
21950
21951 multi_buffer_editor.update(cx, |editor, cx| {
21952 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21953 });
21954
21955 assert_eq!(
21956 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21957 "\n\n\n\n\n\n7777\n8888\n9999",
21958 "After folding the second buffer, its text should not be displayed"
21959 );
21960
21961 multi_buffer_editor.update(cx, |editor, cx| {
21962 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21963 });
21964 assert_eq!(
21965 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21966 "\n\n\n\n\n",
21967 "After folding the third buffer, its text should not be displayed"
21968 );
21969
21970 multi_buffer_editor.update(cx, |editor, cx| {
21971 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21972 });
21973 assert_eq!(
21974 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21975 "\n\n\n\n4444\n5555\n6666\n\n",
21976 "After unfolding the second buffer, its text should be displayed"
21977 );
21978
21979 multi_buffer_editor.update(cx, |editor, cx| {
21980 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21981 });
21982 assert_eq!(
21983 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21984 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21985 "After unfolding the first buffer, its text should be displayed"
21986 );
21987
21988 multi_buffer_editor.update(cx, |editor, cx| {
21989 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21990 });
21991 assert_eq!(
21992 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21993 full_text,
21994 "After unfolding all buffers, all original text should be displayed"
21995 );
21996}
21997
21998#[gpui::test]
21999async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22000 init_test(cx, |_| {});
22001
22002 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22003
22004 let fs = FakeFs::new(cx.executor());
22005 fs.insert_tree(
22006 path!("/a"),
22007 json!({
22008 "main.rs": sample_text,
22009 }),
22010 )
22011 .await;
22012 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22013 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22014 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22015 let worktree = project.update(cx, |project, cx| {
22016 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22017 assert_eq!(worktrees.len(), 1);
22018 worktrees.pop().unwrap()
22019 });
22020 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22021
22022 let buffer_1 = project
22023 .update(cx, |project, cx| {
22024 project.open_buffer((worktree_id, "main.rs"), cx)
22025 })
22026 .await
22027 .unwrap();
22028
22029 let multi_buffer = cx.new(|cx| {
22030 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22031 multi_buffer.push_excerpts(
22032 buffer_1.clone(),
22033 [ExcerptRange::new(
22034 Point::new(0, 0)
22035 ..Point::new(
22036 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22037 0,
22038 ),
22039 )],
22040 cx,
22041 );
22042 multi_buffer
22043 });
22044 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22045 Editor::new(
22046 EditorMode::full(),
22047 multi_buffer,
22048 Some(project.clone()),
22049 window,
22050 cx,
22051 )
22052 });
22053
22054 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22055 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22056 enum TestHighlight {}
22057 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22058 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22059 editor.highlight_text::<TestHighlight>(
22060 vec![highlight_range.clone()],
22061 HighlightStyle::color(Hsla::green()),
22062 cx,
22063 );
22064 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22065 s.select_ranges(Some(highlight_range))
22066 });
22067 });
22068
22069 let full_text = format!("\n\n{sample_text}");
22070 assert_eq!(
22071 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22072 full_text,
22073 );
22074}
22075
22076#[gpui::test]
22077async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22078 init_test(cx, |_| {});
22079 cx.update(|cx| {
22080 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22081 "keymaps/default-linux.json",
22082 cx,
22083 )
22084 .unwrap();
22085 cx.bind_keys(default_key_bindings);
22086 });
22087
22088 let (editor, cx) = cx.add_window_view(|window, cx| {
22089 let multi_buffer = MultiBuffer::build_multi(
22090 [
22091 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22092 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22093 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22094 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22095 ],
22096 cx,
22097 );
22098 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22099
22100 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22101 // fold all but the second buffer, so that we test navigating between two
22102 // adjacent folded buffers, as well as folded buffers at the start and
22103 // end the multibuffer
22104 editor.fold_buffer(buffer_ids[0], cx);
22105 editor.fold_buffer(buffer_ids[2], cx);
22106 editor.fold_buffer(buffer_ids[3], cx);
22107
22108 editor
22109 });
22110 cx.simulate_resize(size(px(1000.), px(1000.)));
22111
22112 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22113 cx.assert_excerpts_with_selections(indoc! {"
22114 [EXCERPT]
22115 ˇ[FOLDED]
22116 [EXCERPT]
22117 a1
22118 b1
22119 [EXCERPT]
22120 [FOLDED]
22121 [EXCERPT]
22122 [FOLDED]
22123 "
22124 });
22125 cx.simulate_keystroke("down");
22126 cx.assert_excerpts_with_selections(indoc! {"
22127 [EXCERPT]
22128 [FOLDED]
22129 [EXCERPT]
22130 ˇa1
22131 b1
22132 [EXCERPT]
22133 [FOLDED]
22134 [EXCERPT]
22135 [FOLDED]
22136 "
22137 });
22138 cx.simulate_keystroke("down");
22139 cx.assert_excerpts_with_selections(indoc! {"
22140 [EXCERPT]
22141 [FOLDED]
22142 [EXCERPT]
22143 a1
22144 ˇb1
22145 [EXCERPT]
22146 [FOLDED]
22147 [EXCERPT]
22148 [FOLDED]
22149 "
22150 });
22151 cx.simulate_keystroke("down");
22152 cx.assert_excerpts_with_selections(indoc! {"
22153 [EXCERPT]
22154 [FOLDED]
22155 [EXCERPT]
22156 a1
22157 b1
22158 ˇ[EXCERPT]
22159 [FOLDED]
22160 [EXCERPT]
22161 [FOLDED]
22162 "
22163 });
22164 cx.simulate_keystroke("down");
22165 cx.assert_excerpts_with_selections(indoc! {"
22166 [EXCERPT]
22167 [FOLDED]
22168 [EXCERPT]
22169 a1
22170 b1
22171 [EXCERPT]
22172 ˇ[FOLDED]
22173 [EXCERPT]
22174 [FOLDED]
22175 "
22176 });
22177 for _ in 0..5 {
22178 cx.simulate_keystroke("down");
22179 cx.assert_excerpts_with_selections(indoc! {"
22180 [EXCERPT]
22181 [FOLDED]
22182 [EXCERPT]
22183 a1
22184 b1
22185 [EXCERPT]
22186 [FOLDED]
22187 [EXCERPT]
22188 ˇ[FOLDED]
22189 "
22190 });
22191 }
22192
22193 cx.simulate_keystroke("up");
22194 cx.assert_excerpts_with_selections(indoc! {"
22195 [EXCERPT]
22196 [FOLDED]
22197 [EXCERPT]
22198 a1
22199 b1
22200 [EXCERPT]
22201 ˇ[FOLDED]
22202 [EXCERPT]
22203 [FOLDED]
22204 "
22205 });
22206 cx.simulate_keystroke("up");
22207 cx.assert_excerpts_with_selections(indoc! {"
22208 [EXCERPT]
22209 [FOLDED]
22210 [EXCERPT]
22211 a1
22212 b1
22213 ˇ[EXCERPT]
22214 [FOLDED]
22215 [EXCERPT]
22216 [FOLDED]
22217 "
22218 });
22219 cx.simulate_keystroke("up");
22220 cx.assert_excerpts_with_selections(indoc! {"
22221 [EXCERPT]
22222 [FOLDED]
22223 [EXCERPT]
22224 a1
22225 ˇb1
22226 [EXCERPT]
22227 [FOLDED]
22228 [EXCERPT]
22229 [FOLDED]
22230 "
22231 });
22232 cx.simulate_keystroke("up");
22233 cx.assert_excerpts_with_selections(indoc! {"
22234 [EXCERPT]
22235 [FOLDED]
22236 [EXCERPT]
22237 ˇa1
22238 b1
22239 [EXCERPT]
22240 [FOLDED]
22241 [EXCERPT]
22242 [FOLDED]
22243 "
22244 });
22245 for _ in 0..5 {
22246 cx.simulate_keystroke("up");
22247 cx.assert_excerpts_with_selections(indoc! {"
22248 [EXCERPT]
22249 ˇ[FOLDED]
22250 [EXCERPT]
22251 a1
22252 b1
22253 [EXCERPT]
22254 [FOLDED]
22255 [EXCERPT]
22256 [FOLDED]
22257 "
22258 });
22259 }
22260}
22261
22262#[gpui::test]
22263async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22264 init_test(cx, |_| {});
22265
22266 // Simple insertion
22267 assert_highlighted_edits(
22268 "Hello, world!",
22269 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22270 true,
22271 cx,
22272 |highlighted_edits, cx| {
22273 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22274 assert_eq!(highlighted_edits.highlights.len(), 1);
22275 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22276 assert_eq!(
22277 highlighted_edits.highlights[0].1.background_color,
22278 Some(cx.theme().status().created_background)
22279 );
22280 },
22281 )
22282 .await;
22283
22284 // Replacement
22285 assert_highlighted_edits(
22286 "This is a test.",
22287 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22288 false,
22289 cx,
22290 |highlighted_edits, cx| {
22291 assert_eq!(highlighted_edits.text, "That is a test.");
22292 assert_eq!(highlighted_edits.highlights.len(), 1);
22293 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22294 assert_eq!(
22295 highlighted_edits.highlights[0].1.background_color,
22296 Some(cx.theme().status().created_background)
22297 );
22298 },
22299 )
22300 .await;
22301
22302 // Multiple edits
22303 assert_highlighted_edits(
22304 "Hello, world!",
22305 vec![
22306 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22307 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22308 ],
22309 false,
22310 cx,
22311 |highlighted_edits, cx| {
22312 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22313 assert_eq!(highlighted_edits.highlights.len(), 2);
22314 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22315 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22316 assert_eq!(
22317 highlighted_edits.highlights[0].1.background_color,
22318 Some(cx.theme().status().created_background)
22319 );
22320 assert_eq!(
22321 highlighted_edits.highlights[1].1.background_color,
22322 Some(cx.theme().status().created_background)
22323 );
22324 },
22325 )
22326 .await;
22327
22328 // Multiple lines with edits
22329 assert_highlighted_edits(
22330 "First line\nSecond line\nThird line\nFourth line",
22331 vec![
22332 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22333 (
22334 Point::new(2, 0)..Point::new(2, 10),
22335 "New third line".to_string(),
22336 ),
22337 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22338 ],
22339 false,
22340 cx,
22341 |highlighted_edits, cx| {
22342 assert_eq!(
22343 highlighted_edits.text,
22344 "Second modified\nNew third line\nFourth updated line"
22345 );
22346 assert_eq!(highlighted_edits.highlights.len(), 3);
22347 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22348 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22349 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22350 for highlight in &highlighted_edits.highlights {
22351 assert_eq!(
22352 highlight.1.background_color,
22353 Some(cx.theme().status().created_background)
22354 );
22355 }
22356 },
22357 )
22358 .await;
22359}
22360
22361#[gpui::test]
22362async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22363 init_test(cx, |_| {});
22364
22365 // Deletion
22366 assert_highlighted_edits(
22367 "Hello, world!",
22368 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22369 true,
22370 cx,
22371 |highlighted_edits, cx| {
22372 assert_eq!(highlighted_edits.text, "Hello, world!");
22373 assert_eq!(highlighted_edits.highlights.len(), 1);
22374 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22375 assert_eq!(
22376 highlighted_edits.highlights[0].1.background_color,
22377 Some(cx.theme().status().deleted_background)
22378 );
22379 },
22380 )
22381 .await;
22382
22383 // Insertion
22384 assert_highlighted_edits(
22385 "Hello, world!",
22386 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22387 true,
22388 cx,
22389 |highlighted_edits, cx| {
22390 assert_eq!(highlighted_edits.highlights.len(), 1);
22391 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22392 assert_eq!(
22393 highlighted_edits.highlights[0].1.background_color,
22394 Some(cx.theme().status().created_background)
22395 );
22396 },
22397 )
22398 .await;
22399}
22400
22401async fn assert_highlighted_edits(
22402 text: &str,
22403 edits: Vec<(Range<Point>, String)>,
22404 include_deletions: bool,
22405 cx: &mut TestAppContext,
22406 assertion_fn: impl Fn(HighlightedText, &App),
22407) {
22408 let window = cx.add_window(|window, cx| {
22409 let buffer = MultiBuffer::build_simple(text, cx);
22410 Editor::new(EditorMode::full(), buffer, None, window, cx)
22411 });
22412 let cx = &mut VisualTestContext::from_window(*window, cx);
22413
22414 let (buffer, snapshot) = window
22415 .update(cx, |editor, _window, cx| {
22416 (
22417 editor.buffer().clone(),
22418 editor.buffer().read(cx).snapshot(cx),
22419 )
22420 })
22421 .unwrap();
22422
22423 let edits = edits
22424 .into_iter()
22425 .map(|(range, edit)| {
22426 (
22427 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22428 edit,
22429 )
22430 })
22431 .collect::<Vec<_>>();
22432
22433 let text_anchor_edits = edits
22434 .clone()
22435 .into_iter()
22436 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22437 .collect::<Vec<_>>();
22438
22439 let edit_preview = window
22440 .update(cx, |_, _window, cx| {
22441 buffer
22442 .read(cx)
22443 .as_singleton()
22444 .unwrap()
22445 .read(cx)
22446 .preview_edits(text_anchor_edits.into(), cx)
22447 })
22448 .unwrap()
22449 .await;
22450
22451 cx.update(|_window, cx| {
22452 let highlighted_edits = edit_prediction_edit_text(
22453 snapshot.as_singleton().unwrap().2,
22454 &edits,
22455 &edit_preview,
22456 include_deletions,
22457 cx,
22458 );
22459 assertion_fn(highlighted_edits, cx)
22460 });
22461}
22462
22463#[track_caller]
22464fn assert_breakpoint(
22465 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22466 path: &Arc<Path>,
22467 expected: Vec<(u32, Breakpoint)>,
22468) {
22469 if expected.is_empty() {
22470 assert!(!breakpoints.contains_key(path), "{}", path.display());
22471 } else {
22472 let mut breakpoint = breakpoints
22473 .get(path)
22474 .unwrap()
22475 .iter()
22476 .map(|breakpoint| {
22477 (
22478 breakpoint.row,
22479 Breakpoint {
22480 message: breakpoint.message.clone(),
22481 state: breakpoint.state,
22482 condition: breakpoint.condition.clone(),
22483 hit_condition: breakpoint.hit_condition.clone(),
22484 },
22485 )
22486 })
22487 .collect::<Vec<_>>();
22488
22489 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22490
22491 assert_eq!(expected, breakpoint);
22492 }
22493}
22494
22495fn add_log_breakpoint_at_cursor(
22496 editor: &mut Editor,
22497 log_message: &str,
22498 window: &mut Window,
22499 cx: &mut Context<Editor>,
22500) {
22501 let (anchor, bp) = editor
22502 .breakpoints_at_cursors(window, cx)
22503 .first()
22504 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22505 .unwrap_or_else(|| {
22506 let cursor_position: Point = editor.selections.newest(cx).head();
22507
22508 let breakpoint_position = editor
22509 .snapshot(window, cx)
22510 .display_snapshot
22511 .buffer_snapshot
22512 .anchor_before(Point::new(cursor_position.row, 0));
22513
22514 (breakpoint_position, Breakpoint::new_log(log_message))
22515 });
22516
22517 editor.edit_breakpoint_at_anchor(
22518 anchor,
22519 bp,
22520 BreakpointEditAction::EditLogMessage(log_message.into()),
22521 cx,
22522 );
22523}
22524
22525#[gpui::test]
22526async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22527 init_test(cx, |_| {});
22528
22529 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22530 let fs = FakeFs::new(cx.executor());
22531 fs.insert_tree(
22532 path!("/a"),
22533 json!({
22534 "main.rs": sample_text,
22535 }),
22536 )
22537 .await;
22538 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22539 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22540 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22541
22542 let fs = FakeFs::new(cx.executor());
22543 fs.insert_tree(
22544 path!("/a"),
22545 json!({
22546 "main.rs": sample_text,
22547 }),
22548 )
22549 .await;
22550 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22551 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22552 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22553 let worktree_id = workspace
22554 .update(cx, |workspace, _window, cx| {
22555 workspace.project().update(cx, |project, cx| {
22556 project.worktrees(cx).next().unwrap().read(cx).id()
22557 })
22558 })
22559 .unwrap();
22560
22561 let buffer = project
22562 .update(cx, |project, cx| {
22563 project.open_buffer((worktree_id, "main.rs"), cx)
22564 })
22565 .await
22566 .unwrap();
22567
22568 let (editor, cx) = cx.add_window_view(|window, cx| {
22569 Editor::new(
22570 EditorMode::full(),
22571 MultiBuffer::build_from_buffer(buffer, cx),
22572 Some(project.clone()),
22573 window,
22574 cx,
22575 )
22576 });
22577
22578 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22579 let abs_path = project.read_with(cx, |project, cx| {
22580 project
22581 .absolute_path(&project_path, cx)
22582 .map(Arc::from)
22583 .unwrap()
22584 });
22585
22586 // assert we can add breakpoint on the first line
22587 editor.update_in(cx, |editor, window, cx| {
22588 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22589 editor.move_to_end(&MoveToEnd, window, cx);
22590 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22591 });
22592
22593 let breakpoints = editor.update(cx, |editor, cx| {
22594 editor
22595 .breakpoint_store()
22596 .as_ref()
22597 .unwrap()
22598 .read(cx)
22599 .all_source_breakpoints(cx)
22600 });
22601
22602 assert_eq!(1, breakpoints.len());
22603 assert_breakpoint(
22604 &breakpoints,
22605 &abs_path,
22606 vec![
22607 (0, Breakpoint::new_standard()),
22608 (3, Breakpoint::new_standard()),
22609 ],
22610 );
22611
22612 editor.update_in(cx, |editor, window, cx| {
22613 editor.move_to_beginning(&MoveToBeginning, window, cx);
22614 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22615 });
22616
22617 let breakpoints = editor.update(cx, |editor, cx| {
22618 editor
22619 .breakpoint_store()
22620 .as_ref()
22621 .unwrap()
22622 .read(cx)
22623 .all_source_breakpoints(cx)
22624 });
22625
22626 assert_eq!(1, breakpoints.len());
22627 assert_breakpoint(
22628 &breakpoints,
22629 &abs_path,
22630 vec![(3, Breakpoint::new_standard())],
22631 );
22632
22633 editor.update_in(cx, |editor, window, cx| {
22634 editor.move_to_end(&MoveToEnd, window, cx);
22635 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22636 });
22637
22638 let breakpoints = editor.update(cx, |editor, cx| {
22639 editor
22640 .breakpoint_store()
22641 .as_ref()
22642 .unwrap()
22643 .read(cx)
22644 .all_source_breakpoints(cx)
22645 });
22646
22647 assert_eq!(0, breakpoints.len());
22648 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22649}
22650
22651#[gpui::test]
22652async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22653 init_test(cx, |_| {});
22654
22655 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22656
22657 let fs = FakeFs::new(cx.executor());
22658 fs.insert_tree(
22659 path!("/a"),
22660 json!({
22661 "main.rs": sample_text,
22662 }),
22663 )
22664 .await;
22665 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22666 let (workspace, cx) =
22667 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22668
22669 let worktree_id = workspace.update(cx, |workspace, cx| {
22670 workspace.project().update(cx, |project, cx| {
22671 project.worktrees(cx).next().unwrap().read(cx).id()
22672 })
22673 });
22674
22675 let buffer = project
22676 .update(cx, |project, cx| {
22677 project.open_buffer((worktree_id, "main.rs"), cx)
22678 })
22679 .await
22680 .unwrap();
22681
22682 let (editor, cx) = cx.add_window_view(|window, cx| {
22683 Editor::new(
22684 EditorMode::full(),
22685 MultiBuffer::build_from_buffer(buffer, cx),
22686 Some(project.clone()),
22687 window,
22688 cx,
22689 )
22690 });
22691
22692 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22693 let abs_path = project.read_with(cx, |project, cx| {
22694 project
22695 .absolute_path(&project_path, cx)
22696 .map(Arc::from)
22697 .unwrap()
22698 });
22699
22700 editor.update_in(cx, |editor, window, cx| {
22701 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22702 });
22703
22704 let breakpoints = editor.update(cx, |editor, cx| {
22705 editor
22706 .breakpoint_store()
22707 .as_ref()
22708 .unwrap()
22709 .read(cx)
22710 .all_source_breakpoints(cx)
22711 });
22712
22713 assert_breakpoint(
22714 &breakpoints,
22715 &abs_path,
22716 vec![(0, Breakpoint::new_log("hello world"))],
22717 );
22718
22719 // Removing a log message from a log breakpoint should remove it
22720 editor.update_in(cx, |editor, window, cx| {
22721 add_log_breakpoint_at_cursor(editor, "", window, cx);
22722 });
22723
22724 let breakpoints = editor.update(cx, |editor, cx| {
22725 editor
22726 .breakpoint_store()
22727 .as_ref()
22728 .unwrap()
22729 .read(cx)
22730 .all_source_breakpoints(cx)
22731 });
22732
22733 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22734
22735 editor.update_in(cx, |editor, window, cx| {
22736 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22737 editor.move_to_end(&MoveToEnd, window, cx);
22738 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22739 // Not adding a log message to a standard breakpoint shouldn't remove it
22740 add_log_breakpoint_at_cursor(editor, "", window, cx);
22741 });
22742
22743 let breakpoints = editor.update(cx, |editor, cx| {
22744 editor
22745 .breakpoint_store()
22746 .as_ref()
22747 .unwrap()
22748 .read(cx)
22749 .all_source_breakpoints(cx)
22750 });
22751
22752 assert_breakpoint(
22753 &breakpoints,
22754 &abs_path,
22755 vec![
22756 (0, Breakpoint::new_standard()),
22757 (3, Breakpoint::new_standard()),
22758 ],
22759 );
22760
22761 editor.update_in(cx, |editor, window, cx| {
22762 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22763 });
22764
22765 let breakpoints = editor.update(cx, |editor, cx| {
22766 editor
22767 .breakpoint_store()
22768 .as_ref()
22769 .unwrap()
22770 .read(cx)
22771 .all_source_breakpoints(cx)
22772 });
22773
22774 assert_breakpoint(
22775 &breakpoints,
22776 &abs_path,
22777 vec![
22778 (0, Breakpoint::new_standard()),
22779 (3, Breakpoint::new_log("hello world")),
22780 ],
22781 );
22782
22783 editor.update_in(cx, |editor, window, cx| {
22784 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22785 });
22786
22787 let breakpoints = editor.update(cx, |editor, cx| {
22788 editor
22789 .breakpoint_store()
22790 .as_ref()
22791 .unwrap()
22792 .read(cx)
22793 .all_source_breakpoints(cx)
22794 });
22795
22796 assert_breakpoint(
22797 &breakpoints,
22798 &abs_path,
22799 vec![
22800 (0, Breakpoint::new_standard()),
22801 (3, Breakpoint::new_log("hello Earth!!")),
22802 ],
22803 );
22804}
22805
22806/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22807/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22808/// or when breakpoints were placed out of order. This tests for a regression too
22809#[gpui::test]
22810async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22811 init_test(cx, |_| {});
22812
22813 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22814 let fs = FakeFs::new(cx.executor());
22815 fs.insert_tree(
22816 path!("/a"),
22817 json!({
22818 "main.rs": sample_text,
22819 }),
22820 )
22821 .await;
22822 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22823 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22824 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22825
22826 let fs = FakeFs::new(cx.executor());
22827 fs.insert_tree(
22828 path!("/a"),
22829 json!({
22830 "main.rs": sample_text,
22831 }),
22832 )
22833 .await;
22834 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22835 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22836 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22837 let worktree_id = workspace
22838 .update(cx, |workspace, _window, cx| {
22839 workspace.project().update(cx, |project, cx| {
22840 project.worktrees(cx).next().unwrap().read(cx).id()
22841 })
22842 })
22843 .unwrap();
22844
22845 let buffer = project
22846 .update(cx, |project, cx| {
22847 project.open_buffer((worktree_id, "main.rs"), cx)
22848 })
22849 .await
22850 .unwrap();
22851
22852 let (editor, cx) = cx.add_window_view(|window, cx| {
22853 Editor::new(
22854 EditorMode::full(),
22855 MultiBuffer::build_from_buffer(buffer, cx),
22856 Some(project.clone()),
22857 window,
22858 cx,
22859 )
22860 });
22861
22862 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22863 let abs_path = project.read_with(cx, |project, cx| {
22864 project
22865 .absolute_path(&project_path, cx)
22866 .map(Arc::from)
22867 .unwrap()
22868 });
22869
22870 // assert we can add breakpoint on the first line
22871 editor.update_in(cx, |editor, window, cx| {
22872 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22873 editor.move_to_end(&MoveToEnd, window, cx);
22874 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22875 editor.move_up(&MoveUp, window, cx);
22876 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22877 });
22878
22879 let breakpoints = editor.update(cx, |editor, cx| {
22880 editor
22881 .breakpoint_store()
22882 .as_ref()
22883 .unwrap()
22884 .read(cx)
22885 .all_source_breakpoints(cx)
22886 });
22887
22888 assert_eq!(1, breakpoints.len());
22889 assert_breakpoint(
22890 &breakpoints,
22891 &abs_path,
22892 vec![
22893 (0, Breakpoint::new_standard()),
22894 (2, Breakpoint::new_standard()),
22895 (3, Breakpoint::new_standard()),
22896 ],
22897 );
22898
22899 editor.update_in(cx, |editor, window, cx| {
22900 editor.move_to_beginning(&MoveToBeginning, window, cx);
22901 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22902 editor.move_to_end(&MoveToEnd, window, cx);
22903 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22904 // Disabling a breakpoint that doesn't exist should do nothing
22905 editor.move_up(&MoveUp, window, cx);
22906 editor.move_up(&MoveUp, window, cx);
22907 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22908 });
22909
22910 let breakpoints = editor.update(cx, |editor, cx| {
22911 editor
22912 .breakpoint_store()
22913 .as_ref()
22914 .unwrap()
22915 .read(cx)
22916 .all_source_breakpoints(cx)
22917 });
22918
22919 let disable_breakpoint = {
22920 let mut bp = Breakpoint::new_standard();
22921 bp.state = BreakpointState::Disabled;
22922 bp
22923 };
22924
22925 assert_eq!(1, breakpoints.len());
22926 assert_breakpoint(
22927 &breakpoints,
22928 &abs_path,
22929 vec![
22930 (0, disable_breakpoint.clone()),
22931 (2, Breakpoint::new_standard()),
22932 (3, disable_breakpoint.clone()),
22933 ],
22934 );
22935
22936 editor.update_in(cx, |editor, window, cx| {
22937 editor.move_to_beginning(&MoveToBeginning, window, cx);
22938 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22939 editor.move_to_end(&MoveToEnd, window, cx);
22940 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22941 editor.move_up(&MoveUp, window, cx);
22942 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22943 });
22944
22945 let breakpoints = editor.update(cx, |editor, cx| {
22946 editor
22947 .breakpoint_store()
22948 .as_ref()
22949 .unwrap()
22950 .read(cx)
22951 .all_source_breakpoints(cx)
22952 });
22953
22954 assert_eq!(1, breakpoints.len());
22955 assert_breakpoint(
22956 &breakpoints,
22957 &abs_path,
22958 vec![
22959 (0, Breakpoint::new_standard()),
22960 (2, disable_breakpoint),
22961 (3, Breakpoint::new_standard()),
22962 ],
22963 );
22964}
22965
22966#[gpui::test]
22967async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22968 init_test(cx, |_| {});
22969 let capabilities = lsp::ServerCapabilities {
22970 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22971 prepare_provider: Some(true),
22972 work_done_progress_options: Default::default(),
22973 })),
22974 ..Default::default()
22975 };
22976 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22977
22978 cx.set_state(indoc! {"
22979 struct Fˇoo {}
22980 "});
22981
22982 cx.update_editor(|editor, _, cx| {
22983 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22984 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22985 editor.highlight_background::<DocumentHighlightRead>(
22986 &[highlight_range],
22987 |theme| theme.colors().editor_document_highlight_read_background,
22988 cx,
22989 );
22990 });
22991
22992 let mut prepare_rename_handler = cx
22993 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22994 move |_, _, _| async move {
22995 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22996 start: lsp::Position {
22997 line: 0,
22998 character: 7,
22999 },
23000 end: lsp::Position {
23001 line: 0,
23002 character: 10,
23003 },
23004 })))
23005 },
23006 );
23007 let prepare_rename_task = cx
23008 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23009 .expect("Prepare rename was not started");
23010 prepare_rename_handler.next().await.unwrap();
23011 prepare_rename_task.await.expect("Prepare rename failed");
23012
23013 let mut rename_handler =
23014 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23015 let edit = lsp::TextEdit {
23016 range: lsp::Range {
23017 start: lsp::Position {
23018 line: 0,
23019 character: 7,
23020 },
23021 end: lsp::Position {
23022 line: 0,
23023 character: 10,
23024 },
23025 },
23026 new_text: "FooRenamed".to_string(),
23027 };
23028 Ok(Some(lsp::WorkspaceEdit::new(
23029 // Specify the same edit twice
23030 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23031 )))
23032 });
23033 let rename_task = cx
23034 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23035 .expect("Confirm rename was not started");
23036 rename_handler.next().await.unwrap();
23037 rename_task.await.expect("Confirm rename failed");
23038 cx.run_until_parked();
23039
23040 // Despite two edits, only one is actually applied as those are identical
23041 cx.assert_editor_state(indoc! {"
23042 struct FooRenamedˇ {}
23043 "});
23044}
23045
23046#[gpui::test]
23047async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23048 init_test(cx, |_| {});
23049 // These capabilities indicate that the server does not support prepare rename.
23050 let capabilities = lsp::ServerCapabilities {
23051 rename_provider: Some(lsp::OneOf::Left(true)),
23052 ..Default::default()
23053 };
23054 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23055
23056 cx.set_state(indoc! {"
23057 struct Fˇoo {}
23058 "});
23059
23060 cx.update_editor(|editor, _window, cx| {
23061 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23062 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23063 editor.highlight_background::<DocumentHighlightRead>(
23064 &[highlight_range],
23065 |theme| theme.colors().editor_document_highlight_read_background,
23066 cx,
23067 );
23068 });
23069
23070 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23071 .expect("Prepare rename was not started")
23072 .await
23073 .expect("Prepare rename failed");
23074
23075 let mut rename_handler =
23076 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23077 let edit = lsp::TextEdit {
23078 range: lsp::Range {
23079 start: lsp::Position {
23080 line: 0,
23081 character: 7,
23082 },
23083 end: lsp::Position {
23084 line: 0,
23085 character: 10,
23086 },
23087 },
23088 new_text: "FooRenamed".to_string(),
23089 };
23090 Ok(Some(lsp::WorkspaceEdit::new(
23091 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23092 )))
23093 });
23094 let rename_task = cx
23095 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23096 .expect("Confirm rename was not started");
23097 rename_handler.next().await.unwrap();
23098 rename_task.await.expect("Confirm rename failed");
23099 cx.run_until_parked();
23100
23101 // Correct range is renamed, as `surrounding_word` is used to find it.
23102 cx.assert_editor_state(indoc! {"
23103 struct FooRenamedˇ {}
23104 "});
23105}
23106
23107#[gpui::test]
23108async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23109 init_test(cx, |_| {});
23110 let mut cx = EditorTestContext::new(cx).await;
23111
23112 let language = Arc::new(
23113 Language::new(
23114 LanguageConfig::default(),
23115 Some(tree_sitter_html::LANGUAGE.into()),
23116 )
23117 .with_brackets_query(
23118 r#"
23119 ("<" @open "/>" @close)
23120 ("</" @open ">" @close)
23121 ("<" @open ">" @close)
23122 ("\"" @open "\"" @close)
23123 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23124 "#,
23125 )
23126 .unwrap(),
23127 );
23128 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23129
23130 cx.set_state(indoc! {"
23131 <span>ˇ</span>
23132 "});
23133 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23134 cx.assert_editor_state(indoc! {"
23135 <span>
23136 ˇ
23137 </span>
23138 "});
23139
23140 cx.set_state(indoc! {"
23141 <span><span></span>ˇ</span>
23142 "});
23143 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23144 cx.assert_editor_state(indoc! {"
23145 <span><span></span>
23146 ˇ</span>
23147 "});
23148
23149 cx.set_state(indoc! {"
23150 <span>ˇ
23151 </span>
23152 "});
23153 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23154 cx.assert_editor_state(indoc! {"
23155 <span>
23156 ˇ
23157 </span>
23158 "});
23159}
23160
23161#[gpui::test(iterations = 10)]
23162async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23163 init_test(cx, |_| {});
23164
23165 let fs = FakeFs::new(cx.executor());
23166 fs.insert_tree(
23167 path!("/dir"),
23168 json!({
23169 "a.ts": "a",
23170 }),
23171 )
23172 .await;
23173
23174 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23175 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23176 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23177
23178 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23179 language_registry.add(Arc::new(Language::new(
23180 LanguageConfig {
23181 name: "TypeScript".into(),
23182 matcher: LanguageMatcher {
23183 path_suffixes: vec!["ts".to_string()],
23184 ..Default::default()
23185 },
23186 ..Default::default()
23187 },
23188 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23189 )));
23190 let mut fake_language_servers = language_registry.register_fake_lsp(
23191 "TypeScript",
23192 FakeLspAdapter {
23193 capabilities: lsp::ServerCapabilities {
23194 code_lens_provider: Some(lsp::CodeLensOptions {
23195 resolve_provider: Some(true),
23196 }),
23197 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23198 commands: vec!["_the/command".to_string()],
23199 ..lsp::ExecuteCommandOptions::default()
23200 }),
23201 ..lsp::ServerCapabilities::default()
23202 },
23203 ..FakeLspAdapter::default()
23204 },
23205 );
23206
23207 let editor = workspace
23208 .update(cx, |workspace, window, cx| {
23209 workspace.open_abs_path(
23210 PathBuf::from(path!("/dir/a.ts")),
23211 OpenOptions::default(),
23212 window,
23213 cx,
23214 )
23215 })
23216 .unwrap()
23217 .await
23218 .unwrap()
23219 .downcast::<Editor>()
23220 .unwrap();
23221 cx.executor().run_until_parked();
23222
23223 let fake_server = fake_language_servers.next().await.unwrap();
23224
23225 let buffer = editor.update(cx, |editor, cx| {
23226 editor
23227 .buffer()
23228 .read(cx)
23229 .as_singleton()
23230 .expect("have opened a single file by path")
23231 });
23232
23233 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23234 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23235 drop(buffer_snapshot);
23236 let actions = cx
23237 .update_window(*workspace, |_, window, cx| {
23238 project.code_actions(&buffer, anchor..anchor, window, cx)
23239 })
23240 .unwrap();
23241
23242 fake_server
23243 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23244 Ok(Some(vec![
23245 lsp::CodeLens {
23246 range: lsp::Range::default(),
23247 command: Some(lsp::Command {
23248 title: "Code lens command".to_owned(),
23249 command: "_the/command".to_owned(),
23250 arguments: None,
23251 }),
23252 data: None,
23253 },
23254 lsp::CodeLens {
23255 range: lsp::Range::default(),
23256 command: Some(lsp::Command {
23257 title: "Command not in capabilities".to_owned(),
23258 command: "not in capabilities".to_owned(),
23259 arguments: None,
23260 }),
23261 data: None,
23262 },
23263 lsp::CodeLens {
23264 range: lsp::Range {
23265 start: lsp::Position {
23266 line: 1,
23267 character: 1,
23268 },
23269 end: lsp::Position {
23270 line: 1,
23271 character: 1,
23272 },
23273 },
23274 command: Some(lsp::Command {
23275 title: "Command not in range".to_owned(),
23276 command: "_the/command".to_owned(),
23277 arguments: None,
23278 }),
23279 data: None,
23280 },
23281 ]))
23282 })
23283 .next()
23284 .await;
23285
23286 let actions = actions.await.unwrap();
23287 assert_eq!(
23288 actions.len(),
23289 1,
23290 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23291 );
23292 let action = actions[0].clone();
23293 let apply = project.update(cx, |project, cx| {
23294 project.apply_code_action(buffer.clone(), action, true, cx)
23295 });
23296
23297 // Resolving the code action does not populate its edits. In absence of
23298 // edits, we must execute the given command.
23299 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23300 |mut lens, _| async move {
23301 let lens_command = lens.command.as_mut().expect("should have a command");
23302 assert_eq!(lens_command.title, "Code lens command");
23303 lens_command.arguments = Some(vec![json!("the-argument")]);
23304 Ok(lens)
23305 },
23306 );
23307
23308 // While executing the command, the language server sends the editor
23309 // a `workspaceEdit` request.
23310 fake_server
23311 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23312 let fake = fake_server.clone();
23313 move |params, _| {
23314 assert_eq!(params.command, "_the/command");
23315 let fake = fake.clone();
23316 async move {
23317 fake.server
23318 .request::<lsp::request::ApplyWorkspaceEdit>(
23319 lsp::ApplyWorkspaceEditParams {
23320 label: None,
23321 edit: lsp::WorkspaceEdit {
23322 changes: Some(
23323 [(
23324 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23325 vec![lsp::TextEdit {
23326 range: lsp::Range::new(
23327 lsp::Position::new(0, 0),
23328 lsp::Position::new(0, 0),
23329 ),
23330 new_text: "X".into(),
23331 }],
23332 )]
23333 .into_iter()
23334 .collect(),
23335 ),
23336 ..lsp::WorkspaceEdit::default()
23337 },
23338 },
23339 )
23340 .await
23341 .into_response()
23342 .unwrap();
23343 Ok(Some(json!(null)))
23344 }
23345 }
23346 })
23347 .next()
23348 .await;
23349
23350 // Applying the code lens command returns a project transaction containing the edits
23351 // sent by the language server in its `workspaceEdit` request.
23352 let transaction = apply.await.unwrap();
23353 assert!(transaction.0.contains_key(&buffer));
23354 buffer.update(cx, |buffer, cx| {
23355 assert_eq!(buffer.text(), "Xa");
23356 buffer.undo(cx);
23357 assert_eq!(buffer.text(), "a");
23358 });
23359
23360 let actions_after_edits = cx
23361 .update_window(*workspace, |_, window, cx| {
23362 project.code_actions(&buffer, anchor..anchor, window, cx)
23363 })
23364 .unwrap()
23365 .await
23366 .unwrap();
23367 assert_eq!(
23368 actions, actions_after_edits,
23369 "For the same selection, same code lens actions should be returned"
23370 );
23371
23372 let _responses =
23373 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23374 panic!("No more code lens requests are expected");
23375 });
23376 editor.update_in(cx, |editor, window, cx| {
23377 editor.select_all(&SelectAll, window, cx);
23378 });
23379 cx.executor().run_until_parked();
23380 let new_actions = cx
23381 .update_window(*workspace, |_, window, cx| {
23382 project.code_actions(&buffer, anchor..anchor, window, cx)
23383 })
23384 .unwrap()
23385 .await
23386 .unwrap();
23387 assert_eq!(
23388 actions, new_actions,
23389 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23390 );
23391}
23392
23393#[gpui::test]
23394async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23395 init_test(cx, |_| {});
23396
23397 let fs = FakeFs::new(cx.executor());
23398 let main_text = r#"fn main() {
23399println!("1");
23400println!("2");
23401println!("3");
23402println!("4");
23403println!("5");
23404}"#;
23405 let lib_text = "mod foo {}";
23406 fs.insert_tree(
23407 path!("/a"),
23408 json!({
23409 "lib.rs": lib_text,
23410 "main.rs": main_text,
23411 }),
23412 )
23413 .await;
23414
23415 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23416 let (workspace, cx) =
23417 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23418 let worktree_id = workspace.update(cx, |workspace, cx| {
23419 workspace.project().update(cx, |project, cx| {
23420 project.worktrees(cx).next().unwrap().read(cx).id()
23421 })
23422 });
23423
23424 let expected_ranges = vec![
23425 Point::new(0, 0)..Point::new(0, 0),
23426 Point::new(1, 0)..Point::new(1, 1),
23427 Point::new(2, 0)..Point::new(2, 2),
23428 Point::new(3, 0)..Point::new(3, 3),
23429 ];
23430
23431 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23432 let editor_1 = workspace
23433 .update_in(cx, |workspace, window, cx| {
23434 workspace.open_path(
23435 (worktree_id, "main.rs"),
23436 Some(pane_1.downgrade()),
23437 true,
23438 window,
23439 cx,
23440 )
23441 })
23442 .unwrap()
23443 .await
23444 .downcast::<Editor>()
23445 .unwrap();
23446 pane_1.update(cx, |pane, cx| {
23447 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23448 open_editor.update(cx, |editor, cx| {
23449 assert_eq!(
23450 editor.display_text(cx),
23451 main_text,
23452 "Original main.rs text on initial open",
23453 );
23454 assert_eq!(
23455 editor
23456 .selections
23457 .all::<Point>(cx)
23458 .into_iter()
23459 .map(|s| s.range())
23460 .collect::<Vec<_>>(),
23461 vec![Point::zero()..Point::zero()],
23462 "Default selections on initial open",
23463 );
23464 })
23465 });
23466 editor_1.update_in(cx, |editor, window, cx| {
23467 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23468 s.select_ranges(expected_ranges.clone());
23469 });
23470 });
23471
23472 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23473 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23474 });
23475 let editor_2 = workspace
23476 .update_in(cx, |workspace, window, cx| {
23477 workspace.open_path(
23478 (worktree_id, "main.rs"),
23479 Some(pane_2.downgrade()),
23480 true,
23481 window,
23482 cx,
23483 )
23484 })
23485 .unwrap()
23486 .await
23487 .downcast::<Editor>()
23488 .unwrap();
23489 pane_2.update(cx, |pane, cx| {
23490 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23491 open_editor.update(cx, |editor, cx| {
23492 assert_eq!(
23493 editor.display_text(cx),
23494 main_text,
23495 "Original main.rs text on initial open in another panel",
23496 );
23497 assert_eq!(
23498 editor
23499 .selections
23500 .all::<Point>(cx)
23501 .into_iter()
23502 .map(|s| s.range())
23503 .collect::<Vec<_>>(),
23504 vec![Point::zero()..Point::zero()],
23505 "Default selections on initial open in another panel",
23506 );
23507 })
23508 });
23509
23510 editor_2.update_in(cx, |editor, window, cx| {
23511 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23512 });
23513
23514 let _other_editor_1 = workspace
23515 .update_in(cx, |workspace, window, cx| {
23516 workspace.open_path(
23517 (worktree_id, "lib.rs"),
23518 Some(pane_1.downgrade()),
23519 true,
23520 window,
23521 cx,
23522 )
23523 })
23524 .unwrap()
23525 .await
23526 .downcast::<Editor>()
23527 .unwrap();
23528 pane_1
23529 .update_in(cx, |pane, window, cx| {
23530 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23531 })
23532 .await
23533 .unwrap();
23534 drop(editor_1);
23535 pane_1.update(cx, |pane, cx| {
23536 pane.active_item()
23537 .unwrap()
23538 .downcast::<Editor>()
23539 .unwrap()
23540 .update(cx, |editor, cx| {
23541 assert_eq!(
23542 editor.display_text(cx),
23543 lib_text,
23544 "Other file should be open and active",
23545 );
23546 });
23547 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23548 });
23549
23550 let _other_editor_2 = workspace
23551 .update_in(cx, |workspace, window, cx| {
23552 workspace.open_path(
23553 (worktree_id, "lib.rs"),
23554 Some(pane_2.downgrade()),
23555 true,
23556 window,
23557 cx,
23558 )
23559 })
23560 .unwrap()
23561 .await
23562 .downcast::<Editor>()
23563 .unwrap();
23564 pane_2
23565 .update_in(cx, |pane, window, cx| {
23566 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23567 })
23568 .await
23569 .unwrap();
23570 drop(editor_2);
23571 pane_2.update(cx, |pane, cx| {
23572 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23573 open_editor.update(cx, |editor, cx| {
23574 assert_eq!(
23575 editor.display_text(cx),
23576 lib_text,
23577 "Other file should be open and active in another panel too",
23578 );
23579 });
23580 assert_eq!(
23581 pane.items().count(),
23582 1,
23583 "No other editors should be open in another pane",
23584 );
23585 });
23586
23587 let _editor_1_reopened = workspace
23588 .update_in(cx, |workspace, window, cx| {
23589 workspace.open_path(
23590 (worktree_id, "main.rs"),
23591 Some(pane_1.downgrade()),
23592 true,
23593 window,
23594 cx,
23595 )
23596 })
23597 .unwrap()
23598 .await
23599 .downcast::<Editor>()
23600 .unwrap();
23601 let _editor_2_reopened = workspace
23602 .update_in(cx, |workspace, window, cx| {
23603 workspace.open_path(
23604 (worktree_id, "main.rs"),
23605 Some(pane_2.downgrade()),
23606 true,
23607 window,
23608 cx,
23609 )
23610 })
23611 .unwrap()
23612 .await
23613 .downcast::<Editor>()
23614 .unwrap();
23615 pane_1.update(cx, |pane, cx| {
23616 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23617 open_editor.update(cx, |editor, cx| {
23618 assert_eq!(
23619 editor.display_text(cx),
23620 main_text,
23621 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23622 );
23623 assert_eq!(
23624 editor
23625 .selections
23626 .all::<Point>(cx)
23627 .into_iter()
23628 .map(|s| s.range())
23629 .collect::<Vec<_>>(),
23630 expected_ranges,
23631 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23632 );
23633 })
23634 });
23635 pane_2.update(cx, |pane, cx| {
23636 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23637 open_editor.update(cx, |editor, cx| {
23638 assert_eq!(
23639 editor.display_text(cx),
23640 r#"fn main() {
23641⋯rintln!("1");
23642⋯intln!("2");
23643⋯ntln!("3");
23644println!("4");
23645println!("5");
23646}"#,
23647 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23648 );
23649 assert_eq!(
23650 editor
23651 .selections
23652 .all::<Point>(cx)
23653 .into_iter()
23654 .map(|s| s.range())
23655 .collect::<Vec<_>>(),
23656 vec![Point::zero()..Point::zero()],
23657 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23658 );
23659 })
23660 });
23661}
23662
23663#[gpui::test]
23664async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23665 init_test(cx, |_| {});
23666
23667 let fs = FakeFs::new(cx.executor());
23668 let main_text = r#"fn main() {
23669println!("1");
23670println!("2");
23671println!("3");
23672println!("4");
23673println!("5");
23674}"#;
23675 let lib_text = "mod foo {}";
23676 fs.insert_tree(
23677 path!("/a"),
23678 json!({
23679 "lib.rs": lib_text,
23680 "main.rs": main_text,
23681 }),
23682 )
23683 .await;
23684
23685 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23686 let (workspace, cx) =
23687 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23688 let worktree_id = workspace.update(cx, |workspace, cx| {
23689 workspace.project().update(cx, |project, cx| {
23690 project.worktrees(cx).next().unwrap().read(cx).id()
23691 })
23692 });
23693
23694 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23695 let editor = workspace
23696 .update_in(cx, |workspace, window, cx| {
23697 workspace.open_path(
23698 (worktree_id, "main.rs"),
23699 Some(pane.downgrade()),
23700 true,
23701 window,
23702 cx,
23703 )
23704 })
23705 .unwrap()
23706 .await
23707 .downcast::<Editor>()
23708 .unwrap();
23709 pane.update(cx, |pane, cx| {
23710 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23711 open_editor.update(cx, |editor, cx| {
23712 assert_eq!(
23713 editor.display_text(cx),
23714 main_text,
23715 "Original main.rs text on initial open",
23716 );
23717 })
23718 });
23719 editor.update_in(cx, |editor, window, cx| {
23720 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23721 });
23722
23723 cx.update_global(|store: &mut SettingsStore, cx| {
23724 store.update_user_settings(cx, |s| {
23725 s.workspace.restore_on_file_reopen = Some(false);
23726 });
23727 });
23728 editor.update_in(cx, |editor, window, cx| {
23729 editor.fold_ranges(
23730 vec![
23731 Point::new(1, 0)..Point::new(1, 1),
23732 Point::new(2, 0)..Point::new(2, 2),
23733 Point::new(3, 0)..Point::new(3, 3),
23734 ],
23735 false,
23736 window,
23737 cx,
23738 );
23739 });
23740 pane.update_in(cx, |pane, window, cx| {
23741 pane.close_all_items(&CloseAllItems::default(), window, cx)
23742 })
23743 .await
23744 .unwrap();
23745 pane.update(cx, |pane, _| {
23746 assert!(pane.active_item().is_none());
23747 });
23748 cx.update_global(|store: &mut SettingsStore, cx| {
23749 store.update_user_settings(cx, |s| {
23750 s.workspace.restore_on_file_reopen = Some(true);
23751 });
23752 });
23753
23754 let _editor_reopened = workspace
23755 .update_in(cx, |workspace, window, cx| {
23756 workspace.open_path(
23757 (worktree_id, "main.rs"),
23758 Some(pane.downgrade()),
23759 true,
23760 window,
23761 cx,
23762 )
23763 })
23764 .unwrap()
23765 .await
23766 .downcast::<Editor>()
23767 .unwrap();
23768 pane.update(cx, |pane, cx| {
23769 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23770 open_editor.update(cx, |editor, cx| {
23771 assert_eq!(
23772 editor.display_text(cx),
23773 main_text,
23774 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23775 );
23776 })
23777 });
23778}
23779
23780#[gpui::test]
23781async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23782 struct EmptyModalView {
23783 focus_handle: gpui::FocusHandle,
23784 }
23785 impl EventEmitter<DismissEvent> for EmptyModalView {}
23786 impl Render for EmptyModalView {
23787 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23788 div()
23789 }
23790 }
23791 impl Focusable for EmptyModalView {
23792 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23793 self.focus_handle.clone()
23794 }
23795 }
23796 impl workspace::ModalView for EmptyModalView {}
23797 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23798 EmptyModalView {
23799 focus_handle: cx.focus_handle(),
23800 }
23801 }
23802
23803 init_test(cx, |_| {});
23804
23805 let fs = FakeFs::new(cx.executor());
23806 let project = Project::test(fs, [], cx).await;
23807 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23808 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23809 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23810 let editor = cx.new_window_entity(|window, cx| {
23811 Editor::new(
23812 EditorMode::full(),
23813 buffer,
23814 Some(project.clone()),
23815 window,
23816 cx,
23817 )
23818 });
23819 workspace
23820 .update(cx, |workspace, window, cx| {
23821 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23822 })
23823 .unwrap();
23824 editor.update_in(cx, |editor, window, cx| {
23825 editor.open_context_menu(&OpenContextMenu, window, cx);
23826 assert!(editor.mouse_context_menu.is_some());
23827 });
23828 workspace
23829 .update(cx, |workspace, window, cx| {
23830 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23831 })
23832 .unwrap();
23833 cx.read(|cx| {
23834 assert!(editor.read(cx).mouse_context_menu.is_none());
23835 });
23836}
23837
23838fn set_linked_edit_ranges(
23839 opening: (Point, Point),
23840 closing: (Point, Point),
23841 editor: &mut Editor,
23842 cx: &mut Context<Editor>,
23843) {
23844 let Some((buffer, _)) = editor
23845 .buffer
23846 .read(cx)
23847 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23848 else {
23849 panic!("Failed to get buffer for selection position");
23850 };
23851 let buffer = buffer.read(cx);
23852 let buffer_id = buffer.remote_id();
23853 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
23854 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
23855 let mut linked_ranges = HashMap::default();
23856 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23857 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23858}
23859
23860#[gpui::test]
23861async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23862 init_test(cx, |_| {});
23863
23864 let fs = FakeFs::new(cx.executor());
23865 fs.insert_file(path!("/file.html"), Default::default())
23866 .await;
23867
23868 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23869
23870 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23871 let html_language = Arc::new(Language::new(
23872 LanguageConfig {
23873 name: "HTML".into(),
23874 matcher: LanguageMatcher {
23875 path_suffixes: vec!["html".to_string()],
23876 ..LanguageMatcher::default()
23877 },
23878 brackets: BracketPairConfig {
23879 pairs: vec![BracketPair {
23880 start: "<".into(),
23881 end: ">".into(),
23882 close: true,
23883 ..Default::default()
23884 }],
23885 ..Default::default()
23886 },
23887 ..Default::default()
23888 },
23889 Some(tree_sitter_html::LANGUAGE.into()),
23890 ));
23891 language_registry.add(html_language);
23892 let mut fake_servers = language_registry.register_fake_lsp(
23893 "HTML",
23894 FakeLspAdapter {
23895 capabilities: lsp::ServerCapabilities {
23896 completion_provider: Some(lsp::CompletionOptions {
23897 resolve_provider: Some(true),
23898 ..Default::default()
23899 }),
23900 ..Default::default()
23901 },
23902 ..Default::default()
23903 },
23904 );
23905
23906 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23907 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23908
23909 let worktree_id = workspace
23910 .update(cx, |workspace, _window, cx| {
23911 workspace.project().update(cx, |project, cx| {
23912 project.worktrees(cx).next().unwrap().read(cx).id()
23913 })
23914 })
23915 .unwrap();
23916 project
23917 .update(cx, |project, cx| {
23918 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23919 })
23920 .await
23921 .unwrap();
23922 let editor = workspace
23923 .update(cx, |workspace, window, cx| {
23924 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23925 })
23926 .unwrap()
23927 .await
23928 .unwrap()
23929 .downcast::<Editor>()
23930 .unwrap();
23931
23932 let fake_server = fake_servers.next().await.unwrap();
23933 editor.update_in(cx, |editor, window, cx| {
23934 editor.set_text("<ad></ad>", window, cx);
23935 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23936 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23937 });
23938 set_linked_edit_ranges(
23939 (Point::new(0, 1), Point::new(0, 3)),
23940 (Point::new(0, 6), Point::new(0, 8)),
23941 editor,
23942 cx,
23943 );
23944 });
23945 let mut completion_handle =
23946 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23947 Ok(Some(lsp::CompletionResponse::Array(vec![
23948 lsp::CompletionItem {
23949 label: "head".to_string(),
23950 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23951 lsp::InsertReplaceEdit {
23952 new_text: "head".to_string(),
23953 insert: lsp::Range::new(
23954 lsp::Position::new(0, 1),
23955 lsp::Position::new(0, 3),
23956 ),
23957 replace: lsp::Range::new(
23958 lsp::Position::new(0, 1),
23959 lsp::Position::new(0, 3),
23960 ),
23961 },
23962 )),
23963 ..Default::default()
23964 },
23965 ])))
23966 });
23967 editor.update_in(cx, |editor, window, cx| {
23968 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23969 });
23970 cx.run_until_parked();
23971 completion_handle.next().await.unwrap();
23972 editor.update(cx, |editor, _| {
23973 assert!(
23974 editor.context_menu_visible(),
23975 "Completion menu should be visible"
23976 );
23977 });
23978 editor.update_in(cx, |editor, window, cx| {
23979 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23980 });
23981 cx.executor().run_until_parked();
23982 editor.update(cx, |editor, cx| {
23983 assert_eq!(editor.text(cx), "<head></head>");
23984 });
23985}
23986
23987#[gpui::test]
23988async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
23989 init_test(cx, |_| {});
23990
23991 let mut cx = EditorTestContext::new(cx).await;
23992 let language = Arc::new(Language::new(
23993 LanguageConfig {
23994 name: "TSX".into(),
23995 matcher: LanguageMatcher {
23996 path_suffixes: vec!["tsx".to_string()],
23997 ..LanguageMatcher::default()
23998 },
23999 brackets: BracketPairConfig {
24000 pairs: vec![BracketPair {
24001 start: "<".into(),
24002 end: ">".into(),
24003 close: true,
24004 ..Default::default()
24005 }],
24006 ..Default::default()
24007 },
24008 linked_edit_characters: HashSet::from_iter(['.']),
24009 ..Default::default()
24010 },
24011 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24012 ));
24013 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24014
24015 // Test typing > does not extend linked pair
24016 cx.set_state("<divˇ<div></div>");
24017 cx.update_editor(|editor, _, cx| {
24018 set_linked_edit_ranges(
24019 (Point::new(0, 1), Point::new(0, 4)),
24020 (Point::new(0, 11), Point::new(0, 14)),
24021 editor,
24022 cx,
24023 );
24024 });
24025 cx.update_editor(|editor, window, cx| {
24026 editor.handle_input(">", window, cx);
24027 });
24028 cx.assert_editor_state("<div>ˇ<div></div>");
24029
24030 // Test typing . do extend linked pair
24031 cx.set_state("<Animatedˇ></Animated>");
24032 cx.update_editor(|editor, _, cx| {
24033 set_linked_edit_ranges(
24034 (Point::new(0, 1), Point::new(0, 9)),
24035 (Point::new(0, 12), Point::new(0, 20)),
24036 editor,
24037 cx,
24038 );
24039 });
24040 cx.update_editor(|editor, window, cx| {
24041 editor.handle_input(".", window, cx);
24042 });
24043 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24044 cx.update_editor(|editor, _, cx| {
24045 set_linked_edit_ranges(
24046 (Point::new(0, 1), Point::new(0, 10)),
24047 (Point::new(0, 13), Point::new(0, 21)),
24048 editor,
24049 cx,
24050 );
24051 });
24052 cx.update_editor(|editor, window, cx| {
24053 editor.handle_input("V", window, cx);
24054 });
24055 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24056}
24057
24058#[gpui::test]
24059async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24060 init_test(cx, |_| {});
24061
24062 let fs = FakeFs::new(cx.executor());
24063 fs.insert_tree(
24064 path!("/root"),
24065 json!({
24066 "a": {
24067 "main.rs": "fn main() {}",
24068 },
24069 "foo": {
24070 "bar": {
24071 "external_file.rs": "pub mod external {}",
24072 }
24073 }
24074 }),
24075 )
24076 .await;
24077
24078 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24079 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24080 language_registry.add(rust_lang());
24081 let _fake_servers = language_registry.register_fake_lsp(
24082 "Rust",
24083 FakeLspAdapter {
24084 ..FakeLspAdapter::default()
24085 },
24086 );
24087 let (workspace, cx) =
24088 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24089 let worktree_id = workspace.update(cx, |workspace, cx| {
24090 workspace.project().update(cx, |project, cx| {
24091 project.worktrees(cx).next().unwrap().read(cx).id()
24092 })
24093 });
24094
24095 let assert_language_servers_count =
24096 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24097 project.update(cx, |project, cx| {
24098 let current = project
24099 .lsp_store()
24100 .read(cx)
24101 .as_local()
24102 .unwrap()
24103 .language_servers
24104 .len();
24105 assert_eq!(expected, current, "{context}");
24106 });
24107 };
24108
24109 assert_language_servers_count(
24110 0,
24111 "No servers should be running before any file is open",
24112 cx,
24113 );
24114 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24115 let main_editor = workspace
24116 .update_in(cx, |workspace, window, cx| {
24117 workspace.open_path(
24118 (worktree_id, "main.rs"),
24119 Some(pane.downgrade()),
24120 true,
24121 window,
24122 cx,
24123 )
24124 })
24125 .unwrap()
24126 .await
24127 .downcast::<Editor>()
24128 .unwrap();
24129 pane.update(cx, |pane, cx| {
24130 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24131 open_editor.update(cx, |editor, cx| {
24132 assert_eq!(
24133 editor.display_text(cx),
24134 "fn main() {}",
24135 "Original main.rs text on initial open",
24136 );
24137 });
24138 assert_eq!(open_editor, main_editor);
24139 });
24140 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24141
24142 let external_editor = workspace
24143 .update_in(cx, |workspace, window, cx| {
24144 workspace.open_abs_path(
24145 PathBuf::from("/root/foo/bar/external_file.rs"),
24146 OpenOptions::default(),
24147 window,
24148 cx,
24149 )
24150 })
24151 .await
24152 .expect("opening external file")
24153 .downcast::<Editor>()
24154 .expect("downcasted external file's open element to editor");
24155 pane.update(cx, |pane, cx| {
24156 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24157 open_editor.update(cx, |editor, cx| {
24158 assert_eq!(
24159 editor.display_text(cx),
24160 "pub mod external {}",
24161 "External file is open now",
24162 );
24163 });
24164 assert_eq!(open_editor, external_editor);
24165 });
24166 assert_language_servers_count(
24167 1,
24168 "Second, external, *.rs file should join the existing server",
24169 cx,
24170 );
24171
24172 pane.update_in(cx, |pane, window, cx| {
24173 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24174 })
24175 .await
24176 .unwrap();
24177 pane.update_in(cx, |pane, window, cx| {
24178 pane.navigate_backward(&Default::default(), window, cx);
24179 });
24180 cx.run_until_parked();
24181 pane.update(cx, |pane, cx| {
24182 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24183 open_editor.update(cx, |editor, cx| {
24184 assert_eq!(
24185 editor.display_text(cx),
24186 "pub mod external {}",
24187 "External file is open now",
24188 );
24189 });
24190 });
24191 assert_language_servers_count(
24192 1,
24193 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24194 cx,
24195 );
24196
24197 cx.update(|_, cx| {
24198 workspace::reload(cx);
24199 });
24200 assert_language_servers_count(
24201 1,
24202 "After reloading the worktree with local and external files opened, only one project should be started",
24203 cx,
24204 );
24205}
24206
24207#[gpui::test]
24208async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24209 init_test(cx, |_| {});
24210
24211 let mut cx = EditorTestContext::new(cx).await;
24212 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24213 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24214
24215 // test cursor move to start of each line on tab
24216 // for `if`, `elif`, `else`, `while`, `with` and `for`
24217 cx.set_state(indoc! {"
24218 def main():
24219 ˇ for item in items:
24220 ˇ while item.active:
24221 ˇ if item.value > 10:
24222 ˇ continue
24223 ˇ elif item.value < 0:
24224 ˇ break
24225 ˇ else:
24226 ˇ with item.context() as ctx:
24227 ˇ yield count
24228 ˇ else:
24229 ˇ log('while else')
24230 ˇ else:
24231 ˇ log('for else')
24232 "});
24233 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24234 cx.assert_editor_state(indoc! {"
24235 def main():
24236 ˇfor item in items:
24237 ˇwhile item.active:
24238 ˇif item.value > 10:
24239 ˇcontinue
24240 ˇelif item.value < 0:
24241 ˇbreak
24242 ˇelse:
24243 ˇwith item.context() as ctx:
24244 ˇyield count
24245 ˇelse:
24246 ˇlog('while else')
24247 ˇelse:
24248 ˇlog('for else')
24249 "});
24250 // test relative indent is preserved when tab
24251 // for `if`, `elif`, `else`, `while`, `with` and `for`
24252 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24253 cx.assert_editor_state(indoc! {"
24254 def main():
24255 ˇfor item in items:
24256 ˇwhile item.active:
24257 ˇif item.value > 10:
24258 ˇcontinue
24259 ˇelif item.value < 0:
24260 ˇbreak
24261 ˇelse:
24262 ˇwith item.context() as ctx:
24263 ˇyield count
24264 ˇelse:
24265 ˇlog('while else')
24266 ˇelse:
24267 ˇlog('for else')
24268 "});
24269
24270 // test cursor move to start of each line on tab
24271 // for `try`, `except`, `else`, `finally`, `match` and `def`
24272 cx.set_state(indoc! {"
24273 def main():
24274 ˇ try:
24275 ˇ fetch()
24276 ˇ except ValueError:
24277 ˇ handle_error()
24278 ˇ else:
24279 ˇ match value:
24280 ˇ case _:
24281 ˇ finally:
24282 ˇ def status():
24283 ˇ return 0
24284 "});
24285 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24286 cx.assert_editor_state(indoc! {"
24287 def main():
24288 ˇtry:
24289 ˇfetch()
24290 ˇexcept ValueError:
24291 ˇhandle_error()
24292 ˇelse:
24293 ˇmatch value:
24294 ˇcase _:
24295 ˇfinally:
24296 ˇdef status():
24297 ˇreturn 0
24298 "});
24299 // test relative indent is preserved when tab
24300 // for `try`, `except`, `else`, `finally`, `match` and `def`
24301 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24302 cx.assert_editor_state(indoc! {"
24303 def main():
24304 ˇtry:
24305 ˇfetch()
24306 ˇexcept ValueError:
24307 ˇhandle_error()
24308 ˇelse:
24309 ˇmatch value:
24310 ˇcase _:
24311 ˇfinally:
24312 ˇdef status():
24313 ˇreturn 0
24314 "});
24315}
24316
24317#[gpui::test]
24318async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24319 init_test(cx, |_| {});
24320
24321 let mut cx = EditorTestContext::new(cx).await;
24322 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24323 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24324
24325 // test `else` auto outdents when typed inside `if` block
24326 cx.set_state(indoc! {"
24327 def main():
24328 if i == 2:
24329 return
24330 ˇ
24331 "});
24332 cx.update_editor(|editor, window, cx| {
24333 editor.handle_input("else:", window, cx);
24334 });
24335 cx.assert_editor_state(indoc! {"
24336 def main():
24337 if i == 2:
24338 return
24339 else:ˇ
24340 "});
24341
24342 // test `except` auto outdents when typed inside `try` block
24343 cx.set_state(indoc! {"
24344 def main():
24345 try:
24346 i = 2
24347 ˇ
24348 "});
24349 cx.update_editor(|editor, window, cx| {
24350 editor.handle_input("except:", window, cx);
24351 });
24352 cx.assert_editor_state(indoc! {"
24353 def main():
24354 try:
24355 i = 2
24356 except:ˇ
24357 "});
24358
24359 // test `else` auto outdents when typed inside `except` block
24360 cx.set_state(indoc! {"
24361 def main():
24362 try:
24363 i = 2
24364 except:
24365 j = 2
24366 ˇ
24367 "});
24368 cx.update_editor(|editor, window, cx| {
24369 editor.handle_input("else:", window, cx);
24370 });
24371 cx.assert_editor_state(indoc! {"
24372 def main():
24373 try:
24374 i = 2
24375 except:
24376 j = 2
24377 else:ˇ
24378 "});
24379
24380 // test `finally` auto outdents when typed inside `else` block
24381 cx.set_state(indoc! {"
24382 def main():
24383 try:
24384 i = 2
24385 except:
24386 j = 2
24387 else:
24388 k = 2
24389 ˇ
24390 "});
24391 cx.update_editor(|editor, window, cx| {
24392 editor.handle_input("finally:", window, cx);
24393 });
24394 cx.assert_editor_state(indoc! {"
24395 def main():
24396 try:
24397 i = 2
24398 except:
24399 j = 2
24400 else:
24401 k = 2
24402 finally:ˇ
24403 "});
24404
24405 // test `else` does not outdents when typed inside `except` block right after for block
24406 cx.set_state(indoc! {"
24407 def main():
24408 try:
24409 i = 2
24410 except:
24411 for i in range(n):
24412 pass
24413 ˇ
24414 "});
24415 cx.update_editor(|editor, window, cx| {
24416 editor.handle_input("else:", window, cx);
24417 });
24418 cx.assert_editor_state(indoc! {"
24419 def main():
24420 try:
24421 i = 2
24422 except:
24423 for i in range(n):
24424 pass
24425 else:ˇ
24426 "});
24427
24428 // test `finally` auto outdents when typed inside `else` block right after for block
24429 cx.set_state(indoc! {"
24430 def main():
24431 try:
24432 i = 2
24433 except:
24434 j = 2
24435 else:
24436 for i in range(n):
24437 pass
24438 ˇ
24439 "});
24440 cx.update_editor(|editor, window, cx| {
24441 editor.handle_input("finally:", window, cx);
24442 });
24443 cx.assert_editor_state(indoc! {"
24444 def main():
24445 try:
24446 i = 2
24447 except:
24448 j = 2
24449 else:
24450 for i in range(n):
24451 pass
24452 finally:ˇ
24453 "});
24454
24455 // test `except` outdents to inner "try" block
24456 cx.set_state(indoc! {"
24457 def main():
24458 try:
24459 i = 2
24460 if i == 2:
24461 try:
24462 i = 3
24463 ˇ
24464 "});
24465 cx.update_editor(|editor, window, cx| {
24466 editor.handle_input("except:", window, cx);
24467 });
24468 cx.assert_editor_state(indoc! {"
24469 def main():
24470 try:
24471 i = 2
24472 if i == 2:
24473 try:
24474 i = 3
24475 except:ˇ
24476 "});
24477
24478 // test `except` outdents to outer "try" block
24479 cx.set_state(indoc! {"
24480 def main():
24481 try:
24482 i = 2
24483 if i == 2:
24484 try:
24485 i = 3
24486 ˇ
24487 "});
24488 cx.update_editor(|editor, window, cx| {
24489 editor.handle_input("except:", window, cx);
24490 });
24491 cx.assert_editor_state(indoc! {"
24492 def main():
24493 try:
24494 i = 2
24495 if i == 2:
24496 try:
24497 i = 3
24498 except:ˇ
24499 "});
24500
24501 // test `else` stays at correct indent when typed after `for` block
24502 cx.set_state(indoc! {"
24503 def main():
24504 for i in range(10):
24505 if i == 3:
24506 break
24507 ˇ
24508 "});
24509 cx.update_editor(|editor, window, cx| {
24510 editor.handle_input("else:", window, cx);
24511 });
24512 cx.assert_editor_state(indoc! {"
24513 def main():
24514 for i in range(10):
24515 if i == 3:
24516 break
24517 else:ˇ
24518 "});
24519
24520 // test does not outdent on typing after line with square brackets
24521 cx.set_state(indoc! {"
24522 def f() -> list[str]:
24523 ˇ
24524 "});
24525 cx.update_editor(|editor, window, cx| {
24526 editor.handle_input("a", window, cx);
24527 });
24528 cx.assert_editor_state(indoc! {"
24529 def f() -> list[str]:
24530 aˇ
24531 "});
24532
24533 // test does not outdent on typing : after case keyword
24534 cx.set_state(indoc! {"
24535 match 1:
24536 caseˇ
24537 "});
24538 cx.update_editor(|editor, window, cx| {
24539 editor.handle_input(":", window, cx);
24540 });
24541 cx.assert_editor_state(indoc! {"
24542 match 1:
24543 case:ˇ
24544 "});
24545}
24546
24547#[gpui::test]
24548async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24549 init_test(cx, |_| {});
24550 update_test_language_settings(cx, |settings| {
24551 settings.defaults.extend_comment_on_newline = Some(false);
24552 });
24553 let mut cx = EditorTestContext::new(cx).await;
24554 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24555 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24556
24557 // test correct indent after newline on comment
24558 cx.set_state(indoc! {"
24559 # COMMENT:ˇ
24560 "});
24561 cx.update_editor(|editor, window, cx| {
24562 editor.newline(&Newline, window, cx);
24563 });
24564 cx.assert_editor_state(indoc! {"
24565 # COMMENT:
24566 ˇ
24567 "});
24568
24569 // test correct indent after newline in brackets
24570 cx.set_state(indoc! {"
24571 {ˇ}
24572 "});
24573 cx.update_editor(|editor, window, cx| {
24574 editor.newline(&Newline, window, cx);
24575 });
24576 cx.run_until_parked();
24577 cx.assert_editor_state(indoc! {"
24578 {
24579 ˇ
24580 }
24581 "});
24582
24583 cx.set_state(indoc! {"
24584 (ˇ)
24585 "});
24586 cx.update_editor(|editor, window, cx| {
24587 editor.newline(&Newline, window, cx);
24588 });
24589 cx.run_until_parked();
24590 cx.assert_editor_state(indoc! {"
24591 (
24592 ˇ
24593 )
24594 "});
24595
24596 // do not indent after empty lists or dictionaries
24597 cx.set_state(indoc! {"
24598 a = []ˇ
24599 "});
24600 cx.update_editor(|editor, window, cx| {
24601 editor.newline(&Newline, window, cx);
24602 });
24603 cx.run_until_parked();
24604 cx.assert_editor_state(indoc! {"
24605 a = []
24606 ˇ
24607 "});
24608}
24609
24610#[gpui::test]
24611async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24612 init_test(cx, |_| {});
24613
24614 let mut cx = EditorTestContext::new(cx).await;
24615 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24616 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24617
24618 // test cursor move to start of each line on tab
24619 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24620 cx.set_state(indoc! {"
24621 function main() {
24622 ˇ for item in $items; do
24623 ˇ while [ -n \"$item\" ]; do
24624 ˇ if [ \"$value\" -gt 10 ]; then
24625 ˇ continue
24626 ˇ elif [ \"$value\" -lt 0 ]; then
24627 ˇ break
24628 ˇ else
24629 ˇ echo \"$item\"
24630 ˇ fi
24631 ˇ done
24632 ˇ done
24633 ˇ}
24634 "});
24635 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24636 cx.assert_editor_state(indoc! {"
24637 function main() {
24638 ˇfor item in $items; do
24639 ˇwhile [ -n \"$item\" ]; do
24640 ˇif [ \"$value\" -gt 10 ]; then
24641 ˇcontinue
24642 ˇelif [ \"$value\" -lt 0 ]; then
24643 ˇbreak
24644 ˇelse
24645 ˇecho \"$item\"
24646 ˇfi
24647 ˇdone
24648 ˇdone
24649 ˇ}
24650 "});
24651 // test relative indent is preserved when tab
24652 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24653 cx.assert_editor_state(indoc! {"
24654 function main() {
24655 ˇfor item in $items; do
24656 ˇwhile [ -n \"$item\" ]; do
24657 ˇif [ \"$value\" -gt 10 ]; then
24658 ˇcontinue
24659 ˇelif [ \"$value\" -lt 0 ]; then
24660 ˇbreak
24661 ˇelse
24662 ˇecho \"$item\"
24663 ˇfi
24664 ˇdone
24665 ˇdone
24666 ˇ}
24667 "});
24668
24669 // test cursor move to start of each line on tab
24670 // for `case` statement with patterns
24671 cx.set_state(indoc! {"
24672 function handle() {
24673 ˇ case \"$1\" in
24674 ˇ start)
24675 ˇ echo \"a\"
24676 ˇ ;;
24677 ˇ stop)
24678 ˇ echo \"b\"
24679 ˇ ;;
24680 ˇ *)
24681 ˇ echo \"c\"
24682 ˇ ;;
24683 ˇ esac
24684 ˇ}
24685 "});
24686 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24687 cx.assert_editor_state(indoc! {"
24688 function handle() {
24689 ˇcase \"$1\" in
24690 ˇstart)
24691 ˇecho \"a\"
24692 ˇ;;
24693 ˇstop)
24694 ˇecho \"b\"
24695 ˇ;;
24696 ˇ*)
24697 ˇecho \"c\"
24698 ˇ;;
24699 ˇesac
24700 ˇ}
24701 "});
24702}
24703
24704#[gpui::test]
24705async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24706 init_test(cx, |_| {});
24707
24708 let mut cx = EditorTestContext::new(cx).await;
24709 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24710 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24711
24712 // test indents on comment insert
24713 cx.set_state(indoc! {"
24714 function main() {
24715 ˇ for item in $items; do
24716 ˇ while [ -n \"$item\" ]; do
24717 ˇ if [ \"$value\" -gt 10 ]; then
24718 ˇ continue
24719 ˇ elif [ \"$value\" -lt 0 ]; then
24720 ˇ break
24721 ˇ else
24722 ˇ echo \"$item\"
24723 ˇ fi
24724 ˇ done
24725 ˇ done
24726 ˇ}
24727 "});
24728 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24729 cx.assert_editor_state(indoc! {"
24730 function main() {
24731 #ˇ for item in $items; do
24732 #ˇ while [ -n \"$item\" ]; do
24733 #ˇ if [ \"$value\" -gt 10 ]; then
24734 #ˇ continue
24735 #ˇ elif [ \"$value\" -lt 0 ]; then
24736 #ˇ break
24737 #ˇ else
24738 #ˇ echo \"$item\"
24739 #ˇ fi
24740 #ˇ done
24741 #ˇ done
24742 #ˇ}
24743 "});
24744}
24745
24746#[gpui::test]
24747async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24748 init_test(cx, |_| {});
24749
24750 let mut cx = EditorTestContext::new(cx).await;
24751 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24752 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24753
24754 // test `else` auto outdents when typed inside `if` block
24755 cx.set_state(indoc! {"
24756 if [ \"$1\" = \"test\" ]; then
24757 echo \"foo bar\"
24758 ˇ
24759 "});
24760 cx.update_editor(|editor, window, cx| {
24761 editor.handle_input("else", window, cx);
24762 });
24763 cx.assert_editor_state(indoc! {"
24764 if [ \"$1\" = \"test\" ]; then
24765 echo \"foo bar\"
24766 elseˇ
24767 "});
24768
24769 // test `elif` auto outdents when typed inside `if` block
24770 cx.set_state(indoc! {"
24771 if [ \"$1\" = \"test\" ]; then
24772 echo \"foo bar\"
24773 ˇ
24774 "});
24775 cx.update_editor(|editor, window, cx| {
24776 editor.handle_input("elif", window, cx);
24777 });
24778 cx.assert_editor_state(indoc! {"
24779 if [ \"$1\" = \"test\" ]; then
24780 echo \"foo bar\"
24781 elifˇ
24782 "});
24783
24784 // test `fi` auto outdents when typed inside `else` block
24785 cx.set_state(indoc! {"
24786 if [ \"$1\" = \"test\" ]; then
24787 echo \"foo bar\"
24788 else
24789 echo \"bar baz\"
24790 ˇ
24791 "});
24792 cx.update_editor(|editor, window, cx| {
24793 editor.handle_input("fi", window, cx);
24794 });
24795 cx.assert_editor_state(indoc! {"
24796 if [ \"$1\" = \"test\" ]; then
24797 echo \"foo bar\"
24798 else
24799 echo \"bar baz\"
24800 fiˇ
24801 "});
24802
24803 // test `done` auto outdents when typed inside `while` block
24804 cx.set_state(indoc! {"
24805 while read line; do
24806 echo \"$line\"
24807 ˇ
24808 "});
24809 cx.update_editor(|editor, window, cx| {
24810 editor.handle_input("done", window, cx);
24811 });
24812 cx.assert_editor_state(indoc! {"
24813 while read line; do
24814 echo \"$line\"
24815 doneˇ
24816 "});
24817
24818 // test `done` auto outdents when typed inside `for` block
24819 cx.set_state(indoc! {"
24820 for file in *.txt; do
24821 cat \"$file\"
24822 ˇ
24823 "});
24824 cx.update_editor(|editor, window, cx| {
24825 editor.handle_input("done", window, cx);
24826 });
24827 cx.assert_editor_state(indoc! {"
24828 for file in *.txt; do
24829 cat \"$file\"
24830 doneˇ
24831 "});
24832
24833 // test `esac` auto outdents when typed inside `case` block
24834 cx.set_state(indoc! {"
24835 case \"$1\" in
24836 start)
24837 echo \"foo bar\"
24838 ;;
24839 stop)
24840 echo \"bar baz\"
24841 ;;
24842 ˇ
24843 "});
24844 cx.update_editor(|editor, window, cx| {
24845 editor.handle_input("esac", window, cx);
24846 });
24847 cx.assert_editor_state(indoc! {"
24848 case \"$1\" in
24849 start)
24850 echo \"foo bar\"
24851 ;;
24852 stop)
24853 echo \"bar baz\"
24854 ;;
24855 esacˇ
24856 "});
24857
24858 // test `*)` auto outdents when typed inside `case` block
24859 cx.set_state(indoc! {"
24860 case \"$1\" in
24861 start)
24862 echo \"foo bar\"
24863 ;;
24864 ˇ
24865 "});
24866 cx.update_editor(|editor, window, cx| {
24867 editor.handle_input("*)", window, cx);
24868 });
24869 cx.assert_editor_state(indoc! {"
24870 case \"$1\" in
24871 start)
24872 echo \"foo bar\"
24873 ;;
24874 *)ˇ
24875 "});
24876
24877 // test `fi` outdents to correct level with nested if blocks
24878 cx.set_state(indoc! {"
24879 if [ \"$1\" = \"test\" ]; then
24880 echo \"outer if\"
24881 if [ \"$2\" = \"debug\" ]; then
24882 echo \"inner if\"
24883 ˇ
24884 "});
24885 cx.update_editor(|editor, window, cx| {
24886 editor.handle_input("fi", window, cx);
24887 });
24888 cx.assert_editor_state(indoc! {"
24889 if [ \"$1\" = \"test\" ]; then
24890 echo \"outer if\"
24891 if [ \"$2\" = \"debug\" ]; then
24892 echo \"inner if\"
24893 fiˇ
24894 "});
24895}
24896
24897#[gpui::test]
24898async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24899 init_test(cx, |_| {});
24900 update_test_language_settings(cx, |settings| {
24901 settings.defaults.extend_comment_on_newline = Some(false);
24902 });
24903 let mut cx = EditorTestContext::new(cx).await;
24904 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24905 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24906
24907 // test correct indent after newline on comment
24908 cx.set_state(indoc! {"
24909 # COMMENT:ˇ
24910 "});
24911 cx.update_editor(|editor, window, cx| {
24912 editor.newline(&Newline, window, cx);
24913 });
24914 cx.assert_editor_state(indoc! {"
24915 # COMMENT:
24916 ˇ
24917 "});
24918
24919 // test correct indent after newline after `then`
24920 cx.set_state(indoc! {"
24921
24922 if [ \"$1\" = \"test\" ]; thenˇ
24923 "});
24924 cx.update_editor(|editor, window, cx| {
24925 editor.newline(&Newline, window, cx);
24926 });
24927 cx.run_until_parked();
24928 cx.assert_editor_state(indoc! {"
24929
24930 if [ \"$1\" = \"test\" ]; then
24931 ˇ
24932 "});
24933
24934 // test correct indent after newline after `else`
24935 cx.set_state(indoc! {"
24936 if [ \"$1\" = \"test\" ]; then
24937 elseˇ
24938 "});
24939 cx.update_editor(|editor, window, cx| {
24940 editor.newline(&Newline, window, cx);
24941 });
24942 cx.run_until_parked();
24943 cx.assert_editor_state(indoc! {"
24944 if [ \"$1\" = \"test\" ]; then
24945 else
24946 ˇ
24947 "});
24948
24949 // test correct indent after newline after `elif`
24950 cx.set_state(indoc! {"
24951 if [ \"$1\" = \"test\" ]; then
24952 elifˇ
24953 "});
24954 cx.update_editor(|editor, window, cx| {
24955 editor.newline(&Newline, window, cx);
24956 });
24957 cx.run_until_parked();
24958 cx.assert_editor_state(indoc! {"
24959 if [ \"$1\" = \"test\" ]; then
24960 elif
24961 ˇ
24962 "});
24963
24964 // test correct indent after newline after `do`
24965 cx.set_state(indoc! {"
24966 for file in *.txt; doˇ
24967 "});
24968 cx.update_editor(|editor, window, cx| {
24969 editor.newline(&Newline, window, cx);
24970 });
24971 cx.run_until_parked();
24972 cx.assert_editor_state(indoc! {"
24973 for file in *.txt; do
24974 ˇ
24975 "});
24976
24977 // test correct indent after newline after case pattern
24978 cx.set_state(indoc! {"
24979 case \"$1\" in
24980 start)ˇ
24981 "});
24982 cx.update_editor(|editor, window, cx| {
24983 editor.newline(&Newline, window, cx);
24984 });
24985 cx.run_until_parked();
24986 cx.assert_editor_state(indoc! {"
24987 case \"$1\" in
24988 start)
24989 ˇ
24990 "});
24991
24992 // test correct indent after newline after case pattern
24993 cx.set_state(indoc! {"
24994 case \"$1\" in
24995 start)
24996 ;;
24997 *)ˇ
24998 "});
24999 cx.update_editor(|editor, window, cx| {
25000 editor.newline(&Newline, window, cx);
25001 });
25002 cx.run_until_parked();
25003 cx.assert_editor_state(indoc! {"
25004 case \"$1\" in
25005 start)
25006 ;;
25007 *)
25008 ˇ
25009 "});
25010
25011 // test correct indent after newline after function opening brace
25012 cx.set_state(indoc! {"
25013 function test() {ˇ}
25014 "});
25015 cx.update_editor(|editor, window, cx| {
25016 editor.newline(&Newline, window, cx);
25017 });
25018 cx.run_until_parked();
25019 cx.assert_editor_state(indoc! {"
25020 function test() {
25021 ˇ
25022 }
25023 "});
25024
25025 // test no extra indent after semicolon on same line
25026 cx.set_state(indoc! {"
25027 echo \"test\";ˇ
25028 "});
25029 cx.update_editor(|editor, window, cx| {
25030 editor.newline(&Newline, window, cx);
25031 });
25032 cx.run_until_parked();
25033 cx.assert_editor_state(indoc! {"
25034 echo \"test\";
25035 ˇ
25036 "});
25037}
25038
25039fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25040 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25041 point..point
25042}
25043
25044#[track_caller]
25045fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25046 let (text, ranges) = marked_text_ranges(marked_text, true);
25047 assert_eq!(editor.text(cx), text);
25048 assert_eq!(
25049 editor.selections.ranges(cx),
25050 ranges,
25051 "Assert selections are {}",
25052 marked_text
25053 );
25054}
25055
25056pub fn handle_signature_help_request(
25057 cx: &mut EditorLspTestContext,
25058 mocked_response: lsp::SignatureHelp,
25059) -> impl Future<Output = ()> + use<> {
25060 let mut request =
25061 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25062 let mocked_response = mocked_response.clone();
25063 async move { Ok(Some(mocked_response)) }
25064 });
25065
25066 async move {
25067 request.next().await;
25068 }
25069}
25070
25071#[track_caller]
25072pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25073 cx.update_editor(|editor, _, _| {
25074 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25075 let entries = menu.entries.borrow();
25076 let entries = entries
25077 .iter()
25078 .map(|entry| entry.string.as_str())
25079 .collect::<Vec<_>>();
25080 assert_eq!(entries, expected);
25081 } else {
25082 panic!("Expected completions menu");
25083 }
25084 });
25085}
25086
25087/// Handle completion request passing a marked string specifying where the completion
25088/// should be triggered from using '|' character, what range should be replaced, and what completions
25089/// should be returned using '<' and '>' to delimit the range.
25090///
25091/// Also see `handle_completion_request_with_insert_and_replace`.
25092#[track_caller]
25093pub fn handle_completion_request(
25094 marked_string: &str,
25095 completions: Vec<&'static str>,
25096 is_incomplete: bool,
25097 counter: Arc<AtomicUsize>,
25098 cx: &mut EditorLspTestContext,
25099) -> impl Future<Output = ()> {
25100 let complete_from_marker: TextRangeMarker = '|'.into();
25101 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25102 let (_, mut marked_ranges) = marked_text_ranges_by(
25103 marked_string,
25104 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25105 );
25106
25107 let complete_from_position =
25108 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25109 let replace_range =
25110 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25111
25112 let mut request =
25113 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25114 let completions = completions.clone();
25115 counter.fetch_add(1, atomic::Ordering::Release);
25116 async move {
25117 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25118 assert_eq!(
25119 params.text_document_position.position,
25120 complete_from_position
25121 );
25122 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25123 is_incomplete,
25124 item_defaults: None,
25125 items: completions
25126 .iter()
25127 .map(|completion_text| lsp::CompletionItem {
25128 label: completion_text.to_string(),
25129 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25130 range: replace_range,
25131 new_text: completion_text.to_string(),
25132 })),
25133 ..Default::default()
25134 })
25135 .collect(),
25136 })))
25137 }
25138 });
25139
25140 async move {
25141 request.next().await;
25142 }
25143}
25144
25145/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25146/// given instead, which also contains an `insert` range.
25147///
25148/// This function uses markers to define ranges:
25149/// - `|` marks the cursor position
25150/// - `<>` marks the replace range
25151/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25152pub fn handle_completion_request_with_insert_and_replace(
25153 cx: &mut EditorLspTestContext,
25154 marked_string: &str,
25155 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25156 counter: Arc<AtomicUsize>,
25157) -> impl Future<Output = ()> {
25158 let complete_from_marker: TextRangeMarker = '|'.into();
25159 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25160 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25161
25162 let (_, mut marked_ranges) = marked_text_ranges_by(
25163 marked_string,
25164 vec![
25165 complete_from_marker.clone(),
25166 replace_range_marker.clone(),
25167 insert_range_marker.clone(),
25168 ],
25169 );
25170
25171 let complete_from_position =
25172 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25173 let replace_range =
25174 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25175
25176 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25177 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25178 _ => lsp::Range {
25179 start: replace_range.start,
25180 end: complete_from_position,
25181 },
25182 };
25183
25184 let mut request =
25185 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25186 let completions = completions.clone();
25187 counter.fetch_add(1, atomic::Ordering::Release);
25188 async move {
25189 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25190 assert_eq!(
25191 params.text_document_position.position, complete_from_position,
25192 "marker `|` position doesn't match",
25193 );
25194 Ok(Some(lsp::CompletionResponse::Array(
25195 completions
25196 .iter()
25197 .map(|(label, new_text)| lsp::CompletionItem {
25198 label: label.to_string(),
25199 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25200 lsp::InsertReplaceEdit {
25201 insert: insert_range,
25202 replace: replace_range,
25203 new_text: new_text.to_string(),
25204 },
25205 )),
25206 ..Default::default()
25207 })
25208 .collect(),
25209 )))
25210 }
25211 });
25212
25213 async move {
25214 request.next().await;
25215 }
25216}
25217
25218fn handle_resolve_completion_request(
25219 cx: &mut EditorLspTestContext,
25220 edits: Option<Vec<(&'static str, &'static str)>>,
25221) -> impl Future<Output = ()> {
25222 let edits = edits.map(|edits| {
25223 edits
25224 .iter()
25225 .map(|(marked_string, new_text)| {
25226 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25227 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25228 lsp::TextEdit::new(replace_range, new_text.to_string())
25229 })
25230 .collect::<Vec<_>>()
25231 });
25232
25233 let mut request =
25234 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25235 let edits = edits.clone();
25236 async move {
25237 Ok(lsp::CompletionItem {
25238 additional_text_edits: edits,
25239 ..Default::default()
25240 })
25241 }
25242 });
25243
25244 async move {
25245 request.next().await;
25246 }
25247}
25248
25249pub(crate) fn update_test_language_settings(
25250 cx: &mut TestAppContext,
25251 f: impl Fn(&mut AllLanguageSettingsContent),
25252) {
25253 cx.update(|cx| {
25254 SettingsStore::update_global(cx, |store, cx| {
25255 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25256 });
25257 });
25258}
25259
25260pub(crate) fn update_test_project_settings(
25261 cx: &mut TestAppContext,
25262 f: impl Fn(&mut ProjectSettingsContent),
25263) {
25264 cx.update(|cx| {
25265 SettingsStore::update_global(cx, |store, cx| {
25266 store.update_user_settings(cx, |settings| f(&mut settings.project));
25267 });
25268 });
25269}
25270
25271pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25272 cx.update(|cx| {
25273 assets::Assets.load_test_fonts(cx);
25274 let store = SettingsStore::test(cx);
25275 cx.set_global(store);
25276 theme::init(theme::LoadThemes::JustBase, cx);
25277 release_channel::init(SemanticVersion::default(), cx);
25278 client::init_settings(cx);
25279 language::init(cx);
25280 Project::init_settings(cx);
25281 workspace::init_settings(cx);
25282 crate::init(cx);
25283 });
25284 zlog::init_test();
25285 update_test_language_settings(cx, f);
25286}
25287
25288#[track_caller]
25289fn assert_hunk_revert(
25290 not_reverted_text_with_selections: &str,
25291 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25292 expected_reverted_text_with_selections: &str,
25293 base_text: &str,
25294 cx: &mut EditorLspTestContext,
25295) {
25296 cx.set_state(not_reverted_text_with_selections);
25297 cx.set_head_text(base_text);
25298 cx.executor().run_until_parked();
25299
25300 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25301 let snapshot = editor.snapshot(window, cx);
25302 let reverted_hunk_statuses = snapshot
25303 .buffer_snapshot
25304 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
25305 .map(|hunk| hunk.status().kind)
25306 .collect::<Vec<_>>();
25307
25308 editor.git_restore(&Default::default(), window, cx);
25309 reverted_hunk_statuses
25310 });
25311 cx.executor().run_until_parked();
25312 cx.assert_editor_state(expected_reverted_text_with_selections);
25313 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25314}
25315
25316#[gpui::test(iterations = 10)]
25317async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25318 init_test(cx, |_| {});
25319
25320 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25321 let counter = diagnostic_requests.clone();
25322
25323 let fs = FakeFs::new(cx.executor());
25324 fs.insert_tree(
25325 path!("/a"),
25326 json!({
25327 "first.rs": "fn main() { let a = 5; }",
25328 "second.rs": "// Test file",
25329 }),
25330 )
25331 .await;
25332
25333 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25334 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25335 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25336
25337 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25338 language_registry.add(rust_lang());
25339 let mut fake_servers = language_registry.register_fake_lsp(
25340 "Rust",
25341 FakeLspAdapter {
25342 capabilities: lsp::ServerCapabilities {
25343 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25344 lsp::DiagnosticOptions {
25345 identifier: None,
25346 inter_file_dependencies: true,
25347 workspace_diagnostics: true,
25348 work_done_progress_options: Default::default(),
25349 },
25350 )),
25351 ..Default::default()
25352 },
25353 ..Default::default()
25354 },
25355 );
25356
25357 let editor = workspace
25358 .update(cx, |workspace, window, cx| {
25359 workspace.open_abs_path(
25360 PathBuf::from(path!("/a/first.rs")),
25361 OpenOptions::default(),
25362 window,
25363 cx,
25364 )
25365 })
25366 .unwrap()
25367 .await
25368 .unwrap()
25369 .downcast::<Editor>()
25370 .unwrap();
25371 let fake_server = fake_servers.next().await.unwrap();
25372 let server_id = fake_server.server.server_id();
25373 let mut first_request = fake_server
25374 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25375 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25376 let result_id = Some(new_result_id.to_string());
25377 assert_eq!(
25378 params.text_document.uri,
25379 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25380 );
25381 async move {
25382 Ok(lsp::DocumentDiagnosticReportResult::Report(
25383 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25384 related_documents: None,
25385 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25386 items: Vec::new(),
25387 result_id,
25388 },
25389 }),
25390 ))
25391 }
25392 });
25393
25394 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25395 project.update(cx, |project, cx| {
25396 let buffer_id = editor
25397 .read(cx)
25398 .buffer()
25399 .read(cx)
25400 .as_singleton()
25401 .expect("created a singleton buffer")
25402 .read(cx)
25403 .remote_id();
25404 let buffer_result_id = project
25405 .lsp_store()
25406 .read(cx)
25407 .result_id(server_id, buffer_id, cx);
25408 assert_eq!(expected, buffer_result_id);
25409 });
25410 };
25411
25412 ensure_result_id(None, cx);
25413 cx.executor().advance_clock(Duration::from_millis(60));
25414 cx.executor().run_until_parked();
25415 assert_eq!(
25416 diagnostic_requests.load(atomic::Ordering::Acquire),
25417 1,
25418 "Opening file should trigger diagnostic request"
25419 );
25420 first_request
25421 .next()
25422 .await
25423 .expect("should have sent the first diagnostics pull request");
25424 ensure_result_id(Some("1".to_string()), cx);
25425
25426 // Editing should trigger diagnostics
25427 editor.update_in(cx, |editor, window, cx| {
25428 editor.handle_input("2", window, cx)
25429 });
25430 cx.executor().advance_clock(Duration::from_millis(60));
25431 cx.executor().run_until_parked();
25432 assert_eq!(
25433 diagnostic_requests.load(atomic::Ordering::Acquire),
25434 2,
25435 "Editing should trigger diagnostic request"
25436 );
25437 ensure_result_id(Some("2".to_string()), cx);
25438
25439 // Moving cursor should not trigger diagnostic request
25440 editor.update_in(cx, |editor, window, cx| {
25441 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25442 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25443 });
25444 });
25445 cx.executor().advance_clock(Duration::from_millis(60));
25446 cx.executor().run_until_parked();
25447 assert_eq!(
25448 diagnostic_requests.load(atomic::Ordering::Acquire),
25449 2,
25450 "Cursor movement should not trigger diagnostic request"
25451 );
25452 ensure_result_id(Some("2".to_string()), cx);
25453 // Multiple rapid edits should be debounced
25454 for _ in 0..5 {
25455 editor.update_in(cx, |editor, window, cx| {
25456 editor.handle_input("x", window, cx)
25457 });
25458 }
25459 cx.executor().advance_clock(Duration::from_millis(60));
25460 cx.executor().run_until_parked();
25461
25462 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25463 assert!(
25464 final_requests <= 4,
25465 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25466 );
25467 ensure_result_id(Some(final_requests.to_string()), cx);
25468}
25469
25470#[gpui::test]
25471async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25472 // Regression test for issue #11671
25473 // Previously, adding a cursor after moving multiple cursors would reset
25474 // the cursor count instead of adding to the existing cursors.
25475 init_test(cx, |_| {});
25476 let mut cx = EditorTestContext::new(cx).await;
25477
25478 // Create a simple buffer with cursor at start
25479 cx.set_state(indoc! {"
25480 ˇaaaa
25481 bbbb
25482 cccc
25483 dddd
25484 eeee
25485 ffff
25486 gggg
25487 hhhh"});
25488
25489 // Add 2 cursors below (so we have 3 total)
25490 cx.update_editor(|editor, window, cx| {
25491 editor.add_selection_below(&Default::default(), window, cx);
25492 editor.add_selection_below(&Default::default(), window, cx);
25493 });
25494
25495 // Verify we have 3 cursors
25496 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25497 assert_eq!(
25498 initial_count, 3,
25499 "Should have 3 cursors after adding 2 below"
25500 );
25501
25502 // Move down one line
25503 cx.update_editor(|editor, window, cx| {
25504 editor.move_down(&MoveDown, window, cx);
25505 });
25506
25507 // Add another cursor below
25508 cx.update_editor(|editor, window, cx| {
25509 editor.add_selection_below(&Default::default(), window, cx);
25510 });
25511
25512 // Should now have 4 cursors (3 original + 1 new)
25513 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25514 assert_eq!(
25515 final_count, 4,
25516 "Should have 4 cursors after moving and adding another"
25517 );
25518}
25519
25520#[gpui::test(iterations = 10)]
25521async fn test_document_colors(cx: &mut TestAppContext) {
25522 let expected_color = Rgba {
25523 r: 0.33,
25524 g: 0.33,
25525 b: 0.33,
25526 a: 0.33,
25527 };
25528
25529 init_test(cx, |_| {});
25530
25531 let fs = FakeFs::new(cx.executor());
25532 fs.insert_tree(
25533 path!("/a"),
25534 json!({
25535 "first.rs": "fn main() { let a = 5; }",
25536 }),
25537 )
25538 .await;
25539
25540 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25541 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25542 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25543
25544 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25545 language_registry.add(rust_lang());
25546 let mut fake_servers = language_registry.register_fake_lsp(
25547 "Rust",
25548 FakeLspAdapter {
25549 capabilities: lsp::ServerCapabilities {
25550 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25551 ..lsp::ServerCapabilities::default()
25552 },
25553 name: "rust-analyzer",
25554 ..FakeLspAdapter::default()
25555 },
25556 );
25557 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25558 "Rust",
25559 FakeLspAdapter {
25560 capabilities: lsp::ServerCapabilities {
25561 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25562 ..lsp::ServerCapabilities::default()
25563 },
25564 name: "not-rust-analyzer",
25565 ..FakeLspAdapter::default()
25566 },
25567 );
25568
25569 let editor = workspace
25570 .update(cx, |workspace, window, cx| {
25571 workspace.open_abs_path(
25572 PathBuf::from(path!("/a/first.rs")),
25573 OpenOptions::default(),
25574 window,
25575 cx,
25576 )
25577 })
25578 .unwrap()
25579 .await
25580 .unwrap()
25581 .downcast::<Editor>()
25582 .unwrap();
25583 let fake_language_server = fake_servers.next().await.unwrap();
25584 let fake_language_server_without_capabilities =
25585 fake_servers_without_capabilities.next().await.unwrap();
25586 let requests_made = Arc::new(AtomicUsize::new(0));
25587 let closure_requests_made = Arc::clone(&requests_made);
25588 let mut color_request_handle = fake_language_server
25589 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25590 let requests_made = Arc::clone(&closure_requests_made);
25591 async move {
25592 assert_eq!(
25593 params.text_document.uri,
25594 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25595 );
25596 requests_made.fetch_add(1, atomic::Ordering::Release);
25597 Ok(vec![
25598 lsp::ColorInformation {
25599 range: lsp::Range {
25600 start: lsp::Position {
25601 line: 0,
25602 character: 0,
25603 },
25604 end: lsp::Position {
25605 line: 0,
25606 character: 1,
25607 },
25608 },
25609 color: lsp::Color {
25610 red: 0.33,
25611 green: 0.33,
25612 blue: 0.33,
25613 alpha: 0.33,
25614 },
25615 },
25616 lsp::ColorInformation {
25617 range: lsp::Range {
25618 start: lsp::Position {
25619 line: 0,
25620 character: 0,
25621 },
25622 end: lsp::Position {
25623 line: 0,
25624 character: 1,
25625 },
25626 },
25627 color: lsp::Color {
25628 red: 0.33,
25629 green: 0.33,
25630 blue: 0.33,
25631 alpha: 0.33,
25632 },
25633 },
25634 ])
25635 }
25636 });
25637
25638 let _handle = fake_language_server_without_capabilities
25639 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25640 panic!("Should not be called");
25641 });
25642 cx.executor().advance_clock(Duration::from_millis(100));
25643 color_request_handle.next().await.unwrap();
25644 cx.run_until_parked();
25645 assert_eq!(
25646 1,
25647 requests_made.load(atomic::Ordering::Acquire),
25648 "Should query for colors once per editor open"
25649 );
25650 editor.update_in(cx, |editor, _, cx| {
25651 assert_eq!(
25652 vec![expected_color],
25653 extract_color_inlays(editor, cx),
25654 "Should have an initial inlay"
25655 );
25656 });
25657
25658 // opening another file in a split should not influence the LSP query counter
25659 workspace
25660 .update(cx, |workspace, window, cx| {
25661 assert_eq!(
25662 workspace.panes().len(),
25663 1,
25664 "Should have one pane with one editor"
25665 );
25666 workspace.move_item_to_pane_in_direction(
25667 &MoveItemToPaneInDirection {
25668 direction: SplitDirection::Right,
25669 focus: false,
25670 clone: true,
25671 },
25672 window,
25673 cx,
25674 );
25675 })
25676 .unwrap();
25677 cx.run_until_parked();
25678 workspace
25679 .update(cx, |workspace, _, cx| {
25680 let panes = workspace.panes();
25681 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25682 for pane in panes {
25683 let editor = pane
25684 .read(cx)
25685 .active_item()
25686 .and_then(|item| item.downcast::<Editor>())
25687 .expect("Should have opened an editor in each split");
25688 let editor_file = editor
25689 .read(cx)
25690 .buffer()
25691 .read(cx)
25692 .as_singleton()
25693 .expect("test deals with singleton buffers")
25694 .read(cx)
25695 .file()
25696 .expect("test buffese should have a file")
25697 .path();
25698 assert_eq!(
25699 editor_file.as_ref(),
25700 Path::new("first.rs"),
25701 "Both editors should be opened for the same file"
25702 )
25703 }
25704 })
25705 .unwrap();
25706
25707 cx.executor().advance_clock(Duration::from_millis(500));
25708 let save = editor.update_in(cx, |editor, window, cx| {
25709 editor.move_to_end(&MoveToEnd, window, cx);
25710 editor.handle_input("dirty", window, cx);
25711 editor.save(
25712 SaveOptions {
25713 format: true,
25714 autosave: true,
25715 },
25716 project.clone(),
25717 window,
25718 cx,
25719 )
25720 });
25721 save.await.unwrap();
25722
25723 color_request_handle.next().await.unwrap();
25724 cx.run_until_parked();
25725 assert_eq!(
25726 3,
25727 requests_made.load(atomic::Ordering::Acquire),
25728 "Should query for colors once per save and once per formatting after save"
25729 );
25730
25731 drop(editor);
25732 let close = workspace
25733 .update(cx, |workspace, window, cx| {
25734 workspace.active_pane().update(cx, |pane, cx| {
25735 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25736 })
25737 })
25738 .unwrap();
25739 close.await.unwrap();
25740 let close = workspace
25741 .update(cx, |workspace, window, cx| {
25742 workspace.active_pane().update(cx, |pane, cx| {
25743 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25744 })
25745 })
25746 .unwrap();
25747 close.await.unwrap();
25748 assert_eq!(
25749 3,
25750 requests_made.load(atomic::Ordering::Acquire),
25751 "After saving and closing all editors, no extra requests should be made"
25752 );
25753 workspace
25754 .update(cx, |workspace, _, cx| {
25755 assert!(
25756 workspace.active_item(cx).is_none(),
25757 "Should close all editors"
25758 )
25759 })
25760 .unwrap();
25761
25762 workspace
25763 .update(cx, |workspace, window, cx| {
25764 workspace.active_pane().update(cx, |pane, cx| {
25765 pane.navigate_backward(&Default::default(), window, cx);
25766 })
25767 })
25768 .unwrap();
25769 cx.executor().advance_clock(Duration::from_millis(100));
25770 cx.run_until_parked();
25771 let editor = workspace
25772 .update(cx, |workspace, _, cx| {
25773 workspace
25774 .active_item(cx)
25775 .expect("Should have reopened the editor again after navigating back")
25776 .downcast::<Editor>()
25777 .expect("Should be an editor")
25778 })
25779 .unwrap();
25780 color_request_handle.next().await.unwrap();
25781 assert_eq!(
25782 3,
25783 requests_made.load(atomic::Ordering::Acquire),
25784 "Cache should be reused on buffer close and reopen"
25785 );
25786 editor.update(cx, |editor, cx| {
25787 assert_eq!(
25788 vec![expected_color],
25789 extract_color_inlays(editor, cx),
25790 "Should have an initial inlay"
25791 );
25792 });
25793}
25794
25795#[gpui::test]
25796async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25797 init_test(cx, |_| {});
25798 let (editor, cx) = cx.add_window_view(Editor::single_line);
25799 editor.update_in(cx, |editor, window, cx| {
25800 editor.set_text("oops\n\nwow\n", window, cx)
25801 });
25802 cx.run_until_parked();
25803 editor.update(cx, |editor, cx| {
25804 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25805 });
25806 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25807 cx.run_until_parked();
25808 editor.update(cx, |editor, cx| {
25809 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25810 });
25811}
25812
25813#[gpui::test]
25814async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25815 init_test(cx, |_| {});
25816
25817 cx.update(|cx| {
25818 register_project_item::<Editor>(cx);
25819 });
25820
25821 let fs = FakeFs::new(cx.executor());
25822 fs.insert_tree("/root1", json!({})).await;
25823 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25824 .await;
25825
25826 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25827 let (workspace, cx) =
25828 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25829
25830 let worktree_id = project.update(cx, |project, cx| {
25831 project.worktrees(cx).next().unwrap().read(cx).id()
25832 });
25833
25834 let handle = workspace
25835 .update_in(cx, |workspace, window, cx| {
25836 let project_path = (worktree_id, "one.pdf");
25837 workspace.open_path(project_path, None, true, window, cx)
25838 })
25839 .await
25840 .unwrap();
25841
25842 assert_eq!(
25843 handle.to_any().entity_type(),
25844 TypeId::of::<InvalidBufferView>()
25845 );
25846}
25847
25848#[gpui::test]
25849async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25850 init_test(cx, |_| {});
25851
25852 let language = Arc::new(Language::new(
25853 LanguageConfig::default(),
25854 Some(tree_sitter_rust::LANGUAGE.into()),
25855 ));
25856
25857 // Test hierarchical sibling navigation
25858 let text = r#"
25859 fn outer() {
25860 if condition {
25861 let a = 1;
25862 }
25863 let b = 2;
25864 }
25865
25866 fn another() {
25867 let c = 3;
25868 }
25869 "#;
25870
25871 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25872 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25873 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25874
25875 // Wait for parsing to complete
25876 editor
25877 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25878 .await;
25879
25880 editor.update_in(cx, |editor, window, cx| {
25881 // Start by selecting "let a = 1;" inside the if block
25882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25883 s.select_display_ranges([
25884 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25885 ]);
25886 });
25887
25888 let initial_selection = editor.selections.display_ranges(cx);
25889 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25890
25891 // Test select next sibling - should move up levels to find the next sibling
25892 // Since "let a = 1;" has no siblings in the if block, it should move up
25893 // to find "let b = 2;" which is a sibling of the if block
25894 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25895 let next_selection = editor.selections.display_ranges(cx);
25896
25897 // Should have a selection and it should be different from the initial
25898 assert_eq!(
25899 next_selection.len(),
25900 1,
25901 "Should have one selection after next"
25902 );
25903 assert_ne!(
25904 next_selection[0], initial_selection[0],
25905 "Next sibling selection should be different"
25906 );
25907
25908 // Test hierarchical navigation by going to the end of the current function
25909 // and trying to navigate to the next function
25910 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25911 s.select_display_ranges([
25912 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25913 ]);
25914 });
25915
25916 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25917 let function_next_selection = editor.selections.display_ranges(cx);
25918
25919 // Should move to the next function
25920 assert_eq!(
25921 function_next_selection.len(),
25922 1,
25923 "Should have one selection after function next"
25924 );
25925
25926 // Test select previous sibling navigation
25927 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25928 let prev_selection = editor.selections.display_ranges(cx);
25929
25930 // Should have a selection and it should be different
25931 assert_eq!(
25932 prev_selection.len(),
25933 1,
25934 "Should have one selection after prev"
25935 );
25936 assert_ne!(
25937 prev_selection[0], function_next_selection[0],
25938 "Previous sibling selection should be different from next"
25939 );
25940 });
25941}
25942
25943#[gpui::test]
25944async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
25945 init_test(cx, |_| {});
25946
25947 let mut cx = EditorTestContext::new(cx).await;
25948 cx.set_state(
25949 "let ˇvariable = 42;
25950let another = variable + 1;
25951let result = variable * 2;",
25952 );
25953
25954 // Set up document highlights manually (simulating LSP response)
25955 cx.update_editor(|editor, _window, cx| {
25956 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
25957
25958 // Create highlights for "variable" occurrences
25959 let highlight_ranges = [
25960 Point::new(0, 4)..Point::new(0, 12), // First "variable"
25961 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
25962 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
25963 ];
25964
25965 let anchor_ranges: Vec<_> = highlight_ranges
25966 .iter()
25967 .map(|range| range.clone().to_anchors(&buffer_snapshot))
25968 .collect();
25969
25970 editor.highlight_background::<DocumentHighlightRead>(
25971 &anchor_ranges,
25972 |theme| theme.colors().editor_document_highlight_read_background,
25973 cx,
25974 );
25975 });
25976
25977 // Go to next highlight - should move to second "variable"
25978 cx.update_editor(|editor, window, cx| {
25979 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25980 });
25981 cx.assert_editor_state(
25982 "let variable = 42;
25983let another = ˇvariable + 1;
25984let result = variable * 2;",
25985 );
25986
25987 // Go to next highlight - should move to third "variable"
25988 cx.update_editor(|editor, window, cx| {
25989 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25990 });
25991 cx.assert_editor_state(
25992 "let variable = 42;
25993let another = variable + 1;
25994let result = ˇvariable * 2;",
25995 );
25996
25997 // Go to next highlight - should stay at third "variable" (no wrap-around)
25998 cx.update_editor(|editor, window, cx| {
25999 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26000 });
26001 cx.assert_editor_state(
26002 "let variable = 42;
26003let another = variable + 1;
26004let result = ˇvariable * 2;",
26005 );
26006
26007 // Now test going backwards from third position
26008 cx.update_editor(|editor, window, cx| {
26009 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26010 });
26011 cx.assert_editor_state(
26012 "let variable = 42;
26013let another = ˇvariable + 1;
26014let result = variable * 2;",
26015 );
26016
26017 // Go to previous highlight - should move to first "variable"
26018 cx.update_editor(|editor, window, cx| {
26019 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26020 });
26021 cx.assert_editor_state(
26022 "let ˇvariable = 42;
26023let another = variable + 1;
26024let result = variable * 2;",
26025 );
26026
26027 // Go to previous highlight - should stay on first "variable"
26028 cx.update_editor(|editor, window, cx| {
26029 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26030 });
26031 cx.assert_editor_state(
26032 "let ˇvariable = 42;
26033let another = variable + 1;
26034let result = variable * 2;",
26035 );
26036}
26037
26038#[gpui::test]
26039async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26040 cx: &mut gpui::TestAppContext,
26041) {
26042 init_test(cx, |_| {});
26043
26044 let url = "https://zed.dev";
26045
26046 let markdown_language = Arc::new(Language::new(
26047 LanguageConfig {
26048 name: "Markdown".into(),
26049 ..LanguageConfig::default()
26050 },
26051 None,
26052 ));
26053
26054 let mut cx = EditorTestContext::new(cx).await;
26055 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26056 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26057
26058 cx.update_editor(|editor, window, cx| {
26059 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26060 editor.paste(&Paste, window, cx);
26061 });
26062
26063 cx.assert_editor_state(&format!(
26064 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26065 ));
26066}
26067
26068#[gpui::test]
26069async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26070 cx: &mut gpui::TestAppContext,
26071) {
26072 init_test(cx, |_| {});
26073
26074 let url = "https://zed.dev";
26075
26076 let markdown_language = Arc::new(Language::new(
26077 LanguageConfig {
26078 name: "Markdown".into(),
26079 ..LanguageConfig::default()
26080 },
26081 None,
26082 ));
26083
26084 let mut cx = EditorTestContext::new(cx).await;
26085 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26086 cx.set_state(&format!(
26087 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26088 ));
26089
26090 cx.update_editor(|editor, window, cx| {
26091 editor.copy(&Copy, window, cx);
26092 });
26093
26094 cx.set_state(&format!(
26095 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26096 ));
26097
26098 cx.update_editor(|editor, window, cx| {
26099 editor.paste(&Paste, window, cx);
26100 });
26101
26102 cx.assert_editor_state(&format!(
26103 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26104 ));
26105}
26106
26107#[gpui::test]
26108async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26109 cx: &mut gpui::TestAppContext,
26110) {
26111 init_test(cx, |_| {});
26112
26113 let url = "https://zed.dev";
26114
26115 let markdown_language = Arc::new(Language::new(
26116 LanguageConfig {
26117 name: "Markdown".into(),
26118 ..LanguageConfig::default()
26119 },
26120 None,
26121 ));
26122
26123 let mut cx = EditorTestContext::new(cx).await;
26124 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26125 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26126
26127 cx.update_editor(|editor, window, cx| {
26128 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26129 editor.paste(&Paste, window, cx);
26130 });
26131
26132 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26133}
26134
26135#[gpui::test]
26136async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26137 cx: &mut gpui::TestAppContext,
26138) {
26139 init_test(cx, |_| {});
26140
26141 let text = "Awesome";
26142
26143 let markdown_language = Arc::new(Language::new(
26144 LanguageConfig {
26145 name: "Markdown".into(),
26146 ..LanguageConfig::default()
26147 },
26148 None,
26149 ));
26150
26151 let mut cx = EditorTestContext::new(cx).await;
26152 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26153 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26154
26155 cx.update_editor(|editor, window, cx| {
26156 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26157 editor.paste(&Paste, window, cx);
26158 });
26159
26160 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26161}
26162
26163#[gpui::test]
26164async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26165 cx: &mut gpui::TestAppContext,
26166) {
26167 init_test(cx, |_| {});
26168
26169 let url = "https://zed.dev";
26170
26171 let markdown_language = Arc::new(Language::new(
26172 LanguageConfig {
26173 name: "Rust".into(),
26174 ..LanguageConfig::default()
26175 },
26176 None,
26177 ));
26178
26179 let mut cx = EditorTestContext::new(cx).await;
26180 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26181 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26182
26183 cx.update_editor(|editor, window, cx| {
26184 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26185 editor.paste(&Paste, window, cx);
26186 });
26187
26188 cx.assert_editor_state(&format!(
26189 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26190 ));
26191}
26192
26193#[gpui::test]
26194async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26195 cx: &mut TestAppContext,
26196) {
26197 init_test(cx, |_| {});
26198
26199 let url = "https://zed.dev";
26200
26201 let markdown_language = Arc::new(Language::new(
26202 LanguageConfig {
26203 name: "Markdown".into(),
26204 ..LanguageConfig::default()
26205 },
26206 None,
26207 ));
26208
26209 let (editor, cx) = cx.add_window_view(|window, cx| {
26210 let multi_buffer = MultiBuffer::build_multi(
26211 [
26212 ("this will embed -> link", vec![Point::row_range(0..1)]),
26213 ("this will replace -> link", vec![Point::row_range(0..1)]),
26214 ],
26215 cx,
26216 );
26217 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26218 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26219 s.select_ranges(vec![
26220 Point::new(0, 19)..Point::new(0, 23),
26221 Point::new(1, 21)..Point::new(1, 25),
26222 ])
26223 });
26224 let first_buffer_id = multi_buffer
26225 .read(cx)
26226 .excerpt_buffer_ids()
26227 .into_iter()
26228 .next()
26229 .unwrap();
26230 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26231 first_buffer.update(cx, |buffer, cx| {
26232 buffer.set_language(Some(markdown_language.clone()), cx);
26233 });
26234
26235 editor
26236 });
26237 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26238
26239 cx.update_editor(|editor, window, cx| {
26240 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26241 editor.paste(&Paste, window, cx);
26242 });
26243
26244 cx.assert_editor_state(&format!(
26245 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26246 ));
26247}
26248
26249#[track_caller]
26250fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26251 editor
26252 .all_inlays(cx)
26253 .into_iter()
26254 .filter_map(|inlay| inlay.get_color())
26255 .map(Rgba::from)
26256 .collect()
26257}