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 assert_eq!(
11929 params.context.only,
11930 Some(vec!["code-action-1".into(), "code-action-2".into()])
11931 );
11932 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
11933 Ok(Some(vec![
11934 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11935 kind: Some("code-action-1".into()),
11936 edit: Some(lsp::WorkspaceEdit::new(
11937 [(
11938 uri.clone(),
11939 vec![lsp::TextEdit::new(
11940 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11941 "applied-code-action-1-edit\n".to_string(),
11942 )],
11943 )]
11944 .into_iter()
11945 .collect(),
11946 )),
11947 command: Some(lsp::Command {
11948 command: "the-command-for-code-action-1".into(),
11949 ..Default::default()
11950 }),
11951 ..Default::default()
11952 }),
11953 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
11954 kind: Some("code-action-2".into()),
11955 edit: Some(lsp::WorkspaceEdit::new(
11956 [(
11957 uri,
11958 vec![lsp::TextEdit::new(
11959 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
11960 "applied-code-action-2-edit\n".to_string(),
11961 )],
11962 )]
11963 .into_iter()
11964 .collect(),
11965 )),
11966 ..Default::default()
11967 }),
11968 ]))
11969 },
11970 );
11971
11972 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
11973 move |params, _| async move { Ok(params) }
11974 });
11975
11976 let command_lock = Arc::new(futures::lock::Mutex::new(()));
11977 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
11978 let fake = fake_server.clone();
11979 let lock = command_lock.clone();
11980 move |params, _| {
11981 assert_eq!(params.command, "the-command-for-code-action-1");
11982 let fake = fake.clone();
11983 let lock = lock.clone();
11984 async move {
11985 lock.lock().await;
11986 fake.server
11987 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
11988 label: None,
11989 edit: lsp::WorkspaceEdit {
11990 changes: Some(
11991 [(
11992 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
11993 vec![lsp::TextEdit {
11994 range: lsp::Range::new(
11995 lsp::Position::new(0, 0),
11996 lsp::Position::new(0, 0),
11997 ),
11998 new_text: "applied-code-action-1-command\n".into(),
11999 }],
12000 )]
12001 .into_iter()
12002 .collect(),
12003 ),
12004 ..Default::default()
12005 },
12006 })
12007 .await
12008 .into_response()
12009 .unwrap();
12010 Ok(Some(json!(null)))
12011 }
12012 }
12013 });
12014
12015 cx.executor().start_waiting();
12016 editor
12017 .update_in(cx, |editor, window, cx| {
12018 editor.perform_format(
12019 project.clone(),
12020 FormatTrigger::Manual,
12021 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12022 window,
12023 cx,
12024 )
12025 })
12026 .unwrap()
12027 .await;
12028 editor.update(cx, |editor, cx| {
12029 assert_eq!(
12030 editor.text(cx),
12031 r#"
12032 applied-code-action-2-edit
12033 applied-code-action-1-command
12034 applied-code-action-1-edit
12035 applied-formatting
12036 one
12037 two
12038 three
12039 "#
12040 .unindent()
12041 );
12042 });
12043
12044 editor.update_in(cx, |editor, window, cx| {
12045 editor.undo(&Default::default(), window, cx);
12046 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12047 });
12048
12049 // Perform a manual edit while waiting for an LSP command
12050 // that's being run as part of a formatting code action.
12051 let lock_guard = command_lock.lock().await;
12052 let format = editor
12053 .update_in(cx, |editor, window, cx| {
12054 editor.perform_format(
12055 project.clone(),
12056 FormatTrigger::Manual,
12057 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12058 window,
12059 cx,
12060 )
12061 })
12062 .unwrap();
12063 cx.run_until_parked();
12064 editor.update(cx, |editor, cx| {
12065 assert_eq!(
12066 editor.text(cx),
12067 r#"
12068 applied-code-action-1-edit
12069 applied-formatting
12070 one
12071 two
12072 three
12073 "#
12074 .unindent()
12075 );
12076
12077 editor.buffer.update(cx, |buffer, cx| {
12078 let ix = buffer.len(cx);
12079 buffer.edit([(ix..ix, "edited\n")], None, cx);
12080 });
12081 });
12082
12083 // Allow the LSP command to proceed. Because the buffer was edited,
12084 // the second code action will not be run.
12085 drop(lock_guard);
12086 format.await;
12087 editor.update_in(cx, |editor, window, cx| {
12088 assert_eq!(
12089 editor.text(cx),
12090 r#"
12091 applied-code-action-1-command
12092 applied-code-action-1-edit
12093 applied-formatting
12094 one
12095 two
12096 three
12097 edited
12098 "#
12099 .unindent()
12100 );
12101
12102 // The manual edit is undone first, because it is the last thing the user did
12103 // (even though the command completed afterwards).
12104 editor.undo(&Default::default(), window, cx);
12105 assert_eq!(
12106 editor.text(cx),
12107 r#"
12108 applied-code-action-1-command
12109 applied-code-action-1-edit
12110 applied-formatting
12111 one
12112 two
12113 three
12114 "#
12115 .unindent()
12116 );
12117
12118 // All the formatting (including the command, which completed after the manual edit)
12119 // is undone together.
12120 editor.undo(&Default::default(), window, cx);
12121 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12122 });
12123}
12124
12125#[gpui::test]
12126async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12127 init_test(cx, |settings| {
12128 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Vec(vec![
12129 Formatter::LanguageServer { name: None },
12130 ])))
12131 });
12132
12133 let fs = FakeFs::new(cx.executor());
12134 fs.insert_file(path!("/file.ts"), Default::default()).await;
12135
12136 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12137
12138 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12139 language_registry.add(Arc::new(Language::new(
12140 LanguageConfig {
12141 name: "TypeScript".into(),
12142 matcher: LanguageMatcher {
12143 path_suffixes: vec!["ts".to_string()],
12144 ..Default::default()
12145 },
12146 ..LanguageConfig::default()
12147 },
12148 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12149 )));
12150 update_test_language_settings(cx, |settings| {
12151 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12152 });
12153 let mut fake_servers = language_registry.register_fake_lsp(
12154 "TypeScript",
12155 FakeLspAdapter {
12156 capabilities: lsp::ServerCapabilities {
12157 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12158 ..Default::default()
12159 },
12160 ..Default::default()
12161 },
12162 );
12163
12164 let buffer = project
12165 .update(cx, |project, cx| {
12166 project.open_local_buffer(path!("/file.ts"), cx)
12167 })
12168 .await
12169 .unwrap();
12170
12171 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12172 let (editor, cx) = cx.add_window_view(|window, cx| {
12173 build_editor_with_project(project.clone(), buffer, window, cx)
12174 });
12175 editor.update_in(cx, |editor, window, cx| {
12176 editor.set_text(
12177 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12178 window,
12179 cx,
12180 )
12181 });
12182
12183 cx.executor().start_waiting();
12184 let fake_server = fake_servers.next().await.unwrap();
12185
12186 let format = editor
12187 .update_in(cx, |editor, window, cx| {
12188 editor.perform_code_action_kind(
12189 project.clone(),
12190 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12191 window,
12192 cx,
12193 )
12194 })
12195 .unwrap();
12196 fake_server
12197 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12198 assert_eq!(
12199 params.text_document.uri,
12200 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12201 );
12202 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12203 lsp::CodeAction {
12204 title: "Organize Imports".to_string(),
12205 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12206 edit: Some(lsp::WorkspaceEdit {
12207 changes: Some(
12208 [(
12209 params.text_document.uri.clone(),
12210 vec![lsp::TextEdit::new(
12211 lsp::Range::new(
12212 lsp::Position::new(1, 0),
12213 lsp::Position::new(2, 0),
12214 ),
12215 "".to_string(),
12216 )],
12217 )]
12218 .into_iter()
12219 .collect(),
12220 ),
12221 ..Default::default()
12222 }),
12223 ..Default::default()
12224 },
12225 )]))
12226 })
12227 .next()
12228 .await;
12229 cx.executor().start_waiting();
12230 format.await;
12231 assert_eq!(
12232 editor.update(cx, |editor, cx| editor.text(cx)),
12233 "import { a } from 'module';\n\nconst x = a;\n"
12234 );
12235
12236 editor.update_in(cx, |editor, window, cx| {
12237 editor.set_text(
12238 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12239 window,
12240 cx,
12241 )
12242 });
12243 // Ensure we don't lock if code action hangs.
12244 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12245 move |params, _| async move {
12246 assert_eq!(
12247 params.text_document.uri,
12248 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12249 );
12250 futures::future::pending::<()>().await;
12251 unreachable!()
12252 },
12253 );
12254 let format = editor
12255 .update_in(cx, |editor, window, cx| {
12256 editor.perform_code_action_kind(
12257 project,
12258 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12259 window,
12260 cx,
12261 )
12262 })
12263 .unwrap();
12264 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12265 cx.executor().start_waiting();
12266 format.await;
12267 assert_eq!(
12268 editor.update(cx, |editor, cx| editor.text(cx)),
12269 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12270 );
12271}
12272
12273#[gpui::test]
12274async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12275 init_test(cx, |_| {});
12276
12277 let mut cx = EditorLspTestContext::new_rust(
12278 lsp::ServerCapabilities {
12279 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12280 ..Default::default()
12281 },
12282 cx,
12283 )
12284 .await;
12285
12286 cx.set_state(indoc! {"
12287 one.twoˇ
12288 "});
12289
12290 // The format request takes a long time. When it completes, it inserts
12291 // a newline and an indent before the `.`
12292 cx.lsp
12293 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12294 let executor = cx.background_executor().clone();
12295 async move {
12296 executor.timer(Duration::from_millis(100)).await;
12297 Ok(Some(vec![lsp::TextEdit {
12298 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12299 new_text: "\n ".into(),
12300 }]))
12301 }
12302 });
12303
12304 // Submit a format request.
12305 let format_1 = cx
12306 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12307 .unwrap();
12308 cx.executor().run_until_parked();
12309
12310 // Submit a second format request.
12311 let format_2 = cx
12312 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12313 .unwrap();
12314 cx.executor().run_until_parked();
12315
12316 // Wait for both format requests to complete
12317 cx.executor().advance_clock(Duration::from_millis(200));
12318 cx.executor().start_waiting();
12319 format_1.await.unwrap();
12320 cx.executor().start_waiting();
12321 format_2.await.unwrap();
12322
12323 // The formatting edits only happens once.
12324 cx.assert_editor_state(indoc! {"
12325 one
12326 .twoˇ
12327 "});
12328}
12329
12330#[gpui::test]
12331async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12332 init_test(cx, |settings| {
12333 settings.defaults.formatter = Some(SelectedFormatter::Auto)
12334 });
12335
12336 let mut cx = EditorLspTestContext::new_rust(
12337 lsp::ServerCapabilities {
12338 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12339 ..Default::default()
12340 },
12341 cx,
12342 )
12343 .await;
12344
12345 // Set up a buffer white some trailing whitespace and no trailing newline.
12346 cx.set_state(
12347 &[
12348 "one ", //
12349 "twoˇ", //
12350 "three ", //
12351 "four", //
12352 ]
12353 .join("\n"),
12354 );
12355
12356 // Submit a format request.
12357 let format = cx
12358 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12359 .unwrap();
12360
12361 // Record which buffer changes have been sent to the language server
12362 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12363 cx.lsp
12364 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12365 let buffer_changes = buffer_changes.clone();
12366 move |params, _| {
12367 buffer_changes.lock().extend(
12368 params
12369 .content_changes
12370 .into_iter()
12371 .map(|e| (e.range.unwrap(), e.text)),
12372 );
12373 }
12374 });
12375
12376 // Handle formatting requests to the language server.
12377 cx.lsp
12378 .set_request_handler::<lsp::request::Formatting, _, _>({
12379 let buffer_changes = buffer_changes.clone();
12380 move |_, _| {
12381 // When formatting is requested, trailing whitespace has already been stripped,
12382 // and the trailing newline has already been added.
12383 assert_eq!(
12384 &buffer_changes.lock()[1..],
12385 &[
12386 (
12387 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12388 "".into()
12389 ),
12390 (
12391 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12392 "".into()
12393 ),
12394 (
12395 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12396 "\n".into()
12397 ),
12398 ]
12399 );
12400
12401 // Insert blank lines between each line of the buffer.
12402 async move {
12403 Ok(Some(vec![
12404 lsp::TextEdit {
12405 range: lsp::Range::new(
12406 lsp::Position::new(1, 0),
12407 lsp::Position::new(1, 0),
12408 ),
12409 new_text: "\n".into(),
12410 },
12411 lsp::TextEdit {
12412 range: lsp::Range::new(
12413 lsp::Position::new(2, 0),
12414 lsp::Position::new(2, 0),
12415 ),
12416 new_text: "\n".into(),
12417 },
12418 ]))
12419 }
12420 }
12421 });
12422
12423 // After formatting the buffer, the trailing whitespace is stripped,
12424 // a newline is appended, and the edits provided by the language server
12425 // have been applied.
12426 format.await.unwrap();
12427 cx.assert_editor_state(
12428 &[
12429 "one", //
12430 "", //
12431 "twoˇ", //
12432 "", //
12433 "three", //
12434 "four", //
12435 "", //
12436 ]
12437 .join("\n"),
12438 );
12439
12440 // Undoing the formatting undoes the trailing whitespace removal, the
12441 // trailing newline, and the LSP edits.
12442 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12443 cx.assert_editor_state(
12444 &[
12445 "one ", //
12446 "twoˇ", //
12447 "three ", //
12448 "four", //
12449 ]
12450 .join("\n"),
12451 );
12452}
12453
12454#[gpui::test]
12455async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12456 cx: &mut TestAppContext,
12457) {
12458 init_test(cx, |_| {});
12459
12460 cx.update(|cx| {
12461 cx.update_global::<SettingsStore, _>(|settings, cx| {
12462 settings.update_user_settings(cx, |settings| {
12463 settings.editor.auto_signature_help = Some(true);
12464 });
12465 });
12466 });
12467
12468 let mut cx = EditorLspTestContext::new_rust(
12469 lsp::ServerCapabilities {
12470 signature_help_provider: Some(lsp::SignatureHelpOptions {
12471 ..Default::default()
12472 }),
12473 ..Default::default()
12474 },
12475 cx,
12476 )
12477 .await;
12478
12479 let language = Language::new(
12480 LanguageConfig {
12481 name: "Rust".into(),
12482 brackets: BracketPairConfig {
12483 pairs: vec![
12484 BracketPair {
12485 start: "{".to_string(),
12486 end: "}".to_string(),
12487 close: true,
12488 surround: true,
12489 newline: true,
12490 },
12491 BracketPair {
12492 start: "(".to_string(),
12493 end: ")".to_string(),
12494 close: true,
12495 surround: true,
12496 newline: true,
12497 },
12498 BracketPair {
12499 start: "/*".to_string(),
12500 end: " */".to_string(),
12501 close: true,
12502 surround: true,
12503 newline: true,
12504 },
12505 BracketPair {
12506 start: "[".to_string(),
12507 end: "]".to_string(),
12508 close: false,
12509 surround: false,
12510 newline: true,
12511 },
12512 BracketPair {
12513 start: "\"".to_string(),
12514 end: "\"".to_string(),
12515 close: true,
12516 surround: true,
12517 newline: false,
12518 },
12519 BracketPair {
12520 start: "<".to_string(),
12521 end: ">".to_string(),
12522 close: false,
12523 surround: true,
12524 newline: true,
12525 },
12526 ],
12527 ..Default::default()
12528 },
12529 autoclose_before: "})]".to_string(),
12530 ..Default::default()
12531 },
12532 Some(tree_sitter_rust::LANGUAGE.into()),
12533 );
12534 let language = Arc::new(language);
12535
12536 cx.language_registry().add(language.clone());
12537 cx.update_buffer(|buffer, cx| {
12538 buffer.set_language(Some(language), cx);
12539 });
12540
12541 cx.set_state(
12542 &r#"
12543 fn main() {
12544 sampleˇ
12545 }
12546 "#
12547 .unindent(),
12548 );
12549
12550 cx.update_editor(|editor, window, cx| {
12551 editor.handle_input("(", window, cx);
12552 });
12553 cx.assert_editor_state(
12554 &"
12555 fn main() {
12556 sample(ˇ)
12557 }
12558 "
12559 .unindent(),
12560 );
12561
12562 let mocked_response = lsp::SignatureHelp {
12563 signatures: vec![lsp::SignatureInformation {
12564 label: "fn sample(param1: u8, param2: u8)".to_string(),
12565 documentation: None,
12566 parameters: Some(vec![
12567 lsp::ParameterInformation {
12568 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12569 documentation: None,
12570 },
12571 lsp::ParameterInformation {
12572 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12573 documentation: None,
12574 },
12575 ]),
12576 active_parameter: None,
12577 }],
12578 active_signature: Some(0),
12579 active_parameter: Some(0),
12580 };
12581 handle_signature_help_request(&mut cx, mocked_response).await;
12582
12583 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12584 .await;
12585
12586 cx.editor(|editor, _, _| {
12587 let signature_help_state = editor.signature_help_state.popover().cloned();
12588 let signature = signature_help_state.unwrap();
12589 assert_eq!(
12590 signature.signatures[signature.current_signature].label,
12591 "fn sample(param1: u8, param2: u8)"
12592 );
12593 });
12594}
12595
12596#[gpui::test]
12597async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
12598 init_test(cx, |_| {});
12599
12600 cx.update(|cx| {
12601 cx.update_global::<SettingsStore, _>(|settings, cx| {
12602 settings.update_user_settings(cx, |settings| {
12603 settings.editor.auto_signature_help = Some(false);
12604 settings.editor.show_signature_help_after_edits = Some(false);
12605 });
12606 });
12607 });
12608
12609 let mut cx = EditorLspTestContext::new_rust(
12610 lsp::ServerCapabilities {
12611 signature_help_provider: Some(lsp::SignatureHelpOptions {
12612 ..Default::default()
12613 }),
12614 ..Default::default()
12615 },
12616 cx,
12617 )
12618 .await;
12619
12620 let language = Language::new(
12621 LanguageConfig {
12622 name: "Rust".into(),
12623 brackets: BracketPairConfig {
12624 pairs: vec![
12625 BracketPair {
12626 start: "{".to_string(),
12627 end: "}".to_string(),
12628 close: true,
12629 surround: true,
12630 newline: true,
12631 },
12632 BracketPair {
12633 start: "(".to_string(),
12634 end: ")".to_string(),
12635 close: true,
12636 surround: true,
12637 newline: true,
12638 },
12639 BracketPair {
12640 start: "/*".to_string(),
12641 end: " */".to_string(),
12642 close: true,
12643 surround: true,
12644 newline: true,
12645 },
12646 BracketPair {
12647 start: "[".to_string(),
12648 end: "]".to_string(),
12649 close: false,
12650 surround: false,
12651 newline: true,
12652 },
12653 BracketPair {
12654 start: "\"".to_string(),
12655 end: "\"".to_string(),
12656 close: true,
12657 surround: true,
12658 newline: false,
12659 },
12660 BracketPair {
12661 start: "<".to_string(),
12662 end: ">".to_string(),
12663 close: false,
12664 surround: true,
12665 newline: true,
12666 },
12667 ],
12668 ..Default::default()
12669 },
12670 autoclose_before: "})]".to_string(),
12671 ..Default::default()
12672 },
12673 Some(tree_sitter_rust::LANGUAGE.into()),
12674 );
12675 let language = Arc::new(language);
12676
12677 cx.language_registry().add(language.clone());
12678 cx.update_buffer(|buffer, cx| {
12679 buffer.set_language(Some(language), cx);
12680 });
12681
12682 // Ensure that signature_help is not called when no signature help is enabled.
12683 cx.set_state(
12684 &r#"
12685 fn main() {
12686 sampleˇ
12687 }
12688 "#
12689 .unindent(),
12690 );
12691 cx.update_editor(|editor, window, cx| {
12692 editor.handle_input("(", window, cx);
12693 });
12694 cx.assert_editor_state(
12695 &"
12696 fn main() {
12697 sample(ˇ)
12698 }
12699 "
12700 .unindent(),
12701 );
12702 cx.editor(|editor, _, _| {
12703 assert!(editor.signature_help_state.task().is_none());
12704 });
12705
12706 let mocked_response = lsp::SignatureHelp {
12707 signatures: vec![lsp::SignatureInformation {
12708 label: "fn sample(param1: u8, param2: u8)".to_string(),
12709 documentation: None,
12710 parameters: Some(vec![
12711 lsp::ParameterInformation {
12712 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12713 documentation: None,
12714 },
12715 lsp::ParameterInformation {
12716 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12717 documentation: None,
12718 },
12719 ]),
12720 active_parameter: None,
12721 }],
12722 active_signature: Some(0),
12723 active_parameter: Some(0),
12724 };
12725
12726 // Ensure that signature_help is called when enabled afte edits
12727 cx.update(|_, cx| {
12728 cx.update_global::<SettingsStore, _>(|settings, cx| {
12729 settings.update_user_settings(cx, |settings| {
12730 settings.editor.auto_signature_help = Some(false);
12731 settings.editor.show_signature_help_after_edits = Some(true);
12732 });
12733 });
12734 });
12735 cx.set_state(
12736 &r#"
12737 fn main() {
12738 sampleˇ
12739 }
12740 "#
12741 .unindent(),
12742 );
12743 cx.update_editor(|editor, window, cx| {
12744 editor.handle_input("(", window, cx);
12745 });
12746 cx.assert_editor_state(
12747 &"
12748 fn main() {
12749 sample(ˇ)
12750 }
12751 "
12752 .unindent(),
12753 );
12754 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12755 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12756 .await;
12757 cx.update_editor(|editor, _, _| {
12758 let signature_help_state = editor.signature_help_state.popover().cloned();
12759 assert!(signature_help_state.is_some());
12760 let signature = signature_help_state.unwrap();
12761 assert_eq!(
12762 signature.signatures[signature.current_signature].label,
12763 "fn sample(param1: u8, param2: u8)"
12764 );
12765 editor.signature_help_state = SignatureHelpState::default();
12766 });
12767
12768 // Ensure that signature_help is called when auto signature help override is enabled
12769 cx.update(|_, cx| {
12770 cx.update_global::<SettingsStore, _>(|settings, cx| {
12771 settings.update_user_settings(cx, |settings| {
12772 settings.editor.auto_signature_help = Some(true);
12773 settings.editor.show_signature_help_after_edits = Some(false);
12774 });
12775 });
12776 });
12777 cx.set_state(
12778 &r#"
12779 fn main() {
12780 sampleˇ
12781 }
12782 "#
12783 .unindent(),
12784 );
12785 cx.update_editor(|editor, window, cx| {
12786 editor.handle_input("(", window, cx);
12787 });
12788 cx.assert_editor_state(
12789 &"
12790 fn main() {
12791 sample(ˇ)
12792 }
12793 "
12794 .unindent(),
12795 );
12796 handle_signature_help_request(&mut cx, mocked_response).await;
12797 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12798 .await;
12799 cx.editor(|editor, _, _| {
12800 let signature_help_state = editor.signature_help_state.popover().cloned();
12801 assert!(signature_help_state.is_some());
12802 let signature = signature_help_state.unwrap();
12803 assert_eq!(
12804 signature.signatures[signature.current_signature].label,
12805 "fn sample(param1: u8, param2: u8)"
12806 );
12807 });
12808}
12809
12810#[gpui::test]
12811async fn test_signature_help(cx: &mut TestAppContext) {
12812 init_test(cx, |_| {});
12813 cx.update(|cx| {
12814 cx.update_global::<SettingsStore, _>(|settings, cx| {
12815 settings.update_user_settings(cx, |settings| {
12816 settings.editor.auto_signature_help = Some(true);
12817 });
12818 });
12819 });
12820
12821 let mut cx = EditorLspTestContext::new_rust(
12822 lsp::ServerCapabilities {
12823 signature_help_provider: Some(lsp::SignatureHelpOptions {
12824 ..Default::default()
12825 }),
12826 ..Default::default()
12827 },
12828 cx,
12829 )
12830 .await;
12831
12832 // A test that directly calls `show_signature_help`
12833 cx.update_editor(|editor, window, cx| {
12834 editor.show_signature_help(&ShowSignatureHelp, window, cx);
12835 });
12836
12837 let mocked_response = lsp::SignatureHelp {
12838 signatures: vec![lsp::SignatureInformation {
12839 label: "fn sample(param1: u8, param2: u8)".to_string(),
12840 documentation: None,
12841 parameters: Some(vec![
12842 lsp::ParameterInformation {
12843 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12844 documentation: None,
12845 },
12846 lsp::ParameterInformation {
12847 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12848 documentation: None,
12849 },
12850 ]),
12851 active_parameter: None,
12852 }],
12853 active_signature: Some(0),
12854 active_parameter: Some(0),
12855 };
12856 handle_signature_help_request(&mut cx, mocked_response).await;
12857
12858 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12859 .await;
12860
12861 cx.editor(|editor, _, _| {
12862 let signature_help_state = editor.signature_help_state.popover().cloned();
12863 assert!(signature_help_state.is_some());
12864 let signature = signature_help_state.unwrap();
12865 assert_eq!(
12866 signature.signatures[signature.current_signature].label,
12867 "fn sample(param1: u8, param2: u8)"
12868 );
12869 });
12870
12871 // When exiting outside from inside the brackets, `signature_help` is closed.
12872 cx.set_state(indoc! {"
12873 fn main() {
12874 sample(ˇ);
12875 }
12876
12877 fn sample(param1: u8, param2: u8) {}
12878 "});
12879
12880 cx.update_editor(|editor, window, cx| {
12881 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12882 s.select_ranges([0..0])
12883 });
12884 });
12885
12886 let mocked_response = lsp::SignatureHelp {
12887 signatures: Vec::new(),
12888 active_signature: None,
12889 active_parameter: None,
12890 };
12891 handle_signature_help_request(&mut cx, mocked_response).await;
12892
12893 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
12894 .await;
12895
12896 cx.editor(|editor, _, _| {
12897 assert!(!editor.signature_help_state.is_shown());
12898 });
12899
12900 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
12901 cx.set_state(indoc! {"
12902 fn main() {
12903 sample(ˇ);
12904 }
12905
12906 fn sample(param1: u8, param2: u8) {}
12907 "});
12908
12909 let mocked_response = lsp::SignatureHelp {
12910 signatures: vec![lsp::SignatureInformation {
12911 label: "fn sample(param1: u8, param2: u8)".to_string(),
12912 documentation: None,
12913 parameters: Some(vec![
12914 lsp::ParameterInformation {
12915 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12916 documentation: None,
12917 },
12918 lsp::ParameterInformation {
12919 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12920 documentation: None,
12921 },
12922 ]),
12923 active_parameter: None,
12924 }],
12925 active_signature: Some(0),
12926 active_parameter: Some(0),
12927 };
12928 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12929 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12930 .await;
12931 cx.editor(|editor, _, _| {
12932 assert!(editor.signature_help_state.is_shown());
12933 });
12934
12935 // Restore the popover with more parameter input
12936 cx.set_state(indoc! {"
12937 fn main() {
12938 sample(param1, param2ˇ);
12939 }
12940
12941 fn sample(param1: u8, param2: u8) {}
12942 "});
12943
12944 let mocked_response = lsp::SignatureHelp {
12945 signatures: vec![lsp::SignatureInformation {
12946 label: "fn sample(param1: u8, param2: u8)".to_string(),
12947 documentation: None,
12948 parameters: Some(vec![
12949 lsp::ParameterInformation {
12950 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12951 documentation: None,
12952 },
12953 lsp::ParameterInformation {
12954 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12955 documentation: None,
12956 },
12957 ]),
12958 active_parameter: None,
12959 }],
12960 active_signature: Some(0),
12961 active_parameter: Some(1),
12962 };
12963 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
12964 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12965 .await;
12966
12967 // When selecting a range, the popover is gone.
12968 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
12969 cx.update_editor(|editor, window, cx| {
12970 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12971 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
12972 })
12973 });
12974 cx.assert_editor_state(indoc! {"
12975 fn main() {
12976 sample(param1, «ˇparam2»);
12977 }
12978
12979 fn sample(param1: u8, param2: u8) {}
12980 "});
12981 cx.editor(|editor, _, _| {
12982 assert!(!editor.signature_help_state.is_shown());
12983 });
12984
12985 // When unselecting again, the popover is back if within the brackets.
12986 cx.update_editor(|editor, window, cx| {
12987 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
12988 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
12989 })
12990 });
12991 cx.assert_editor_state(indoc! {"
12992 fn main() {
12993 sample(param1, ˇparam2);
12994 }
12995
12996 fn sample(param1: u8, param2: u8) {}
12997 "});
12998 handle_signature_help_request(&mut cx, mocked_response).await;
12999 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13000 .await;
13001 cx.editor(|editor, _, _| {
13002 assert!(editor.signature_help_state.is_shown());
13003 });
13004
13005 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13006 cx.update_editor(|editor, window, cx| {
13007 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13008 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13009 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13010 })
13011 });
13012 cx.assert_editor_state(indoc! {"
13013 fn main() {
13014 sample(param1, ˇparam2);
13015 }
13016
13017 fn sample(param1: u8, param2: u8) {}
13018 "});
13019
13020 let mocked_response = lsp::SignatureHelp {
13021 signatures: vec![lsp::SignatureInformation {
13022 label: "fn sample(param1: u8, param2: u8)".to_string(),
13023 documentation: None,
13024 parameters: Some(vec![
13025 lsp::ParameterInformation {
13026 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13027 documentation: None,
13028 },
13029 lsp::ParameterInformation {
13030 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13031 documentation: None,
13032 },
13033 ]),
13034 active_parameter: None,
13035 }],
13036 active_signature: Some(0),
13037 active_parameter: Some(1),
13038 };
13039 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13040 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13041 .await;
13042 cx.update_editor(|editor, _, cx| {
13043 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13044 });
13045 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13046 .await;
13047 cx.update_editor(|editor, window, cx| {
13048 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13049 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13050 })
13051 });
13052 cx.assert_editor_state(indoc! {"
13053 fn main() {
13054 sample(param1, «ˇparam2»);
13055 }
13056
13057 fn sample(param1: u8, param2: u8) {}
13058 "});
13059 cx.update_editor(|editor, window, cx| {
13060 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13061 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13062 })
13063 });
13064 cx.assert_editor_state(indoc! {"
13065 fn main() {
13066 sample(param1, ˇparam2);
13067 }
13068
13069 fn sample(param1: u8, param2: u8) {}
13070 "});
13071 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13072 .await;
13073}
13074
13075#[gpui::test]
13076async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13077 init_test(cx, |_| {});
13078
13079 let mut cx = EditorLspTestContext::new_rust(
13080 lsp::ServerCapabilities {
13081 signature_help_provider: Some(lsp::SignatureHelpOptions {
13082 ..Default::default()
13083 }),
13084 ..Default::default()
13085 },
13086 cx,
13087 )
13088 .await;
13089
13090 cx.set_state(indoc! {"
13091 fn main() {
13092 overloadedˇ
13093 }
13094 "});
13095
13096 cx.update_editor(|editor, window, cx| {
13097 editor.handle_input("(", window, cx);
13098 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13099 });
13100
13101 // Mock response with 3 signatures
13102 let mocked_response = lsp::SignatureHelp {
13103 signatures: vec![
13104 lsp::SignatureInformation {
13105 label: "fn overloaded(x: i32)".to_string(),
13106 documentation: None,
13107 parameters: Some(vec![lsp::ParameterInformation {
13108 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13109 documentation: None,
13110 }]),
13111 active_parameter: None,
13112 },
13113 lsp::SignatureInformation {
13114 label: "fn overloaded(x: i32, y: i32)".to_string(),
13115 documentation: None,
13116 parameters: Some(vec![
13117 lsp::ParameterInformation {
13118 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13119 documentation: None,
13120 },
13121 lsp::ParameterInformation {
13122 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13123 documentation: None,
13124 },
13125 ]),
13126 active_parameter: None,
13127 },
13128 lsp::SignatureInformation {
13129 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13130 documentation: None,
13131 parameters: Some(vec![
13132 lsp::ParameterInformation {
13133 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13134 documentation: None,
13135 },
13136 lsp::ParameterInformation {
13137 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13138 documentation: None,
13139 },
13140 lsp::ParameterInformation {
13141 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13142 documentation: None,
13143 },
13144 ]),
13145 active_parameter: None,
13146 },
13147 ],
13148 active_signature: Some(1),
13149 active_parameter: Some(0),
13150 };
13151 handle_signature_help_request(&mut cx, mocked_response).await;
13152
13153 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13154 .await;
13155
13156 // Verify we have multiple signatures and the right one is selected
13157 cx.editor(|editor, _, _| {
13158 let popover = editor.signature_help_state.popover().cloned().unwrap();
13159 assert_eq!(popover.signatures.len(), 3);
13160 // active_signature was 1, so that should be the current
13161 assert_eq!(popover.current_signature, 1);
13162 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13163 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13164 assert_eq!(
13165 popover.signatures[2].label,
13166 "fn overloaded(x: i32, y: i32, z: i32)"
13167 );
13168 });
13169
13170 // Test navigation functionality
13171 cx.update_editor(|editor, window, cx| {
13172 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13173 });
13174
13175 cx.editor(|editor, _, _| {
13176 let popover = editor.signature_help_state.popover().cloned().unwrap();
13177 assert_eq!(popover.current_signature, 2);
13178 });
13179
13180 // Test wrap around
13181 cx.update_editor(|editor, window, cx| {
13182 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13183 });
13184
13185 cx.editor(|editor, _, _| {
13186 let popover = editor.signature_help_state.popover().cloned().unwrap();
13187 assert_eq!(popover.current_signature, 0);
13188 });
13189
13190 // Test previous navigation
13191 cx.update_editor(|editor, window, cx| {
13192 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13193 });
13194
13195 cx.editor(|editor, _, _| {
13196 let popover = editor.signature_help_state.popover().cloned().unwrap();
13197 assert_eq!(popover.current_signature, 2);
13198 });
13199}
13200
13201#[gpui::test]
13202async fn test_completion_mode(cx: &mut TestAppContext) {
13203 init_test(cx, |_| {});
13204 let mut cx = EditorLspTestContext::new_rust(
13205 lsp::ServerCapabilities {
13206 completion_provider: Some(lsp::CompletionOptions {
13207 resolve_provider: Some(true),
13208 ..Default::default()
13209 }),
13210 ..Default::default()
13211 },
13212 cx,
13213 )
13214 .await;
13215
13216 struct Run {
13217 run_description: &'static str,
13218 initial_state: String,
13219 buffer_marked_text: String,
13220 completion_label: &'static str,
13221 completion_text: &'static str,
13222 expected_with_insert_mode: String,
13223 expected_with_replace_mode: String,
13224 expected_with_replace_subsequence_mode: String,
13225 expected_with_replace_suffix_mode: String,
13226 }
13227
13228 let runs = [
13229 Run {
13230 run_description: "Start of word matches completion text",
13231 initial_state: "before ediˇ after".into(),
13232 buffer_marked_text: "before <edi|> after".into(),
13233 completion_label: "editor",
13234 completion_text: "editor",
13235 expected_with_insert_mode: "before editorˇ after".into(),
13236 expected_with_replace_mode: "before editorˇ after".into(),
13237 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13238 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13239 },
13240 Run {
13241 run_description: "Accept same text at the middle of the word",
13242 initial_state: "before ediˇtor after".into(),
13243 buffer_marked_text: "before <edi|tor> after".into(),
13244 completion_label: "editor",
13245 completion_text: "editor",
13246 expected_with_insert_mode: "before editorˇtor after".into(),
13247 expected_with_replace_mode: "before editorˇ after".into(),
13248 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13249 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13250 },
13251 Run {
13252 run_description: "End of word matches completion text -- cursor at end",
13253 initial_state: "before torˇ after".into(),
13254 buffer_marked_text: "before <tor|> after".into(),
13255 completion_label: "editor",
13256 completion_text: "editor",
13257 expected_with_insert_mode: "before editorˇ after".into(),
13258 expected_with_replace_mode: "before editorˇ after".into(),
13259 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13260 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13261 },
13262 Run {
13263 run_description: "End of word matches completion text -- cursor at start",
13264 initial_state: "before ˇtor after".into(),
13265 buffer_marked_text: "before <|tor> after".into(),
13266 completion_label: "editor",
13267 completion_text: "editor",
13268 expected_with_insert_mode: "before editorˇtor after".into(),
13269 expected_with_replace_mode: "before editorˇ after".into(),
13270 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13271 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13272 },
13273 Run {
13274 run_description: "Prepend text containing whitespace",
13275 initial_state: "pˇfield: bool".into(),
13276 buffer_marked_text: "<p|field>: bool".into(),
13277 completion_label: "pub ",
13278 completion_text: "pub ",
13279 expected_with_insert_mode: "pub ˇfield: bool".into(),
13280 expected_with_replace_mode: "pub ˇ: bool".into(),
13281 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13282 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13283 },
13284 Run {
13285 run_description: "Add element to start of list",
13286 initial_state: "[element_ˇelement_2]".into(),
13287 buffer_marked_text: "[<element_|element_2>]".into(),
13288 completion_label: "element_1",
13289 completion_text: "element_1",
13290 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13291 expected_with_replace_mode: "[element_1ˇ]".into(),
13292 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13293 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13294 },
13295 Run {
13296 run_description: "Add element to start of list -- first and second elements are equal",
13297 initial_state: "[elˇelement]".into(),
13298 buffer_marked_text: "[<el|element>]".into(),
13299 completion_label: "element",
13300 completion_text: "element",
13301 expected_with_insert_mode: "[elementˇelement]".into(),
13302 expected_with_replace_mode: "[elementˇ]".into(),
13303 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13304 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13305 },
13306 Run {
13307 run_description: "Ends with matching suffix",
13308 initial_state: "SubˇError".into(),
13309 buffer_marked_text: "<Sub|Error>".into(),
13310 completion_label: "SubscriptionError",
13311 completion_text: "SubscriptionError",
13312 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13313 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13314 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13315 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13316 },
13317 Run {
13318 run_description: "Suffix is a subsequence -- contiguous",
13319 initial_state: "SubˇErr".into(),
13320 buffer_marked_text: "<Sub|Err>".into(),
13321 completion_label: "SubscriptionError",
13322 completion_text: "SubscriptionError",
13323 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13324 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13325 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13326 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13327 },
13328 Run {
13329 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13330 initial_state: "Suˇscrirr".into(),
13331 buffer_marked_text: "<Su|scrirr>".into(),
13332 completion_label: "SubscriptionError",
13333 completion_text: "SubscriptionError",
13334 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13335 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13336 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13337 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13338 },
13339 Run {
13340 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13341 initial_state: "foo(indˇix)".into(),
13342 buffer_marked_text: "foo(<ind|ix>)".into(),
13343 completion_label: "node_index",
13344 completion_text: "node_index",
13345 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13346 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13347 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13348 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13349 },
13350 Run {
13351 run_description: "Replace range ends before cursor - should extend to cursor",
13352 initial_state: "before editˇo after".into(),
13353 buffer_marked_text: "before <{ed}>it|o after".into(),
13354 completion_label: "editor",
13355 completion_text: "editor",
13356 expected_with_insert_mode: "before editorˇo after".into(),
13357 expected_with_replace_mode: "before editorˇo after".into(),
13358 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13359 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13360 },
13361 Run {
13362 run_description: "Uses label for suffix matching",
13363 initial_state: "before ediˇtor after".into(),
13364 buffer_marked_text: "before <edi|tor> after".into(),
13365 completion_label: "editor",
13366 completion_text: "editor()",
13367 expected_with_insert_mode: "before editor()ˇtor after".into(),
13368 expected_with_replace_mode: "before editor()ˇ after".into(),
13369 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13370 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13371 },
13372 Run {
13373 run_description: "Case insensitive subsequence and suffix matching",
13374 initial_state: "before EDiˇtoR after".into(),
13375 buffer_marked_text: "before <EDi|toR> after".into(),
13376 completion_label: "editor",
13377 completion_text: "editor",
13378 expected_with_insert_mode: "before editorˇtoR after".into(),
13379 expected_with_replace_mode: "before editorˇ after".into(),
13380 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13381 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13382 },
13383 ];
13384
13385 for run in runs {
13386 let run_variations = [
13387 (LspInsertMode::Insert, run.expected_with_insert_mode),
13388 (LspInsertMode::Replace, run.expected_with_replace_mode),
13389 (
13390 LspInsertMode::ReplaceSubsequence,
13391 run.expected_with_replace_subsequence_mode,
13392 ),
13393 (
13394 LspInsertMode::ReplaceSuffix,
13395 run.expected_with_replace_suffix_mode,
13396 ),
13397 ];
13398
13399 for (lsp_insert_mode, expected_text) in run_variations {
13400 eprintln!(
13401 "run = {:?}, mode = {lsp_insert_mode:.?}",
13402 run.run_description,
13403 );
13404
13405 update_test_language_settings(&mut cx, |settings| {
13406 settings.defaults.completions = Some(CompletionSettingsContent {
13407 lsp_insert_mode: Some(lsp_insert_mode),
13408 words: Some(WordsCompletionMode::Disabled),
13409 words_min_length: Some(0),
13410 ..Default::default()
13411 });
13412 });
13413
13414 cx.set_state(&run.initial_state);
13415 cx.update_editor(|editor, window, cx| {
13416 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13417 });
13418
13419 let counter = Arc::new(AtomicUsize::new(0));
13420 handle_completion_request_with_insert_and_replace(
13421 &mut cx,
13422 &run.buffer_marked_text,
13423 vec![(run.completion_label, run.completion_text)],
13424 counter.clone(),
13425 )
13426 .await;
13427 cx.condition(|editor, _| editor.context_menu_visible())
13428 .await;
13429 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13430
13431 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13432 editor
13433 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13434 .unwrap()
13435 });
13436 cx.assert_editor_state(&expected_text);
13437 handle_resolve_completion_request(&mut cx, None).await;
13438 apply_additional_edits.await.unwrap();
13439 }
13440 }
13441}
13442
13443#[gpui::test]
13444async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13445 init_test(cx, |_| {});
13446 let mut cx = EditorLspTestContext::new_rust(
13447 lsp::ServerCapabilities {
13448 completion_provider: Some(lsp::CompletionOptions {
13449 resolve_provider: Some(true),
13450 ..Default::default()
13451 }),
13452 ..Default::default()
13453 },
13454 cx,
13455 )
13456 .await;
13457
13458 let initial_state = "SubˇError";
13459 let buffer_marked_text = "<Sub|Error>";
13460 let completion_text = "SubscriptionError";
13461 let expected_with_insert_mode = "SubscriptionErrorˇError";
13462 let expected_with_replace_mode = "SubscriptionErrorˇ";
13463
13464 update_test_language_settings(&mut cx, |settings| {
13465 settings.defaults.completions = Some(CompletionSettingsContent {
13466 words: Some(WordsCompletionMode::Disabled),
13467 words_min_length: Some(0),
13468 // set the opposite here to ensure that the action is overriding the default behavior
13469 lsp_insert_mode: Some(LspInsertMode::Insert),
13470 ..Default::default()
13471 });
13472 });
13473
13474 cx.set_state(initial_state);
13475 cx.update_editor(|editor, window, cx| {
13476 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13477 });
13478
13479 let counter = Arc::new(AtomicUsize::new(0));
13480 handle_completion_request_with_insert_and_replace(
13481 &mut cx,
13482 buffer_marked_text,
13483 vec![(completion_text, completion_text)],
13484 counter.clone(),
13485 )
13486 .await;
13487 cx.condition(|editor, _| editor.context_menu_visible())
13488 .await;
13489 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13490
13491 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13492 editor
13493 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13494 .unwrap()
13495 });
13496 cx.assert_editor_state(expected_with_replace_mode);
13497 handle_resolve_completion_request(&mut cx, None).await;
13498 apply_additional_edits.await.unwrap();
13499
13500 update_test_language_settings(&mut cx, |settings| {
13501 settings.defaults.completions = Some(CompletionSettingsContent {
13502 words: Some(WordsCompletionMode::Disabled),
13503 words_min_length: Some(0),
13504 // set the opposite here to ensure that the action is overriding the default behavior
13505 lsp_insert_mode: Some(LspInsertMode::Replace),
13506 ..Default::default()
13507 });
13508 });
13509
13510 cx.set_state(initial_state);
13511 cx.update_editor(|editor, window, cx| {
13512 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13513 });
13514 handle_completion_request_with_insert_and_replace(
13515 &mut cx,
13516 buffer_marked_text,
13517 vec![(completion_text, completion_text)],
13518 counter.clone(),
13519 )
13520 .await;
13521 cx.condition(|editor, _| editor.context_menu_visible())
13522 .await;
13523 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13524
13525 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13526 editor
13527 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13528 .unwrap()
13529 });
13530 cx.assert_editor_state(expected_with_insert_mode);
13531 handle_resolve_completion_request(&mut cx, None).await;
13532 apply_additional_edits.await.unwrap();
13533}
13534
13535#[gpui::test]
13536async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13537 init_test(cx, |_| {});
13538 let mut cx = EditorLspTestContext::new_rust(
13539 lsp::ServerCapabilities {
13540 completion_provider: Some(lsp::CompletionOptions {
13541 resolve_provider: Some(true),
13542 ..Default::default()
13543 }),
13544 ..Default::default()
13545 },
13546 cx,
13547 )
13548 .await;
13549
13550 // scenario: surrounding text matches completion text
13551 let completion_text = "to_offset";
13552 let initial_state = indoc! {"
13553 1. buf.to_offˇsuffix
13554 2. buf.to_offˇsuf
13555 3. buf.to_offˇfix
13556 4. buf.to_offˇ
13557 5. into_offˇensive
13558 6. ˇsuffix
13559 7. let ˇ //
13560 8. aaˇzz
13561 9. buf.to_off«zzzzzˇ»suffix
13562 10. buf.«ˇzzzzz»suffix
13563 11. to_off«ˇzzzzz»
13564
13565 buf.to_offˇsuffix // newest cursor
13566 "};
13567 let completion_marked_buffer = indoc! {"
13568 1. buf.to_offsuffix
13569 2. buf.to_offsuf
13570 3. buf.to_offfix
13571 4. buf.to_off
13572 5. into_offensive
13573 6. suffix
13574 7. let //
13575 8. aazz
13576 9. buf.to_offzzzzzsuffix
13577 10. buf.zzzzzsuffix
13578 11. to_offzzzzz
13579
13580 buf.<to_off|suffix> // newest cursor
13581 "};
13582 let expected = indoc! {"
13583 1. buf.to_offsetˇ
13584 2. buf.to_offsetˇsuf
13585 3. buf.to_offsetˇfix
13586 4. buf.to_offsetˇ
13587 5. into_offsetˇensive
13588 6. to_offsetˇsuffix
13589 7. let to_offsetˇ //
13590 8. aato_offsetˇzz
13591 9. buf.to_offsetˇ
13592 10. buf.to_offsetˇsuffix
13593 11. to_offsetˇ
13594
13595 buf.to_offsetˇ // newest cursor
13596 "};
13597 cx.set_state(initial_state);
13598 cx.update_editor(|editor, window, cx| {
13599 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13600 });
13601 handle_completion_request_with_insert_and_replace(
13602 &mut cx,
13603 completion_marked_buffer,
13604 vec![(completion_text, completion_text)],
13605 Arc::new(AtomicUsize::new(0)),
13606 )
13607 .await;
13608 cx.condition(|editor, _| editor.context_menu_visible())
13609 .await;
13610 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13611 editor
13612 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13613 .unwrap()
13614 });
13615 cx.assert_editor_state(expected);
13616 handle_resolve_completion_request(&mut cx, None).await;
13617 apply_additional_edits.await.unwrap();
13618
13619 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
13620 let completion_text = "foo_and_bar";
13621 let initial_state = indoc! {"
13622 1. ooanbˇ
13623 2. zooanbˇ
13624 3. ooanbˇz
13625 4. zooanbˇz
13626 5. ooanˇ
13627 6. oanbˇ
13628
13629 ooanbˇ
13630 "};
13631 let completion_marked_buffer = indoc! {"
13632 1. ooanb
13633 2. zooanb
13634 3. ooanbz
13635 4. zooanbz
13636 5. ooan
13637 6. oanb
13638
13639 <ooanb|>
13640 "};
13641 let expected = indoc! {"
13642 1. foo_and_barˇ
13643 2. zfoo_and_barˇ
13644 3. foo_and_barˇz
13645 4. zfoo_and_barˇz
13646 5. ooanfoo_and_barˇ
13647 6. oanbfoo_and_barˇ
13648
13649 foo_and_barˇ
13650 "};
13651 cx.set_state(initial_state);
13652 cx.update_editor(|editor, window, cx| {
13653 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13654 });
13655 handle_completion_request_with_insert_and_replace(
13656 &mut cx,
13657 completion_marked_buffer,
13658 vec![(completion_text, completion_text)],
13659 Arc::new(AtomicUsize::new(0)),
13660 )
13661 .await;
13662 cx.condition(|editor, _| editor.context_menu_visible())
13663 .await;
13664 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13665 editor
13666 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13667 .unwrap()
13668 });
13669 cx.assert_editor_state(expected);
13670 handle_resolve_completion_request(&mut cx, None).await;
13671 apply_additional_edits.await.unwrap();
13672
13673 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
13674 // (expects the same as if it was inserted at the end)
13675 let completion_text = "foo_and_bar";
13676 let initial_state = indoc! {"
13677 1. ooˇanb
13678 2. zooˇanb
13679 3. ooˇanbz
13680 4. zooˇanbz
13681
13682 ooˇanb
13683 "};
13684 let completion_marked_buffer = indoc! {"
13685 1. ooanb
13686 2. zooanb
13687 3. ooanbz
13688 4. zooanbz
13689
13690 <oo|anb>
13691 "};
13692 let expected = indoc! {"
13693 1. foo_and_barˇ
13694 2. zfoo_and_barˇ
13695 3. foo_and_barˇz
13696 4. zfoo_and_barˇz
13697
13698 foo_and_barˇ
13699 "};
13700 cx.set_state(initial_state);
13701 cx.update_editor(|editor, window, cx| {
13702 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13703 });
13704 handle_completion_request_with_insert_and_replace(
13705 &mut cx,
13706 completion_marked_buffer,
13707 vec![(completion_text, completion_text)],
13708 Arc::new(AtomicUsize::new(0)),
13709 )
13710 .await;
13711 cx.condition(|editor, _| editor.context_menu_visible())
13712 .await;
13713 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13714 editor
13715 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13716 .unwrap()
13717 });
13718 cx.assert_editor_state(expected);
13719 handle_resolve_completion_request(&mut cx, None).await;
13720 apply_additional_edits.await.unwrap();
13721}
13722
13723// This used to crash
13724#[gpui::test]
13725async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
13726 init_test(cx, |_| {});
13727
13728 let buffer_text = indoc! {"
13729 fn main() {
13730 10.satu;
13731
13732 //
13733 // separate cursors so they open in different excerpts (manually reproducible)
13734 //
13735
13736 10.satu20;
13737 }
13738 "};
13739 let multibuffer_text_with_selections = indoc! {"
13740 fn main() {
13741 10.satuˇ;
13742
13743 //
13744
13745 //
13746
13747 10.satuˇ20;
13748 }
13749 "};
13750 let expected_multibuffer = indoc! {"
13751 fn main() {
13752 10.saturating_sub()ˇ;
13753
13754 //
13755
13756 //
13757
13758 10.saturating_sub()ˇ;
13759 }
13760 "};
13761
13762 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
13763 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
13764
13765 let fs = FakeFs::new(cx.executor());
13766 fs.insert_tree(
13767 path!("/a"),
13768 json!({
13769 "main.rs": buffer_text,
13770 }),
13771 )
13772 .await;
13773
13774 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13775 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13776 language_registry.add(rust_lang());
13777 let mut fake_servers = language_registry.register_fake_lsp(
13778 "Rust",
13779 FakeLspAdapter {
13780 capabilities: lsp::ServerCapabilities {
13781 completion_provider: Some(lsp::CompletionOptions {
13782 resolve_provider: None,
13783 ..lsp::CompletionOptions::default()
13784 }),
13785 ..lsp::ServerCapabilities::default()
13786 },
13787 ..FakeLspAdapter::default()
13788 },
13789 );
13790 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13791 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13792 let buffer = project
13793 .update(cx, |project, cx| {
13794 project.open_local_buffer(path!("/a/main.rs"), cx)
13795 })
13796 .await
13797 .unwrap();
13798
13799 let multi_buffer = cx.new(|cx| {
13800 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
13801 multi_buffer.push_excerpts(
13802 buffer.clone(),
13803 [ExcerptRange::new(0..first_excerpt_end)],
13804 cx,
13805 );
13806 multi_buffer.push_excerpts(
13807 buffer.clone(),
13808 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
13809 cx,
13810 );
13811 multi_buffer
13812 });
13813
13814 let editor = workspace
13815 .update(cx, |_, window, cx| {
13816 cx.new(|cx| {
13817 Editor::new(
13818 EditorMode::Full {
13819 scale_ui_elements_with_buffer_font_size: false,
13820 show_active_line_background: false,
13821 sized_by_content: false,
13822 },
13823 multi_buffer.clone(),
13824 Some(project.clone()),
13825 window,
13826 cx,
13827 )
13828 })
13829 })
13830 .unwrap();
13831
13832 let pane = workspace
13833 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13834 .unwrap();
13835 pane.update_in(cx, |pane, window, cx| {
13836 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
13837 });
13838
13839 let fake_server = fake_servers.next().await.unwrap();
13840
13841 editor.update_in(cx, |editor, window, cx| {
13842 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13843 s.select_ranges([
13844 Point::new(1, 11)..Point::new(1, 11),
13845 Point::new(7, 11)..Point::new(7, 11),
13846 ])
13847 });
13848
13849 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
13850 });
13851
13852 editor.update_in(cx, |editor, window, cx| {
13853 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
13854 });
13855
13856 fake_server
13857 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
13858 let completion_item = lsp::CompletionItem {
13859 label: "saturating_sub()".into(),
13860 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13861 lsp::InsertReplaceEdit {
13862 new_text: "saturating_sub()".to_owned(),
13863 insert: lsp::Range::new(
13864 lsp::Position::new(7, 7),
13865 lsp::Position::new(7, 11),
13866 ),
13867 replace: lsp::Range::new(
13868 lsp::Position::new(7, 7),
13869 lsp::Position::new(7, 13),
13870 ),
13871 },
13872 )),
13873 ..lsp::CompletionItem::default()
13874 };
13875
13876 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
13877 })
13878 .next()
13879 .await
13880 .unwrap();
13881
13882 cx.condition(&editor, |editor, _| editor.context_menu_visible())
13883 .await;
13884
13885 editor
13886 .update_in(cx, |editor, window, cx| {
13887 editor
13888 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13889 .unwrap()
13890 })
13891 .await
13892 .unwrap();
13893
13894 editor.update(cx, |editor, cx| {
13895 assert_text_with_selections(editor, expected_multibuffer, cx);
13896 })
13897}
13898
13899#[gpui::test]
13900async fn test_completion(cx: &mut TestAppContext) {
13901 init_test(cx, |_| {});
13902
13903 let mut cx = EditorLspTestContext::new_rust(
13904 lsp::ServerCapabilities {
13905 completion_provider: Some(lsp::CompletionOptions {
13906 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
13907 resolve_provider: Some(true),
13908 ..Default::default()
13909 }),
13910 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
13911 ..Default::default()
13912 },
13913 cx,
13914 )
13915 .await;
13916 let counter = Arc::new(AtomicUsize::new(0));
13917
13918 cx.set_state(indoc! {"
13919 oneˇ
13920 two
13921 three
13922 "});
13923 cx.simulate_keystroke(".");
13924 handle_completion_request(
13925 indoc! {"
13926 one.|<>
13927 two
13928 three
13929 "},
13930 vec!["first_completion", "second_completion"],
13931 true,
13932 counter.clone(),
13933 &mut cx,
13934 )
13935 .await;
13936 cx.condition(|editor, _| editor.context_menu_visible())
13937 .await;
13938 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13939
13940 let _handler = handle_signature_help_request(
13941 &mut cx,
13942 lsp::SignatureHelp {
13943 signatures: vec![lsp::SignatureInformation {
13944 label: "test signature".to_string(),
13945 documentation: None,
13946 parameters: Some(vec![lsp::ParameterInformation {
13947 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
13948 documentation: None,
13949 }]),
13950 active_parameter: None,
13951 }],
13952 active_signature: None,
13953 active_parameter: None,
13954 },
13955 );
13956 cx.update_editor(|editor, window, cx| {
13957 assert!(
13958 !editor.signature_help_state.is_shown(),
13959 "No signature help was called for"
13960 );
13961 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13962 });
13963 cx.run_until_parked();
13964 cx.update_editor(|editor, _, _| {
13965 assert!(
13966 !editor.signature_help_state.is_shown(),
13967 "No signature help should be shown when completions menu is open"
13968 );
13969 });
13970
13971 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13972 editor.context_menu_next(&Default::default(), window, cx);
13973 editor
13974 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13975 .unwrap()
13976 });
13977 cx.assert_editor_state(indoc! {"
13978 one.second_completionˇ
13979 two
13980 three
13981 "});
13982
13983 handle_resolve_completion_request(
13984 &mut cx,
13985 Some(vec![
13986 (
13987 //This overlaps with the primary completion edit which is
13988 //misbehavior from the LSP spec, test that we filter it out
13989 indoc! {"
13990 one.second_ˇcompletion
13991 two
13992 threeˇ
13993 "},
13994 "overlapping additional edit",
13995 ),
13996 (
13997 indoc! {"
13998 one.second_completion
13999 two
14000 threeˇ
14001 "},
14002 "\nadditional edit",
14003 ),
14004 ]),
14005 )
14006 .await;
14007 apply_additional_edits.await.unwrap();
14008 cx.assert_editor_state(indoc! {"
14009 one.second_completionˇ
14010 two
14011 three
14012 additional edit
14013 "});
14014
14015 cx.set_state(indoc! {"
14016 one.second_completion
14017 twoˇ
14018 threeˇ
14019 additional edit
14020 "});
14021 cx.simulate_keystroke(" ");
14022 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14023 cx.simulate_keystroke("s");
14024 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14025
14026 cx.assert_editor_state(indoc! {"
14027 one.second_completion
14028 two sˇ
14029 three sˇ
14030 additional edit
14031 "});
14032 handle_completion_request(
14033 indoc! {"
14034 one.second_completion
14035 two s
14036 three <s|>
14037 additional edit
14038 "},
14039 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14040 true,
14041 counter.clone(),
14042 &mut cx,
14043 )
14044 .await;
14045 cx.condition(|editor, _| editor.context_menu_visible())
14046 .await;
14047 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14048
14049 cx.simulate_keystroke("i");
14050
14051 handle_completion_request(
14052 indoc! {"
14053 one.second_completion
14054 two si
14055 three <si|>
14056 additional edit
14057 "},
14058 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14059 true,
14060 counter.clone(),
14061 &mut cx,
14062 )
14063 .await;
14064 cx.condition(|editor, _| editor.context_menu_visible())
14065 .await;
14066 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14067
14068 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14069 editor
14070 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14071 .unwrap()
14072 });
14073 cx.assert_editor_state(indoc! {"
14074 one.second_completion
14075 two sixth_completionˇ
14076 three sixth_completionˇ
14077 additional edit
14078 "});
14079
14080 apply_additional_edits.await.unwrap();
14081
14082 update_test_language_settings(&mut cx, |settings| {
14083 settings.defaults.show_completions_on_input = Some(false);
14084 });
14085 cx.set_state("editorˇ");
14086 cx.simulate_keystroke(".");
14087 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14088 cx.simulate_keystrokes("c l o");
14089 cx.assert_editor_state("editor.cloˇ");
14090 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14091 cx.update_editor(|editor, window, cx| {
14092 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14093 });
14094 handle_completion_request(
14095 "editor.<clo|>",
14096 vec!["close", "clobber"],
14097 true,
14098 counter.clone(),
14099 &mut cx,
14100 )
14101 .await;
14102 cx.condition(|editor, _| editor.context_menu_visible())
14103 .await;
14104 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14105
14106 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14107 editor
14108 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14109 .unwrap()
14110 });
14111 cx.assert_editor_state("editor.clobberˇ");
14112 handle_resolve_completion_request(&mut cx, None).await;
14113 apply_additional_edits.await.unwrap();
14114}
14115
14116#[gpui::test]
14117async fn test_completion_reuse(cx: &mut TestAppContext) {
14118 init_test(cx, |_| {});
14119
14120 let mut cx = EditorLspTestContext::new_rust(
14121 lsp::ServerCapabilities {
14122 completion_provider: Some(lsp::CompletionOptions {
14123 trigger_characters: Some(vec![".".to_string()]),
14124 ..Default::default()
14125 }),
14126 ..Default::default()
14127 },
14128 cx,
14129 )
14130 .await;
14131
14132 let counter = Arc::new(AtomicUsize::new(0));
14133 cx.set_state("objˇ");
14134 cx.simulate_keystroke(".");
14135
14136 // Initial completion request returns complete results
14137 let is_incomplete = false;
14138 handle_completion_request(
14139 "obj.|<>",
14140 vec!["a", "ab", "abc"],
14141 is_incomplete,
14142 counter.clone(),
14143 &mut cx,
14144 )
14145 .await;
14146 cx.run_until_parked();
14147 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14148 cx.assert_editor_state("obj.ˇ");
14149 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14150
14151 // Type "a" - filters existing completions
14152 cx.simulate_keystroke("a");
14153 cx.run_until_parked();
14154 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14155 cx.assert_editor_state("obj.aˇ");
14156 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14157
14158 // Type "b" - filters existing completions
14159 cx.simulate_keystroke("b");
14160 cx.run_until_parked();
14161 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14162 cx.assert_editor_state("obj.abˇ");
14163 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14164
14165 // Type "c" - filters existing completions
14166 cx.simulate_keystroke("c");
14167 cx.run_until_parked();
14168 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14169 cx.assert_editor_state("obj.abcˇ");
14170 check_displayed_completions(vec!["abc"], &mut cx);
14171
14172 // Backspace to delete "c" - filters existing completions
14173 cx.update_editor(|editor, window, cx| {
14174 editor.backspace(&Backspace, window, cx);
14175 });
14176 cx.run_until_parked();
14177 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14178 cx.assert_editor_state("obj.abˇ");
14179 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14180
14181 // Moving cursor to the left dismisses menu.
14182 cx.update_editor(|editor, window, cx| {
14183 editor.move_left(&MoveLeft, window, cx);
14184 });
14185 cx.run_until_parked();
14186 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14187 cx.assert_editor_state("obj.aˇb");
14188 cx.update_editor(|editor, _, _| {
14189 assert_eq!(editor.context_menu_visible(), false);
14190 });
14191
14192 // Type "b" - new request
14193 cx.simulate_keystroke("b");
14194 let is_incomplete = false;
14195 handle_completion_request(
14196 "obj.<ab|>a",
14197 vec!["ab", "abc"],
14198 is_incomplete,
14199 counter.clone(),
14200 &mut cx,
14201 )
14202 .await;
14203 cx.run_until_parked();
14204 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14205 cx.assert_editor_state("obj.abˇb");
14206 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14207
14208 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14209 cx.update_editor(|editor, window, cx| {
14210 editor.backspace(&Backspace, window, cx);
14211 });
14212 let is_incomplete = false;
14213 handle_completion_request(
14214 "obj.<a|>b",
14215 vec!["a", "ab", "abc"],
14216 is_incomplete,
14217 counter.clone(),
14218 &mut cx,
14219 )
14220 .await;
14221 cx.run_until_parked();
14222 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14223 cx.assert_editor_state("obj.aˇb");
14224 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14225
14226 // Backspace to delete "a" - dismisses menu.
14227 cx.update_editor(|editor, window, cx| {
14228 editor.backspace(&Backspace, window, cx);
14229 });
14230 cx.run_until_parked();
14231 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14232 cx.assert_editor_state("obj.ˇb");
14233 cx.update_editor(|editor, _, _| {
14234 assert_eq!(editor.context_menu_visible(), false);
14235 });
14236}
14237
14238#[gpui::test]
14239async fn test_word_completion(cx: &mut TestAppContext) {
14240 let lsp_fetch_timeout_ms = 10;
14241 init_test(cx, |language_settings| {
14242 language_settings.defaults.completions = Some(CompletionSettingsContent {
14243 words_min_length: Some(0),
14244 lsp_fetch_timeout_ms: Some(10),
14245 lsp_insert_mode: Some(LspInsertMode::Insert),
14246 ..Default::default()
14247 });
14248 });
14249
14250 let mut cx = EditorLspTestContext::new_rust(
14251 lsp::ServerCapabilities {
14252 completion_provider: Some(lsp::CompletionOptions {
14253 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14254 ..lsp::CompletionOptions::default()
14255 }),
14256 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14257 ..lsp::ServerCapabilities::default()
14258 },
14259 cx,
14260 )
14261 .await;
14262
14263 let throttle_completions = Arc::new(AtomicBool::new(false));
14264
14265 let lsp_throttle_completions = throttle_completions.clone();
14266 let _completion_requests_handler =
14267 cx.lsp
14268 .server
14269 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14270 let lsp_throttle_completions = lsp_throttle_completions.clone();
14271 let cx = cx.clone();
14272 async move {
14273 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14274 cx.background_executor()
14275 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14276 .await;
14277 }
14278 Ok(Some(lsp::CompletionResponse::Array(vec![
14279 lsp::CompletionItem {
14280 label: "first".into(),
14281 ..lsp::CompletionItem::default()
14282 },
14283 lsp::CompletionItem {
14284 label: "last".into(),
14285 ..lsp::CompletionItem::default()
14286 },
14287 ])))
14288 }
14289 });
14290
14291 cx.set_state(indoc! {"
14292 oneˇ
14293 two
14294 three
14295 "});
14296 cx.simulate_keystroke(".");
14297 cx.executor().run_until_parked();
14298 cx.condition(|editor, _| editor.context_menu_visible())
14299 .await;
14300 cx.update_editor(|editor, window, cx| {
14301 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14302 {
14303 assert_eq!(
14304 completion_menu_entries(menu),
14305 &["first", "last"],
14306 "When LSP server is fast to reply, no fallback word completions are used"
14307 );
14308 } else {
14309 panic!("expected completion menu to be open");
14310 }
14311 editor.cancel(&Cancel, window, cx);
14312 });
14313 cx.executor().run_until_parked();
14314 cx.condition(|editor, _| !editor.context_menu_visible())
14315 .await;
14316
14317 throttle_completions.store(true, atomic::Ordering::Release);
14318 cx.simulate_keystroke(".");
14319 cx.executor()
14320 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14321 cx.executor().run_until_parked();
14322 cx.condition(|editor, _| editor.context_menu_visible())
14323 .await;
14324 cx.update_editor(|editor, _, _| {
14325 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14326 {
14327 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14328 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14329 } else {
14330 panic!("expected completion menu to be open");
14331 }
14332 });
14333}
14334
14335#[gpui::test]
14336async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14337 init_test(cx, |language_settings| {
14338 language_settings.defaults.completions = Some(CompletionSettingsContent {
14339 words: Some(WordsCompletionMode::Enabled),
14340 words_min_length: Some(0),
14341 lsp_insert_mode: Some(LspInsertMode::Insert),
14342 ..Default::default()
14343 });
14344 });
14345
14346 let mut cx = EditorLspTestContext::new_rust(
14347 lsp::ServerCapabilities {
14348 completion_provider: Some(lsp::CompletionOptions {
14349 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14350 ..lsp::CompletionOptions::default()
14351 }),
14352 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14353 ..lsp::ServerCapabilities::default()
14354 },
14355 cx,
14356 )
14357 .await;
14358
14359 let _completion_requests_handler =
14360 cx.lsp
14361 .server
14362 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14363 Ok(Some(lsp::CompletionResponse::Array(vec![
14364 lsp::CompletionItem {
14365 label: "first".into(),
14366 ..lsp::CompletionItem::default()
14367 },
14368 lsp::CompletionItem {
14369 label: "last".into(),
14370 ..lsp::CompletionItem::default()
14371 },
14372 ])))
14373 });
14374
14375 cx.set_state(indoc! {"ˇ
14376 first
14377 last
14378 second
14379 "});
14380 cx.simulate_keystroke(".");
14381 cx.executor().run_until_parked();
14382 cx.condition(|editor, _| editor.context_menu_visible())
14383 .await;
14384 cx.update_editor(|editor, _, _| {
14385 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14386 {
14387 assert_eq!(
14388 completion_menu_entries(menu),
14389 &["first", "last", "second"],
14390 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14391 );
14392 } else {
14393 panic!("expected completion menu to be open");
14394 }
14395 });
14396}
14397
14398#[gpui::test]
14399async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14400 init_test(cx, |language_settings| {
14401 language_settings.defaults.completions = Some(CompletionSettingsContent {
14402 words: Some(WordsCompletionMode::Disabled),
14403 words_min_length: Some(0),
14404 lsp_insert_mode: Some(LspInsertMode::Insert),
14405 ..Default::default()
14406 });
14407 });
14408
14409 let mut cx = EditorLspTestContext::new_rust(
14410 lsp::ServerCapabilities {
14411 completion_provider: Some(lsp::CompletionOptions {
14412 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14413 ..lsp::CompletionOptions::default()
14414 }),
14415 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14416 ..lsp::ServerCapabilities::default()
14417 },
14418 cx,
14419 )
14420 .await;
14421
14422 let _completion_requests_handler =
14423 cx.lsp
14424 .server
14425 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14426 panic!("LSP completions should not be queried when dealing with word completions")
14427 });
14428
14429 cx.set_state(indoc! {"ˇ
14430 first
14431 last
14432 second
14433 "});
14434 cx.update_editor(|editor, window, cx| {
14435 editor.show_word_completions(&ShowWordCompletions, window, cx);
14436 });
14437 cx.executor().run_until_parked();
14438 cx.condition(|editor, _| editor.context_menu_visible())
14439 .await;
14440 cx.update_editor(|editor, _, _| {
14441 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14442 {
14443 assert_eq!(
14444 completion_menu_entries(menu),
14445 &["first", "last", "second"],
14446 "`ShowWordCompletions` action should show word completions"
14447 );
14448 } else {
14449 panic!("expected completion menu to be open");
14450 }
14451 });
14452
14453 cx.simulate_keystroke("l");
14454 cx.executor().run_until_parked();
14455 cx.condition(|editor, _| editor.context_menu_visible())
14456 .await;
14457 cx.update_editor(|editor, _, _| {
14458 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14459 {
14460 assert_eq!(
14461 completion_menu_entries(menu),
14462 &["last"],
14463 "After showing word completions, further editing should filter them and not query the LSP"
14464 );
14465 } else {
14466 panic!("expected completion menu to be open");
14467 }
14468 });
14469}
14470
14471#[gpui::test]
14472async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14473 init_test(cx, |language_settings| {
14474 language_settings.defaults.completions = Some(CompletionSettingsContent {
14475 words_min_length: Some(0),
14476 lsp: Some(false),
14477 lsp_insert_mode: Some(LspInsertMode::Insert),
14478 ..Default::default()
14479 });
14480 });
14481
14482 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14483
14484 cx.set_state(indoc! {"ˇ
14485 0_usize
14486 let
14487 33
14488 4.5f32
14489 "});
14490 cx.update_editor(|editor, window, cx| {
14491 editor.show_completions(&ShowCompletions::default(), window, cx);
14492 });
14493 cx.executor().run_until_parked();
14494 cx.condition(|editor, _| editor.context_menu_visible())
14495 .await;
14496 cx.update_editor(|editor, window, cx| {
14497 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14498 {
14499 assert_eq!(
14500 completion_menu_entries(menu),
14501 &["let"],
14502 "With no digits in the completion query, no digits should be in the word completions"
14503 );
14504 } else {
14505 panic!("expected completion menu to be open");
14506 }
14507 editor.cancel(&Cancel, window, cx);
14508 });
14509
14510 cx.set_state(indoc! {"3ˇ
14511 0_usize
14512 let
14513 3
14514 33.35f32
14515 "});
14516 cx.update_editor(|editor, window, cx| {
14517 editor.show_completions(&ShowCompletions::default(), window, cx);
14518 });
14519 cx.executor().run_until_parked();
14520 cx.condition(|editor, _| editor.context_menu_visible())
14521 .await;
14522 cx.update_editor(|editor, _, _| {
14523 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14524 {
14525 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14526 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14527 } else {
14528 panic!("expected completion menu to be open");
14529 }
14530 });
14531}
14532
14533#[gpui::test]
14534async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14535 init_test(cx, |language_settings| {
14536 language_settings.defaults.completions = Some(CompletionSettingsContent {
14537 words: Some(WordsCompletionMode::Enabled),
14538 words_min_length: Some(3),
14539 lsp_insert_mode: Some(LspInsertMode::Insert),
14540 ..Default::default()
14541 });
14542 });
14543
14544 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14545 cx.set_state(indoc! {"ˇ
14546 wow
14547 wowen
14548 wowser
14549 "});
14550 cx.simulate_keystroke("w");
14551 cx.executor().run_until_parked();
14552 cx.update_editor(|editor, _, _| {
14553 if editor.context_menu.borrow_mut().is_some() {
14554 panic!(
14555 "expected completion menu to be hidden, as words completion threshold is not met"
14556 );
14557 }
14558 });
14559
14560 cx.update_editor(|editor, window, cx| {
14561 editor.show_word_completions(&ShowWordCompletions, window, cx);
14562 });
14563 cx.executor().run_until_parked();
14564 cx.update_editor(|editor, window, cx| {
14565 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14566 {
14567 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");
14568 } else {
14569 panic!("expected completion menu to be open after the word completions are called with an action");
14570 }
14571
14572 editor.cancel(&Cancel, window, cx);
14573 });
14574 cx.update_editor(|editor, _, _| {
14575 if editor.context_menu.borrow_mut().is_some() {
14576 panic!("expected completion menu to be hidden after canceling");
14577 }
14578 });
14579
14580 cx.simulate_keystroke("o");
14581 cx.executor().run_until_parked();
14582 cx.update_editor(|editor, _, _| {
14583 if editor.context_menu.borrow_mut().is_some() {
14584 panic!(
14585 "expected completion menu to be hidden, as words completion threshold is not met still"
14586 );
14587 }
14588 });
14589
14590 cx.simulate_keystroke("w");
14591 cx.executor().run_until_parked();
14592 cx.update_editor(|editor, _, _| {
14593 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14594 {
14595 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
14596 } else {
14597 panic!("expected completion menu to be open after the word completions threshold is met");
14598 }
14599 });
14600}
14601
14602#[gpui::test]
14603async fn test_word_completions_disabled(cx: &mut TestAppContext) {
14604 init_test(cx, |language_settings| {
14605 language_settings.defaults.completions = Some(CompletionSettingsContent {
14606 words: Some(WordsCompletionMode::Enabled),
14607 words_min_length: Some(0),
14608 lsp_insert_mode: Some(LspInsertMode::Insert),
14609 ..Default::default()
14610 });
14611 });
14612
14613 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14614 cx.update_editor(|editor, _, _| {
14615 editor.disable_word_completions();
14616 });
14617 cx.set_state(indoc! {"ˇ
14618 wow
14619 wowen
14620 wowser
14621 "});
14622 cx.simulate_keystroke("w");
14623 cx.executor().run_until_parked();
14624 cx.update_editor(|editor, _, _| {
14625 if editor.context_menu.borrow_mut().is_some() {
14626 panic!(
14627 "expected completion menu to be hidden, as words completion are disabled for this editor"
14628 );
14629 }
14630 });
14631
14632 cx.update_editor(|editor, window, cx| {
14633 editor.show_word_completions(&ShowWordCompletions, window, cx);
14634 });
14635 cx.executor().run_until_parked();
14636 cx.update_editor(|editor, _, _| {
14637 if editor.context_menu.borrow_mut().is_some() {
14638 panic!(
14639 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
14640 );
14641 }
14642 });
14643}
14644
14645fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
14646 let position = || lsp::Position {
14647 line: params.text_document_position.position.line,
14648 character: params.text_document_position.position.character,
14649 };
14650 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14651 range: lsp::Range {
14652 start: position(),
14653 end: position(),
14654 },
14655 new_text: text.to_string(),
14656 }))
14657}
14658
14659#[gpui::test]
14660async fn test_multiline_completion(cx: &mut TestAppContext) {
14661 init_test(cx, |_| {});
14662
14663 let fs = FakeFs::new(cx.executor());
14664 fs.insert_tree(
14665 path!("/a"),
14666 json!({
14667 "main.ts": "a",
14668 }),
14669 )
14670 .await;
14671
14672 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14673 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14674 let typescript_language = Arc::new(Language::new(
14675 LanguageConfig {
14676 name: "TypeScript".into(),
14677 matcher: LanguageMatcher {
14678 path_suffixes: vec!["ts".to_string()],
14679 ..LanguageMatcher::default()
14680 },
14681 line_comments: vec!["// ".into()],
14682 ..LanguageConfig::default()
14683 },
14684 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14685 ));
14686 language_registry.add(typescript_language.clone());
14687 let mut fake_servers = language_registry.register_fake_lsp(
14688 "TypeScript",
14689 FakeLspAdapter {
14690 capabilities: lsp::ServerCapabilities {
14691 completion_provider: Some(lsp::CompletionOptions {
14692 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14693 ..lsp::CompletionOptions::default()
14694 }),
14695 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14696 ..lsp::ServerCapabilities::default()
14697 },
14698 // Emulate vtsls label generation
14699 label_for_completion: Some(Box::new(|item, _| {
14700 let text = if let Some(description) = item
14701 .label_details
14702 .as_ref()
14703 .and_then(|label_details| label_details.description.as_ref())
14704 {
14705 format!("{} {}", item.label, description)
14706 } else if let Some(detail) = &item.detail {
14707 format!("{} {}", item.label, detail)
14708 } else {
14709 item.label.clone()
14710 };
14711 let len = text.len();
14712 Some(language::CodeLabel {
14713 text,
14714 runs: Vec::new(),
14715 filter_range: 0..len,
14716 })
14717 })),
14718 ..FakeLspAdapter::default()
14719 },
14720 );
14721 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14722 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14723 let worktree_id = workspace
14724 .update(cx, |workspace, _window, cx| {
14725 workspace.project().update(cx, |project, cx| {
14726 project.worktrees(cx).next().unwrap().read(cx).id()
14727 })
14728 })
14729 .unwrap();
14730 let _buffer = project
14731 .update(cx, |project, cx| {
14732 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
14733 })
14734 .await
14735 .unwrap();
14736 let editor = workspace
14737 .update(cx, |workspace, window, cx| {
14738 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
14739 })
14740 .unwrap()
14741 .await
14742 .unwrap()
14743 .downcast::<Editor>()
14744 .unwrap();
14745 let fake_server = fake_servers.next().await.unwrap();
14746
14747 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
14748 let multiline_label_2 = "a\nb\nc\n";
14749 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
14750 let multiline_description = "d\ne\nf\n";
14751 let multiline_detail_2 = "g\nh\ni\n";
14752
14753 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
14754 move |params, _| async move {
14755 Ok(Some(lsp::CompletionResponse::Array(vec![
14756 lsp::CompletionItem {
14757 label: multiline_label.to_string(),
14758 text_edit: gen_text_edit(¶ms, "new_text_1"),
14759 ..lsp::CompletionItem::default()
14760 },
14761 lsp::CompletionItem {
14762 label: "single line label 1".to_string(),
14763 detail: Some(multiline_detail.to_string()),
14764 text_edit: gen_text_edit(¶ms, "new_text_2"),
14765 ..lsp::CompletionItem::default()
14766 },
14767 lsp::CompletionItem {
14768 label: "single line label 2".to_string(),
14769 label_details: Some(lsp::CompletionItemLabelDetails {
14770 description: Some(multiline_description.to_string()),
14771 detail: None,
14772 }),
14773 text_edit: gen_text_edit(¶ms, "new_text_2"),
14774 ..lsp::CompletionItem::default()
14775 },
14776 lsp::CompletionItem {
14777 label: multiline_label_2.to_string(),
14778 detail: Some(multiline_detail_2.to_string()),
14779 text_edit: gen_text_edit(¶ms, "new_text_3"),
14780 ..lsp::CompletionItem::default()
14781 },
14782 lsp::CompletionItem {
14783 label: "Label with many spaces and \t but without newlines".to_string(),
14784 detail: Some(
14785 "Details with many spaces and \t but without newlines".to_string(),
14786 ),
14787 text_edit: gen_text_edit(¶ms, "new_text_4"),
14788 ..lsp::CompletionItem::default()
14789 },
14790 ])))
14791 },
14792 );
14793
14794 editor.update_in(cx, |editor, window, cx| {
14795 cx.focus_self(window);
14796 editor.move_to_end(&MoveToEnd, window, cx);
14797 editor.handle_input(".", window, cx);
14798 });
14799 cx.run_until_parked();
14800 completion_handle.next().await.unwrap();
14801
14802 editor.update(cx, |editor, _| {
14803 assert!(editor.context_menu_visible());
14804 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14805 {
14806 let completion_labels = menu
14807 .completions
14808 .borrow()
14809 .iter()
14810 .map(|c| c.label.text.clone())
14811 .collect::<Vec<_>>();
14812 assert_eq!(
14813 completion_labels,
14814 &[
14815 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
14816 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
14817 "single line label 2 d e f ",
14818 "a b c g h i ",
14819 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
14820 ],
14821 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
14822 );
14823
14824 for completion in menu
14825 .completions
14826 .borrow()
14827 .iter() {
14828 assert_eq!(
14829 completion.label.filter_range,
14830 0..completion.label.text.len(),
14831 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
14832 );
14833 }
14834 } else {
14835 panic!("expected completion menu to be open");
14836 }
14837 });
14838}
14839
14840#[gpui::test]
14841async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
14842 init_test(cx, |_| {});
14843 let mut cx = EditorLspTestContext::new_rust(
14844 lsp::ServerCapabilities {
14845 completion_provider: Some(lsp::CompletionOptions {
14846 trigger_characters: Some(vec![".".to_string()]),
14847 ..Default::default()
14848 }),
14849 ..Default::default()
14850 },
14851 cx,
14852 )
14853 .await;
14854 cx.lsp
14855 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14856 Ok(Some(lsp::CompletionResponse::Array(vec![
14857 lsp::CompletionItem {
14858 label: "first".into(),
14859 ..Default::default()
14860 },
14861 lsp::CompletionItem {
14862 label: "last".into(),
14863 ..Default::default()
14864 },
14865 ])))
14866 });
14867 cx.set_state("variableˇ");
14868 cx.simulate_keystroke(".");
14869 cx.executor().run_until_parked();
14870
14871 cx.update_editor(|editor, _, _| {
14872 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14873 {
14874 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
14875 } else {
14876 panic!("expected completion menu to be open");
14877 }
14878 });
14879
14880 cx.update_editor(|editor, window, cx| {
14881 editor.move_page_down(&MovePageDown::default(), window, cx);
14882 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14883 {
14884 assert!(
14885 menu.selected_item == 1,
14886 "expected PageDown to select the last item from the context menu"
14887 );
14888 } else {
14889 panic!("expected completion menu to stay open after PageDown");
14890 }
14891 });
14892
14893 cx.update_editor(|editor, window, cx| {
14894 editor.move_page_up(&MovePageUp::default(), window, cx);
14895 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14896 {
14897 assert!(
14898 menu.selected_item == 0,
14899 "expected PageUp to select the first item from the context menu"
14900 );
14901 } else {
14902 panic!("expected completion menu to stay open after PageUp");
14903 }
14904 });
14905}
14906
14907#[gpui::test]
14908async fn test_as_is_completions(cx: &mut TestAppContext) {
14909 init_test(cx, |_| {});
14910 let mut cx = EditorLspTestContext::new_rust(
14911 lsp::ServerCapabilities {
14912 completion_provider: Some(lsp::CompletionOptions {
14913 ..Default::default()
14914 }),
14915 ..Default::default()
14916 },
14917 cx,
14918 )
14919 .await;
14920 cx.lsp
14921 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14922 Ok(Some(lsp::CompletionResponse::Array(vec![
14923 lsp::CompletionItem {
14924 label: "unsafe".into(),
14925 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14926 range: lsp::Range {
14927 start: lsp::Position {
14928 line: 1,
14929 character: 2,
14930 },
14931 end: lsp::Position {
14932 line: 1,
14933 character: 3,
14934 },
14935 },
14936 new_text: "unsafe".to_string(),
14937 })),
14938 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
14939 ..Default::default()
14940 },
14941 ])))
14942 });
14943 cx.set_state("fn a() {}\n nˇ");
14944 cx.executor().run_until_parked();
14945 cx.update_editor(|editor, window, cx| {
14946 editor.show_completions(
14947 &ShowCompletions {
14948 trigger: Some("\n".into()),
14949 },
14950 window,
14951 cx,
14952 );
14953 });
14954 cx.executor().run_until_parked();
14955
14956 cx.update_editor(|editor, window, cx| {
14957 editor.confirm_completion(&Default::default(), window, cx)
14958 });
14959 cx.executor().run_until_parked();
14960 cx.assert_editor_state("fn a() {}\n unsafeˇ");
14961}
14962
14963#[gpui::test]
14964async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
14965 init_test(cx, |_| {});
14966 let language =
14967 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
14968 let mut cx = EditorLspTestContext::new(
14969 language,
14970 lsp::ServerCapabilities {
14971 completion_provider: Some(lsp::CompletionOptions {
14972 ..lsp::CompletionOptions::default()
14973 }),
14974 ..lsp::ServerCapabilities::default()
14975 },
14976 cx,
14977 )
14978 .await;
14979
14980 cx.set_state(
14981 "#ifndef BAR_H
14982#define BAR_H
14983
14984#include <stdbool.h>
14985
14986int fn_branch(bool do_branch1, bool do_branch2);
14987
14988#endif // BAR_H
14989ˇ",
14990 );
14991 cx.executor().run_until_parked();
14992 cx.update_editor(|editor, window, cx| {
14993 editor.handle_input("#", window, cx);
14994 });
14995 cx.executor().run_until_parked();
14996 cx.update_editor(|editor, window, cx| {
14997 editor.handle_input("i", window, cx);
14998 });
14999 cx.executor().run_until_parked();
15000 cx.update_editor(|editor, window, cx| {
15001 editor.handle_input("n", window, cx);
15002 });
15003 cx.executor().run_until_parked();
15004 cx.assert_editor_state(
15005 "#ifndef BAR_H
15006#define BAR_H
15007
15008#include <stdbool.h>
15009
15010int fn_branch(bool do_branch1, bool do_branch2);
15011
15012#endif // BAR_H
15013#inˇ",
15014 );
15015
15016 cx.lsp
15017 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15018 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15019 is_incomplete: false,
15020 item_defaults: None,
15021 items: vec![lsp::CompletionItem {
15022 kind: Some(lsp::CompletionItemKind::SNIPPET),
15023 label_details: Some(lsp::CompletionItemLabelDetails {
15024 detail: Some("header".to_string()),
15025 description: None,
15026 }),
15027 label: " include".to_string(),
15028 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15029 range: lsp::Range {
15030 start: lsp::Position {
15031 line: 8,
15032 character: 1,
15033 },
15034 end: lsp::Position {
15035 line: 8,
15036 character: 1,
15037 },
15038 },
15039 new_text: "include \"$0\"".to_string(),
15040 })),
15041 sort_text: Some("40b67681include".to_string()),
15042 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15043 filter_text: Some("include".to_string()),
15044 insert_text: Some("include \"$0\"".to_string()),
15045 ..lsp::CompletionItem::default()
15046 }],
15047 })))
15048 });
15049 cx.update_editor(|editor, window, cx| {
15050 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15051 });
15052 cx.executor().run_until_parked();
15053 cx.update_editor(|editor, window, cx| {
15054 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15055 });
15056 cx.executor().run_until_parked();
15057 cx.assert_editor_state(
15058 "#ifndef BAR_H
15059#define BAR_H
15060
15061#include <stdbool.h>
15062
15063int fn_branch(bool do_branch1, bool do_branch2);
15064
15065#endif // BAR_H
15066#include \"ˇ\"",
15067 );
15068
15069 cx.lsp
15070 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15071 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15072 is_incomplete: true,
15073 item_defaults: None,
15074 items: vec![lsp::CompletionItem {
15075 kind: Some(lsp::CompletionItemKind::FILE),
15076 label: "AGL/".to_string(),
15077 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15078 range: lsp::Range {
15079 start: lsp::Position {
15080 line: 8,
15081 character: 10,
15082 },
15083 end: lsp::Position {
15084 line: 8,
15085 character: 11,
15086 },
15087 },
15088 new_text: "AGL/".to_string(),
15089 })),
15090 sort_text: Some("40b67681AGL/".to_string()),
15091 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15092 filter_text: Some("AGL/".to_string()),
15093 insert_text: Some("AGL/".to_string()),
15094 ..lsp::CompletionItem::default()
15095 }],
15096 })))
15097 });
15098 cx.update_editor(|editor, window, cx| {
15099 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
15100 });
15101 cx.executor().run_until_parked();
15102 cx.update_editor(|editor, window, cx| {
15103 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15104 });
15105 cx.executor().run_until_parked();
15106 cx.assert_editor_state(
15107 r##"#ifndef BAR_H
15108#define BAR_H
15109
15110#include <stdbool.h>
15111
15112int fn_branch(bool do_branch1, bool do_branch2);
15113
15114#endif // BAR_H
15115#include "AGL/ˇ"##,
15116 );
15117
15118 cx.update_editor(|editor, window, cx| {
15119 editor.handle_input("\"", window, cx);
15120 });
15121 cx.executor().run_until_parked();
15122 cx.assert_editor_state(
15123 r##"#ifndef BAR_H
15124#define BAR_H
15125
15126#include <stdbool.h>
15127
15128int fn_branch(bool do_branch1, bool do_branch2);
15129
15130#endif // BAR_H
15131#include "AGL/"ˇ"##,
15132 );
15133}
15134
15135#[gpui::test]
15136async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15137 init_test(cx, |_| {});
15138
15139 let mut cx = EditorLspTestContext::new_rust(
15140 lsp::ServerCapabilities {
15141 completion_provider: Some(lsp::CompletionOptions {
15142 trigger_characters: Some(vec![".".to_string()]),
15143 resolve_provider: Some(true),
15144 ..Default::default()
15145 }),
15146 ..Default::default()
15147 },
15148 cx,
15149 )
15150 .await;
15151
15152 cx.set_state("fn main() { let a = 2ˇ; }");
15153 cx.simulate_keystroke(".");
15154 let completion_item = lsp::CompletionItem {
15155 label: "Some".into(),
15156 kind: Some(lsp::CompletionItemKind::SNIPPET),
15157 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15158 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15159 kind: lsp::MarkupKind::Markdown,
15160 value: "```rust\nSome(2)\n```".to_string(),
15161 })),
15162 deprecated: Some(false),
15163 sort_text: Some("Some".to_string()),
15164 filter_text: Some("Some".to_string()),
15165 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15166 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15167 range: lsp::Range {
15168 start: lsp::Position {
15169 line: 0,
15170 character: 22,
15171 },
15172 end: lsp::Position {
15173 line: 0,
15174 character: 22,
15175 },
15176 },
15177 new_text: "Some(2)".to_string(),
15178 })),
15179 additional_text_edits: Some(vec![lsp::TextEdit {
15180 range: lsp::Range {
15181 start: lsp::Position {
15182 line: 0,
15183 character: 20,
15184 },
15185 end: lsp::Position {
15186 line: 0,
15187 character: 22,
15188 },
15189 },
15190 new_text: "".to_string(),
15191 }]),
15192 ..Default::default()
15193 };
15194
15195 let closure_completion_item = completion_item.clone();
15196 let counter = Arc::new(AtomicUsize::new(0));
15197 let counter_clone = counter.clone();
15198 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15199 let task_completion_item = closure_completion_item.clone();
15200 counter_clone.fetch_add(1, atomic::Ordering::Release);
15201 async move {
15202 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15203 is_incomplete: true,
15204 item_defaults: None,
15205 items: vec![task_completion_item],
15206 })))
15207 }
15208 });
15209
15210 cx.condition(|editor, _| editor.context_menu_visible())
15211 .await;
15212 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15213 assert!(request.next().await.is_some());
15214 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15215
15216 cx.simulate_keystrokes("S o m");
15217 cx.condition(|editor, _| editor.context_menu_visible())
15218 .await;
15219 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15220 assert!(request.next().await.is_some());
15221 assert!(request.next().await.is_some());
15222 assert!(request.next().await.is_some());
15223 request.close();
15224 assert!(request.next().await.is_none());
15225 assert_eq!(
15226 counter.load(atomic::Ordering::Acquire),
15227 4,
15228 "With the completions menu open, only one LSP request should happen per input"
15229 );
15230}
15231
15232#[gpui::test]
15233async fn test_toggle_comment(cx: &mut TestAppContext) {
15234 init_test(cx, |_| {});
15235 let mut cx = EditorTestContext::new(cx).await;
15236 let language = Arc::new(Language::new(
15237 LanguageConfig {
15238 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15239 ..Default::default()
15240 },
15241 Some(tree_sitter_rust::LANGUAGE.into()),
15242 ));
15243 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15244
15245 // If multiple selections intersect a line, the line is only toggled once.
15246 cx.set_state(indoc! {"
15247 fn a() {
15248 «//b();
15249 ˇ»// «c();
15250 //ˇ» d();
15251 }
15252 "});
15253
15254 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15255
15256 cx.assert_editor_state(indoc! {"
15257 fn a() {
15258 «b();
15259 c();
15260 ˇ» d();
15261 }
15262 "});
15263
15264 // The comment prefix is inserted at the same column for every line in a
15265 // selection.
15266 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15267
15268 cx.assert_editor_state(indoc! {"
15269 fn a() {
15270 // «b();
15271 // c();
15272 ˇ»// d();
15273 }
15274 "});
15275
15276 // If a selection ends at the beginning of a line, that line is not toggled.
15277 cx.set_selections_state(indoc! {"
15278 fn a() {
15279 // b();
15280 «// c();
15281 ˇ» // d();
15282 }
15283 "});
15284
15285 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15286
15287 cx.assert_editor_state(indoc! {"
15288 fn a() {
15289 // b();
15290 «c();
15291 ˇ» // d();
15292 }
15293 "});
15294
15295 // If a selection span a single line and is empty, the line is toggled.
15296 cx.set_state(indoc! {"
15297 fn a() {
15298 a();
15299 b();
15300 ˇ
15301 }
15302 "});
15303
15304 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15305
15306 cx.assert_editor_state(indoc! {"
15307 fn a() {
15308 a();
15309 b();
15310 //•ˇ
15311 }
15312 "});
15313
15314 // If a selection span multiple lines, empty lines are not toggled.
15315 cx.set_state(indoc! {"
15316 fn a() {
15317 «a();
15318
15319 c();ˇ»
15320 }
15321 "});
15322
15323 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15324
15325 cx.assert_editor_state(indoc! {"
15326 fn a() {
15327 // «a();
15328
15329 // c();ˇ»
15330 }
15331 "});
15332
15333 // If a selection includes multiple comment prefixes, all lines are uncommented.
15334 cx.set_state(indoc! {"
15335 fn a() {
15336 «// a();
15337 /// b();
15338 //! c();ˇ»
15339 }
15340 "});
15341
15342 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15343
15344 cx.assert_editor_state(indoc! {"
15345 fn a() {
15346 «a();
15347 b();
15348 c();ˇ»
15349 }
15350 "});
15351}
15352
15353#[gpui::test]
15354async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15355 init_test(cx, |_| {});
15356 let mut cx = EditorTestContext::new(cx).await;
15357 let language = Arc::new(Language::new(
15358 LanguageConfig {
15359 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15360 ..Default::default()
15361 },
15362 Some(tree_sitter_rust::LANGUAGE.into()),
15363 ));
15364 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15365
15366 let toggle_comments = &ToggleComments {
15367 advance_downwards: false,
15368 ignore_indent: true,
15369 };
15370
15371 // If multiple selections intersect a line, the line is only toggled once.
15372 cx.set_state(indoc! {"
15373 fn a() {
15374 // «b();
15375 // c();
15376 // ˇ» d();
15377 }
15378 "});
15379
15380 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15381
15382 cx.assert_editor_state(indoc! {"
15383 fn a() {
15384 «b();
15385 c();
15386 ˇ» d();
15387 }
15388 "});
15389
15390 // The comment prefix is inserted at the beginning of each line
15391 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15392
15393 cx.assert_editor_state(indoc! {"
15394 fn a() {
15395 // «b();
15396 // c();
15397 // ˇ» d();
15398 }
15399 "});
15400
15401 // If a selection ends at the beginning of a line, that line is not toggled.
15402 cx.set_selections_state(indoc! {"
15403 fn a() {
15404 // b();
15405 // «c();
15406 ˇ»// d();
15407 }
15408 "});
15409
15410 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15411
15412 cx.assert_editor_state(indoc! {"
15413 fn a() {
15414 // b();
15415 «c();
15416 ˇ»// d();
15417 }
15418 "});
15419
15420 // If a selection span a single line and is empty, the line is toggled.
15421 cx.set_state(indoc! {"
15422 fn a() {
15423 a();
15424 b();
15425 ˇ
15426 }
15427 "});
15428
15429 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15430
15431 cx.assert_editor_state(indoc! {"
15432 fn a() {
15433 a();
15434 b();
15435 //ˇ
15436 }
15437 "});
15438
15439 // If a selection span multiple lines, empty lines are not toggled.
15440 cx.set_state(indoc! {"
15441 fn a() {
15442 «a();
15443
15444 c();ˇ»
15445 }
15446 "});
15447
15448 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15449
15450 cx.assert_editor_state(indoc! {"
15451 fn a() {
15452 // «a();
15453
15454 // c();ˇ»
15455 }
15456 "});
15457
15458 // If a selection includes multiple comment prefixes, all lines are uncommented.
15459 cx.set_state(indoc! {"
15460 fn a() {
15461 // «a();
15462 /// b();
15463 //! c();ˇ»
15464 }
15465 "});
15466
15467 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15468
15469 cx.assert_editor_state(indoc! {"
15470 fn a() {
15471 «a();
15472 b();
15473 c();ˇ»
15474 }
15475 "});
15476}
15477
15478#[gpui::test]
15479async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15480 init_test(cx, |_| {});
15481
15482 let language = Arc::new(Language::new(
15483 LanguageConfig {
15484 line_comments: vec!["// ".into()],
15485 ..Default::default()
15486 },
15487 Some(tree_sitter_rust::LANGUAGE.into()),
15488 ));
15489
15490 let mut cx = EditorTestContext::new(cx).await;
15491
15492 cx.language_registry().add(language.clone());
15493 cx.update_buffer(|buffer, cx| {
15494 buffer.set_language(Some(language), cx);
15495 });
15496
15497 let toggle_comments = &ToggleComments {
15498 advance_downwards: true,
15499 ignore_indent: false,
15500 };
15501
15502 // Single cursor on one line -> advance
15503 // Cursor moves horizontally 3 characters as well on non-blank line
15504 cx.set_state(indoc!(
15505 "fn a() {
15506 ˇdog();
15507 cat();
15508 }"
15509 ));
15510 cx.update_editor(|editor, window, cx| {
15511 editor.toggle_comments(toggle_comments, window, cx);
15512 });
15513 cx.assert_editor_state(indoc!(
15514 "fn a() {
15515 // dog();
15516 catˇ();
15517 }"
15518 ));
15519
15520 // Single selection on one line -> don't advance
15521 cx.set_state(indoc!(
15522 "fn a() {
15523 «dog()ˇ»;
15524 cat();
15525 }"
15526 ));
15527 cx.update_editor(|editor, window, cx| {
15528 editor.toggle_comments(toggle_comments, window, cx);
15529 });
15530 cx.assert_editor_state(indoc!(
15531 "fn a() {
15532 // «dog()ˇ»;
15533 cat();
15534 }"
15535 ));
15536
15537 // Multiple cursors on one line -> advance
15538 cx.set_state(indoc!(
15539 "fn a() {
15540 ˇdˇog();
15541 cat();
15542 }"
15543 ));
15544 cx.update_editor(|editor, window, cx| {
15545 editor.toggle_comments(toggle_comments, window, cx);
15546 });
15547 cx.assert_editor_state(indoc!(
15548 "fn a() {
15549 // dog();
15550 catˇ(ˇ);
15551 }"
15552 ));
15553
15554 // Multiple cursors on one line, with selection -> don't advance
15555 cx.set_state(indoc!(
15556 "fn a() {
15557 ˇdˇog«()ˇ»;
15558 cat();
15559 }"
15560 ));
15561 cx.update_editor(|editor, window, cx| {
15562 editor.toggle_comments(toggle_comments, window, cx);
15563 });
15564 cx.assert_editor_state(indoc!(
15565 "fn a() {
15566 // ˇdˇog«()ˇ»;
15567 cat();
15568 }"
15569 ));
15570
15571 // Single cursor on one line -> advance
15572 // Cursor moves to column 0 on blank line
15573 cx.set_state(indoc!(
15574 "fn a() {
15575 ˇdog();
15576
15577 cat();
15578 }"
15579 ));
15580 cx.update_editor(|editor, window, cx| {
15581 editor.toggle_comments(toggle_comments, window, cx);
15582 });
15583 cx.assert_editor_state(indoc!(
15584 "fn a() {
15585 // dog();
15586 ˇ
15587 cat();
15588 }"
15589 ));
15590
15591 // Single cursor on one line -> advance
15592 // Cursor starts and ends at column 0
15593 cx.set_state(indoc!(
15594 "fn a() {
15595 ˇ dog();
15596 cat();
15597 }"
15598 ));
15599 cx.update_editor(|editor, window, cx| {
15600 editor.toggle_comments(toggle_comments, window, cx);
15601 });
15602 cx.assert_editor_state(indoc!(
15603 "fn a() {
15604 // dog();
15605 ˇ cat();
15606 }"
15607 ));
15608}
15609
15610#[gpui::test]
15611async fn test_toggle_block_comment(cx: &mut TestAppContext) {
15612 init_test(cx, |_| {});
15613
15614 let mut cx = EditorTestContext::new(cx).await;
15615
15616 let html_language = Arc::new(
15617 Language::new(
15618 LanguageConfig {
15619 name: "HTML".into(),
15620 block_comment: Some(BlockCommentConfig {
15621 start: "<!-- ".into(),
15622 prefix: "".into(),
15623 end: " -->".into(),
15624 tab_size: 0,
15625 }),
15626 ..Default::default()
15627 },
15628 Some(tree_sitter_html::LANGUAGE.into()),
15629 )
15630 .with_injection_query(
15631 r#"
15632 (script_element
15633 (raw_text) @injection.content
15634 (#set! injection.language "javascript"))
15635 "#,
15636 )
15637 .unwrap(),
15638 );
15639
15640 let javascript_language = Arc::new(Language::new(
15641 LanguageConfig {
15642 name: "JavaScript".into(),
15643 line_comments: vec!["// ".into()],
15644 ..Default::default()
15645 },
15646 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
15647 ));
15648
15649 cx.language_registry().add(html_language.clone());
15650 cx.language_registry().add(javascript_language);
15651 cx.update_buffer(|buffer, cx| {
15652 buffer.set_language(Some(html_language), cx);
15653 });
15654
15655 // Toggle comments for empty selections
15656 cx.set_state(
15657 &r#"
15658 <p>A</p>ˇ
15659 <p>B</p>ˇ
15660 <p>C</p>ˇ
15661 "#
15662 .unindent(),
15663 );
15664 cx.update_editor(|editor, window, cx| {
15665 editor.toggle_comments(&ToggleComments::default(), window, cx)
15666 });
15667 cx.assert_editor_state(
15668 &r#"
15669 <!-- <p>A</p>ˇ -->
15670 <!-- <p>B</p>ˇ -->
15671 <!-- <p>C</p>ˇ -->
15672 "#
15673 .unindent(),
15674 );
15675 cx.update_editor(|editor, window, cx| {
15676 editor.toggle_comments(&ToggleComments::default(), window, cx)
15677 });
15678 cx.assert_editor_state(
15679 &r#"
15680 <p>A</p>ˇ
15681 <p>B</p>ˇ
15682 <p>C</p>ˇ
15683 "#
15684 .unindent(),
15685 );
15686
15687 // Toggle comments for mixture of empty and non-empty selections, where
15688 // multiple selections occupy a given line.
15689 cx.set_state(
15690 &r#"
15691 <p>A«</p>
15692 <p>ˇ»B</p>ˇ
15693 <p>C«</p>
15694 <p>ˇ»D</p>ˇ
15695 "#
15696 .unindent(),
15697 );
15698
15699 cx.update_editor(|editor, window, cx| {
15700 editor.toggle_comments(&ToggleComments::default(), window, cx)
15701 });
15702 cx.assert_editor_state(
15703 &r#"
15704 <!-- <p>A«</p>
15705 <p>ˇ»B</p>ˇ -->
15706 <!-- <p>C«</p>
15707 <p>ˇ»D</p>ˇ -->
15708 "#
15709 .unindent(),
15710 );
15711 cx.update_editor(|editor, window, cx| {
15712 editor.toggle_comments(&ToggleComments::default(), window, cx)
15713 });
15714 cx.assert_editor_state(
15715 &r#"
15716 <p>A«</p>
15717 <p>ˇ»B</p>ˇ
15718 <p>C«</p>
15719 <p>ˇ»D</p>ˇ
15720 "#
15721 .unindent(),
15722 );
15723
15724 // Toggle comments when different languages are active for different
15725 // selections.
15726 cx.set_state(
15727 &r#"
15728 ˇ<script>
15729 ˇvar x = new Y();
15730 ˇ</script>
15731 "#
15732 .unindent(),
15733 );
15734 cx.executor().run_until_parked();
15735 cx.update_editor(|editor, window, cx| {
15736 editor.toggle_comments(&ToggleComments::default(), window, cx)
15737 });
15738 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
15739 // Uncommenting and commenting from this position brings in even more wrong artifacts.
15740 cx.assert_editor_state(
15741 &r#"
15742 <!-- ˇ<script> -->
15743 // ˇvar x = new Y();
15744 <!-- ˇ</script> -->
15745 "#
15746 .unindent(),
15747 );
15748}
15749
15750#[gpui::test]
15751fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
15752 init_test(cx, |_| {});
15753
15754 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15755 let multibuffer = cx.new(|cx| {
15756 let mut multibuffer = MultiBuffer::new(ReadWrite);
15757 multibuffer.push_excerpts(
15758 buffer.clone(),
15759 [
15760 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
15761 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
15762 ],
15763 cx,
15764 );
15765 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
15766 multibuffer
15767 });
15768
15769 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15770 editor.update_in(cx, |editor, window, cx| {
15771 assert_eq!(editor.text(cx), "aaaa\nbbbb");
15772 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15773 s.select_ranges([
15774 Point::new(0, 0)..Point::new(0, 0),
15775 Point::new(1, 0)..Point::new(1, 0),
15776 ])
15777 });
15778
15779 editor.handle_input("X", window, cx);
15780 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
15781 assert_eq!(
15782 editor.selections.ranges(cx),
15783 [
15784 Point::new(0, 1)..Point::new(0, 1),
15785 Point::new(1, 1)..Point::new(1, 1),
15786 ]
15787 );
15788
15789 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
15790 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15791 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
15792 });
15793 editor.backspace(&Default::default(), window, cx);
15794 assert_eq!(editor.text(cx), "Xa\nbbb");
15795 assert_eq!(
15796 editor.selections.ranges(cx),
15797 [Point::new(1, 0)..Point::new(1, 0)]
15798 );
15799
15800 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15801 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
15802 });
15803 editor.backspace(&Default::default(), window, cx);
15804 assert_eq!(editor.text(cx), "X\nbb");
15805 assert_eq!(
15806 editor.selections.ranges(cx),
15807 [Point::new(0, 1)..Point::new(0, 1)]
15808 );
15809 });
15810}
15811
15812#[gpui::test]
15813fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
15814 init_test(cx, |_| {});
15815
15816 let markers = vec![('[', ']').into(), ('(', ')').into()];
15817 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
15818 indoc! {"
15819 [aaaa
15820 (bbbb]
15821 cccc)",
15822 },
15823 markers.clone(),
15824 );
15825 let excerpt_ranges = markers.into_iter().map(|marker| {
15826 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
15827 ExcerptRange::new(context)
15828 });
15829 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
15830 let multibuffer = cx.new(|cx| {
15831 let mut multibuffer = MultiBuffer::new(ReadWrite);
15832 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
15833 multibuffer
15834 });
15835
15836 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
15837 editor.update_in(cx, |editor, window, cx| {
15838 let (expected_text, selection_ranges) = marked_text_ranges(
15839 indoc! {"
15840 aaaa
15841 bˇbbb
15842 bˇbbˇb
15843 cccc"
15844 },
15845 true,
15846 );
15847 assert_eq!(editor.text(cx), expected_text);
15848 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15849 s.select_ranges(selection_ranges)
15850 });
15851
15852 editor.handle_input("X", window, cx);
15853
15854 let (expected_text, expected_selections) = marked_text_ranges(
15855 indoc! {"
15856 aaaa
15857 bXˇbbXb
15858 bXˇbbXˇb
15859 cccc"
15860 },
15861 false,
15862 );
15863 assert_eq!(editor.text(cx), expected_text);
15864 assert_eq!(editor.selections.ranges(cx), expected_selections);
15865
15866 editor.newline(&Newline, window, cx);
15867 let (expected_text, expected_selections) = marked_text_ranges(
15868 indoc! {"
15869 aaaa
15870 bX
15871 ˇbbX
15872 b
15873 bX
15874 ˇbbX
15875 ˇb
15876 cccc"
15877 },
15878 false,
15879 );
15880 assert_eq!(editor.text(cx), expected_text);
15881 assert_eq!(editor.selections.ranges(cx), expected_selections);
15882 });
15883}
15884
15885#[gpui::test]
15886fn test_refresh_selections(cx: &mut TestAppContext) {
15887 init_test(cx, |_| {});
15888
15889 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15890 let mut excerpt1_id = None;
15891 let multibuffer = cx.new(|cx| {
15892 let mut multibuffer = MultiBuffer::new(ReadWrite);
15893 excerpt1_id = multibuffer
15894 .push_excerpts(
15895 buffer.clone(),
15896 [
15897 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15898 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15899 ],
15900 cx,
15901 )
15902 .into_iter()
15903 .next();
15904 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15905 multibuffer
15906 });
15907
15908 let editor = cx.add_window(|window, cx| {
15909 let mut editor = build_editor(multibuffer.clone(), window, cx);
15910 let snapshot = editor.snapshot(window, cx);
15911 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
15912 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
15913 });
15914 editor.begin_selection(
15915 Point::new(2, 1).to_display_point(&snapshot),
15916 true,
15917 1,
15918 window,
15919 cx,
15920 );
15921 assert_eq!(
15922 editor.selections.ranges(cx),
15923 [
15924 Point::new(1, 3)..Point::new(1, 3),
15925 Point::new(2, 1)..Point::new(2, 1),
15926 ]
15927 );
15928 editor
15929 });
15930
15931 // Refreshing selections is a no-op when excerpts haven't changed.
15932 _ = editor.update(cx, |editor, window, cx| {
15933 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15934 assert_eq!(
15935 editor.selections.ranges(cx),
15936 [
15937 Point::new(1, 3)..Point::new(1, 3),
15938 Point::new(2, 1)..Point::new(2, 1),
15939 ]
15940 );
15941 });
15942
15943 multibuffer.update(cx, |multibuffer, cx| {
15944 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
15945 });
15946 _ = editor.update(cx, |editor, window, cx| {
15947 // Removing an excerpt causes the first selection to become degenerate.
15948 assert_eq!(
15949 editor.selections.ranges(cx),
15950 [
15951 Point::new(0, 0)..Point::new(0, 0),
15952 Point::new(0, 1)..Point::new(0, 1)
15953 ]
15954 );
15955
15956 // Refreshing selections will relocate the first selection to the original buffer
15957 // location.
15958 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
15959 assert_eq!(
15960 editor.selections.ranges(cx),
15961 [
15962 Point::new(0, 1)..Point::new(0, 1),
15963 Point::new(0, 3)..Point::new(0, 3)
15964 ]
15965 );
15966 assert!(editor.selections.pending_anchor().is_some());
15967 });
15968}
15969
15970#[gpui::test]
15971fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
15972 init_test(cx, |_| {});
15973
15974 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
15975 let mut excerpt1_id = None;
15976 let multibuffer = cx.new(|cx| {
15977 let mut multibuffer = MultiBuffer::new(ReadWrite);
15978 excerpt1_id = multibuffer
15979 .push_excerpts(
15980 buffer.clone(),
15981 [
15982 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
15983 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
15984 ],
15985 cx,
15986 )
15987 .into_iter()
15988 .next();
15989 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
15990 multibuffer
15991 });
15992
15993 let editor = cx.add_window(|window, cx| {
15994 let mut editor = build_editor(multibuffer.clone(), window, cx);
15995 let snapshot = editor.snapshot(window, cx);
15996 editor.begin_selection(
15997 Point::new(1, 3).to_display_point(&snapshot),
15998 false,
15999 1,
16000 window,
16001 cx,
16002 );
16003 assert_eq!(
16004 editor.selections.ranges(cx),
16005 [Point::new(1, 3)..Point::new(1, 3)]
16006 );
16007 editor
16008 });
16009
16010 multibuffer.update(cx, |multibuffer, cx| {
16011 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16012 });
16013 _ = editor.update(cx, |editor, window, cx| {
16014 assert_eq!(
16015 editor.selections.ranges(cx),
16016 [Point::new(0, 0)..Point::new(0, 0)]
16017 );
16018
16019 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16020 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16021 assert_eq!(
16022 editor.selections.ranges(cx),
16023 [Point::new(0, 3)..Point::new(0, 3)]
16024 );
16025 assert!(editor.selections.pending_anchor().is_some());
16026 });
16027}
16028
16029#[gpui::test]
16030async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16031 init_test(cx, |_| {});
16032
16033 let language = Arc::new(
16034 Language::new(
16035 LanguageConfig {
16036 brackets: BracketPairConfig {
16037 pairs: vec![
16038 BracketPair {
16039 start: "{".to_string(),
16040 end: "}".to_string(),
16041 close: true,
16042 surround: true,
16043 newline: true,
16044 },
16045 BracketPair {
16046 start: "/* ".to_string(),
16047 end: " */".to_string(),
16048 close: true,
16049 surround: true,
16050 newline: true,
16051 },
16052 ],
16053 ..Default::default()
16054 },
16055 ..Default::default()
16056 },
16057 Some(tree_sitter_rust::LANGUAGE.into()),
16058 )
16059 .with_indents_query("")
16060 .unwrap(),
16061 );
16062
16063 let text = concat!(
16064 "{ }\n", //
16065 " x\n", //
16066 " /* */\n", //
16067 "x\n", //
16068 "{{} }\n", //
16069 );
16070
16071 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16072 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16073 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16074 editor
16075 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16076 .await;
16077
16078 editor.update_in(cx, |editor, window, cx| {
16079 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16080 s.select_display_ranges([
16081 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16082 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16083 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16084 ])
16085 });
16086 editor.newline(&Newline, window, cx);
16087
16088 assert_eq!(
16089 editor.buffer().read(cx).read(cx).text(),
16090 concat!(
16091 "{ \n", // Suppress rustfmt
16092 "\n", //
16093 "}\n", //
16094 " x\n", //
16095 " /* \n", //
16096 " \n", //
16097 " */\n", //
16098 "x\n", //
16099 "{{} \n", //
16100 "}\n", //
16101 )
16102 );
16103 });
16104}
16105
16106#[gpui::test]
16107fn test_highlighted_ranges(cx: &mut TestAppContext) {
16108 init_test(cx, |_| {});
16109
16110 let editor = cx.add_window(|window, cx| {
16111 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16112 build_editor(buffer, window, cx)
16113 });
16114
16115 _ = editor.update(cx, |editor, window, cx| {
16116 struct Type1;
16117 struct Type2;
16118
16119 let buffer = editor.buffer.read(cx).snapshot(cx);
16120
16121 let anchor_range =
16122 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16123
16124 editor.highlight_background::<Type1>(
16125 &[
16126 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16127 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16128 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16129 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16130 ],
16131 |_| Hsla::red(),
16132 cx,
16133 );
16134 editor.highlight_background::<Type2>(
16135 &[
16136 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16137 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16138 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16139 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16140 ],
16141 |_| Hsla::green(),
16142 cx,
16143 );
16144
16145 let snapshot = editor.snapshot(window, cx);
16146 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16147 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16148 &snapshot,
16149 cx.theme(),
16150 );
16151 assert_eq!(
16152 highlighted_ranges,
16153 &[
16154 (
16155 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16156 Hsla::green(),
16157 ),
16158 (
16159 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16160 Hsla::red(),
16161 ),
16162 (
16163 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16164 Hsla::green(),
16165 ),
16166 (
16167 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16168 Hsla::red(),
16169 ),
16170 ]
16171 );
16172 assert_eq!(
16173 editor.sorted_background_highlights_in_range(
16174 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16175 &snapshot,
16176 cx.theme(),
16177 ),
16178 &[(
16179 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16180 Hsla::red(),
16181 )]
16182 );
16183 });
16184}
16185
16186#[gpui::test]
16187async fn test_following(cx: &mut TestAppContext) {
16188 init_test(cx, |_| {});
16189
16190 let fs = FakeFs::new(cx.executor());
16191 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16192
16193 let buffer = project.update(cx, |project, cx| {
16194 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16195 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16196 });
16197 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16198 let follower = cx.update(|cx| {
16199 cx.open_window(
16200 WindowOptions {
16201 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16202 gpui::Point::new(px(0.), px(0.)),
16203 gpui::Point::new(px(10.), px(80.)),
16204 ))),
16205 ..Default::default()
16206 },
16207 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16208 )
16209 .unwrap()
16210 });
16211
16212 let is_still_following = Rc::new(RefCell::new(true));
16213 let follower_edit_event_count = Rc::new(RefCell::new(0));
16214 let pending_update = Rc::new(RefCell::new(None));
16215 let leader_entity = leader.root(cx).unwrap();
16216 let follower_entity = follower.root(cx).unwrap();
16217 _ = follower.update(cx, {
16218 let update = pending_update.clone();
16219 let is_still_following = is_still_following.clone();
16220 let follower_edit_event_count = follower_edit_event_count.clone();
16221 |_, window, cx| {
16222 cx.subscribe_in(
16223 &leader_entity,
16224 window,
16225 move |_, leader, event, window, cx| {
16226 leader.read(cx).add_event_to_update_proto(
16227 event,
16228 &mut update.borrow_mut(),
16229 window,
16230 cx,
16231 );
16232 },
16233 )
16234 .detach();
16235
16236 cx.subscribe_in(
16237 &follower_entity,
16238 window,
16239 move |_, _, event: &EditorEvent, _window, _cx| {
16240 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16241 *is_still_following.borrow_mut() = false;
16242 }
16243
16244 if let EditorEvent::BufferEdited = event {
16245 *follower_edit_event_count.borrow_mut() += 1;
16246 }
16247 },
16248 )
16249 .detach();
16250 }
16251 });
16252
16253 // Update the selections only
16254 _ = leader.update(cx, |leader, window, cx| {
16255 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16256 s.select_ranges([1..1])
16257 });
16258 });
16259 follower
16260 .update(cx, |follower, window, cx| {
16261 follower.apply_update_proto(
16262 &project,
16263 pending_update.borrow_mut().take().unwrap(),
16264 window,
16265 cx,
16266 )
16267 })
16268 .unwrap()
16269 .await
16270 .unwrap();
16271 _ = follower.update(cx, |follower, _, cx| {
16272 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
16273 });
16274 assert!(*is_still_following.borrow());
16275 assert_eq!(*follower_edit_event_count.borrow(), 0);
16276
16277 // Update the scroll position only
16278 _ = leader.update(cx, |leader, window, cx| {
16279 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16280 });
16281 follower
16282 .update(cx, |follower, window, cx| {
16283 follower.apply_update_proto(
16284 &project,
16285 pending_update.borrow_mut().take().unwrap(),
16286 window,
16287 cx,
16288 )
16289 })
16290 .unwrap()
16291 .await
16292 .unwrap();
16293 assert_eq!(
16294 follower
16295 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16296 .unwrap(),
16297 gpui::Point::new(1.5, 3.5)
16298 );
16299 assert!(*is_still_following.borrow());
16300 assert_eq!(*follower_edit_event_count.borrow(), 0);
16301
16302 // Update the selections and scroll position. The follower's scroll position is updated
16303 // via autoscroll, not via the leader's exact scroll position.
16304 _ = leader.update(cx, |leader, window, cx| {
16305 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16306 s.select_ranges([0..0])
16307 });
16308 leader.request_autoscroll(Autoscroll::newest(), cx);
16309 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16310 });
16311 follower
16312 .update(cx, |follower, window, cx| {
16313 follower.apply_update_proto(
16314 &project,
16315 pending_update.borrow_mut().take().unwrap(),
16316 window,
16317 cx,
16318 )
16319 })
16320 .unwrap()
16321 .await
16322 .unwrap();
16323 _ = follower.update(cx, |follower, _, cx| {
16324 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16325 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
16326 });
16327 assert!(*is_still_following.borrow());
16328
16329 // Creating a pending selection that precedes another selection
16330 _ = leader.update(cx, |leader, window, cx| {
16331 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16332 s.select_ranges([1..1])
16333 });
16334 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16335 });
16336 follower
16337 .update(cx, |follower, window, cx| {
16338 follower.apply_update_proto(
16339 &project,
16340 pending_update.borrow_mut().take().unwrap(),
16341 window,
16342 cx,
16343 )
16344 })
16345 .unwrap()
16346 .await
16347 .unwrap();
16348 _ = follower.update(cx, |follower, _, cx| {
16349 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
16350 });
16351 assert!(*is_still_following.borrow());
16352
16353 // Extend the pending selection so that it surrounds another selection
16354 _ = leader.update(cx, |leader, window, cx| {
16355 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16356 });
16357 follower
16358 .update(cx, |follower, window, cx| {
16359 follower.apply_update_proto(
16360 &project,
16361 pending_update.borrow_mut().take().unwrap(),
16362 window,
16363 cx,
16364 )
16365 })
16366 .unwrap()
16367 .await
16368 .unwrap();
16369 _ = follower.update(cx, |follower, _, cx| {
16370 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
16371 });
16372
16373 // Scrolling locally breaks the follow
16374 _ = follower.update(cx, |follower, window, cx| {
16375 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16376 follower.set_scroll_anchor(
16377 ScrollAnchor {
16378 anchor: top_anchor,
16379 offset: gpui::Point::new(0.0, 0.5),
16380 },
16381 window,
16382 cx,
16383 );
16384 });
16385 assert!(!(*is_still_following.borrow()));
16386}
16387
16388#[gpui::test]
16389async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16390 init_test(cx, |_| {});
16391
16392 let fs = FakeFs::new(cx.executor());
16393 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16394 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16395 let pane = workspace
16396 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16397 .unwrap();
16398
16399 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16400
16401 let leader = pane.update_in(cx, |_, window, cx| {
16402 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16403 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16404 });
16405
16406 // Start following the editor when it has no excerpts.
16407 let mut state_message =
16408 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16409 let workspace_entity = workspace.root(cx).unwrap();
16410 let follower_1 = cx
16411 .update_window(*workspace.deref(), |_, window, cx| {
16412 Editor::from_state_proto(
16413 workspace_entity,
16414 ViewId {
16415 creator: CollaboratorId::PeerId(PeerId::default()),
16416 id: 0,
16417 },
16418 &mut state_message,
16419 window,
16420 cx,
16421 )
16422 })
16423 .unwrap()
16424 .unwrap()
16425 .await
16426 .unwrap();
16427
16428 let update_message = Rc::new(RefCell::new(None));
16429 follower_1.update_in(cx, {
16430 let update = update_message.clone();
16431 |_, window, cx| {
16432 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16433 leader.read(cx).add_event_to_update_proto(
16434 event,
16435 &mut update.borrow_mut(),
16436 window,
16437 cx,
16438 );
16439 })
16440 .detach();
16441 }
16442 });
16443
16444 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16445 (
16446 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16447 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16448 )
16449 });
16450
16451 // Insert some excerpts.
16452 leader.update(cx, |leader, cx| {
16453 leader.buffer.update(cx, |multibuffer, cx| {
16454 multibuffer.set_excerpts_for_path(
16455 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
16456 buffer_1.clone(),
16457 vec![
16458 Point::row_range(0..3),
16459 Point::row_range(1..6),
16460 Point::row_range(12..15),
16461 ],
16462 0,
16463 cx,
16464 );
16465 multibuffer.set_excerpts_for_path(
16466 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
16467 buffer_2.clone(),
16468 vec![Point::row_range(0..6), Point::row_range(8..12)],
16469 0,
16470 cx,
16471 );
16472 });
16473 });
16474
16475 // Apply the update of adding the excerpts.
16476 follower_1
16477 .update_in(cx, |follower, window, cx| {
16478 follower.apply_update_proto(
16479 &project,
16480 update_message.borrow().clone().unwrap(),
16481 window,
16482 cx,
16483 )
16484 })
16485 .await
16486 .unwrap();
16487 assert_eq!(
16488 follower_1.update(cx, |editor, cx| editor.text(cx)),
16489 leader.update(cx, |editor, cx| editor.text(cx))
16490 );
16491 update_message.borrow_mut().take();
16492
16493 // Start following separately after it already has excerpts.
16494 let mut state_message =
16495 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16496 let workspace_entity = workspace.root(cx).unwrap();
16497 let follower_2 = cx
16498 .update_window(*workspace.deref(), |_, window, cx| {
16499 Editor::from_state_proto(
16500 workspace_entity,
16501 ViewId {
16502 creator: CollaboratorId::PeerId(PeerId::default()),
16503 id: 0,
16504 },
16505 &mut state_message,
16506 window,
16507 cx,
16508 )
16509 })
16510 .unwrap()
16511 .unwrap()
16512 .await
16513 .unwrap();
16514 assert_eq!(
16515 follower_2.update(cx, |editor, cx| editor.text(cx)),
16516 leader.update(cx, |editor, cx| editor.text(cx))
16517 );
16518
16519 // Remove some excerpts.
16520 leader.update(cx, |leader, cx| {
16521 leader.buffer.update(cx, |multibuffer, cx| {
16522 let excerpt_ids = multibuffer.excerpt_ids();
16523 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16524 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16525 });
16526 });
16527
16528 // Apply the update of removing the excerpts.
16529 follower_1
16530 .update_in(cx, |follower, window, cx| {
16531 follower.apply_update_proto(
16532 &project,
16533 update_message.borrow().clone().unwrap(),
16534 window,
16535 cx,
16536 )
16537 })
16538 .await
16539 .unwrap();
16540 follower_2
16541 .update_in(cx, |follower, window, cx| {
16542 follower.apply_update_proto(
16543 &project,
16544 update_message.borrow().clone().unwrap(),
16545 window,
16546 cx,
16547 )
16548 })
16549 .await
16550 .unwrap();
16551 update_message.borrow_mut().take();
16552 assert_eq!(
16553 follower_1.update(cx, |editor, cx| editor.text(cx)),
16554 leader.update(cx, |editor, cx| editor.text(cx))
16555 );
16556}
16557
16558#[gpui::test]
16559async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16560 init_test(cx, |_| {});
16561
16562 let mut cx = EditorTestContext::new(cx).await;
16563 let lsp_store =
16564 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
16565
16566 cx.set_state(indoc! {"
16567 ˇfn func(abc def: i32) -> u32 {
16568 }
16569 "});
16570
16571 cx.update(|_, cx| {
16572 lsp_store.update(cx, |lsp_store, cx| {
16573 lsp_store
16574 .update_diagnostics(
16575 LanguageServerId(0),
16576 lsp::PublishDiagnosticsParams {
16577 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
16578 version: None,
16579 diagnostics: vec![
16580 lsp::Diagnostic {
16581 range: lsp::Range::new(
16582 lsp::Position::new(0, 11),
16583 lsp::Position::new(0, 12),
16584 ),
16585 severity: Some(lsp::DiagnosticSeverity::ERROR),
16586 ..Default::default()
16587 },
16588 lsp::Diagnostic {
16589 range: lsp::Range::new(
16590 lsp::Position::new(0, 12),
16591 lsp::Position::new(0, 15),
16592 ),
16593 severity: Some(lsp::DiagnosticSeverity::ERROR),
16594 ..Default::default()
16595 },
16596 lsp::Diagnostic {
16597 range: lsp::Range::new(
16598 lsp::Position::new(0, 25),
16599 lsp::Position::new(0, 28),
16600 ),
16601 severity: Some(lsp::DiagnosticSeverity::ERROR),
16602 ..Default::default()
16603 },
16604 ],
16605 },
16606 None,
16607 DiagnosticSourceKind::Pushed,
16608 &[],
16609 cx,
16610 )
16611 .unwrap()
16612 });
16613 });
16614
16615 executor.run_until_parked();
16616
16617 cx.update_editor(|editor, window, cx| {
16618 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16619 });
16620
16621 cx.assert_editor_state(indoc! {"
16622 fn func(abc def: i32) -> ˇu32 {
16623 }
16624 "});
16625
16626 cx.update_editor(|editor, window, cx| {
16627 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16628 });
16629
16630 cx.assert_editor_state(indoc! {"
16631 fn func(abc ˇdef: i32) -> u32 {
16632 }
16633 "});
16634
16635 cx.update_editor(|editor, window, cx| {
16636 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16637 });
16638
16639 cx.assert_editor_state(indoc! {"
16640 fn func(abcˇ def: i32) -> u32 {
16641 }
16642 "});
16643
16644 cx.update_editor(|editor, window, cx| {
16645 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
16646 });
16647
16648 cx.assert_editor_state(indoc! {"
16649 fn func(abc def: i32) -> ˇu32 {
16650 }
16651 "});
16652}
16653
16654#[gpui::test]
16655async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16656 init_test(cx, |_| {});
16657
16658 let mut cx = EditorTestContext::new(cx).await;
16659
16660 let diff_base = r#"
16661 use some::mod;
16662
16663 const A: u32 = 42;
16664
16665 fn main() {
16666 println!("hello");
16667
16668 println!("world");
16669 }
16670 "#
16671 .unindent();
16672
16673 // Edits are modified, removed, modified, added
16674 cx.set_state(
16675 &r#"
16676 use some::modified;
16677
16678 ˇ
16679 fn main() {
16680 println!("hello there");
16681
16682 println!("around the");
16683 println!("world");
16684 }
16685 "#
16686 .unindent(),
16687 );
16688
16689 cx.set_head_text(&diff_base);
16690 executor.run_until_parked();
16691
16692 cx.update_editor(|editor, window, cx| {
16693 //Wrap around the bottom of the buffer
16694 for _ in 0..3 {
16695 editor.go_to_next_hunk(&GoToHunk, window, cx);
16696 }
16697 });
16698
16699 cx.assert_editor_state(
16700 &r#"
16701 ˇuse some::modified;
16702
16703
16704 fn main() {
16705 println!("hello there");
16706
16707 println!("around the");
16708 println!("world");
16709 }
16710 "#
16711 .unindent(),
16712 );
16713
16714 cx.update_editor(|editor, window, cx| {
16715 //Wrap around the top of the buffer
16716 for _ in 0..2 {
16717 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16718 }
16719 });
16720
16721 cx.assert_editor_state(
16722 &r#"
16723 use some::modified;
16724
16725
16726 fn main() {
16727 ˇ println!("hello there");
16728
16729 println!("around the");
16730 println!("world");
16731 }
16732 "#
16733 .unindent(),
16734 );
16735
16736 cx.update_editor(|editor, window, cx| {
16737 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16738 });
16739
16740 cx.assert_editor_state(
16741 &r#"
16742 use some::modified;
16743
16744 ˇ
16745 fn main() {
16746 println!("hello there");
16747
16748 println!("around the");
16749 println!("world");
16750 }
16751 "#
16752 .unindent(),
16753 );
16754
16755 cx.update_editor(|editor, window, cx| {
16756 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16757 });
16758
16759 cx.assert_editor_state(
16760 &r#"
16761 ˇuse some::modified;
16762
16763
16764 fn main() {
16765 println!("hello there");
16766
16767 println!("around the");
16768 println!("world");
16769 }
16770 "#
16771 .unindent(),
16772 );
16773
16774 cx.update_editor(|editor, window, cx| {
16775 for _ in 0..2 {
16776 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
16777 }
16778 });
16779
16780 cx.assert_editor_state(
16781 &r#"
16782 use some::modified;
16783
16784
16785 fn main() {
16786 ˇ println!("hello there");
16787
16788 println!("around the");
16789 println!("world");
16790 }
16791 "#
16792 .unindent(),
16793 );
16794
16795 cx.update_editor(|editor, window, cx| {
16796 editor.fold(&Fold, window, cx);
16797 });
16798
16799 cx.update_editor(|editor, window, cx| {
16800 editor.go_to_next_hunk(&GoToHunk, window, cx);
16801 });
16802
16803 cx.assert_editor_state(
16804 &r#"
16805 ˇuse some::modified;
16806
16807
16808 fn main() {
16809 println!("hello there");
16810
16811 println!("around the");
16812 println!("world");
16813 }
16814 "#
16815 .unindent(),
16816 );
16817}
16818
16819#[test]
16820fn test_split_words() {
16821 fn split(text: &str) -> Vec<&str> {
16822 split_words(text).collect()
16823 }
16824
16825 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
16826 assert_eq!(split("hello_world"), &["hello_", "world"]);
16827 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
16828 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
16829 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
16830 assert_eq!(split("helloworld"), &["helloworld"]);
16831
16832 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
16833}
16834
16835#[gpui::test]
16836async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
16837 init_test(cx, |_| {});
16838
16839 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
16840 let mut assert = |before, after| {
16841 let _state_context = cx.set_state(before);
16842 cx.run_until_parked();
16843 cx.update_editor(|editor, window, cx| {
16844 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
16845 });
16846 cx.run_until_parked();
16847 cx.assert_editor_state(after);
16848 };
16849
16850 // Outside bracket jumps to outside of matching bracket
16851 assert("console.logˇ(var);", "console.log(var)ˇ;");
16852 assert("console.log(var)ˇ;", "console.logˇ(var);");
16853
16854 // Inside bracket jumps to inside of matching bracket
16855 assert("console.log(ˇvar);", "console.log(varˇ);");
16856 assert("console.log(varˇ);", "console.log(ˇvar);");
16857
16858 // When outside a bracket and inside, favor jumping to the inside bracket
16859 assert(
16860 "console.log('foo', [1, 2, 3]ˇ);",
16861 "console.log(ˇ'foo', [1, 2, 3]);",
16862 );
16863 assert(
16864 "console.log(ˇ'foo', [1, 2, 3]);",
16865 "console.log('foo', [1, 2, 3]ˇ);",
16866 );
16867
16868 // Bias forward if two options are equally likely
16869 assert(
16870 "let result = curried_fun()ˇ();",
16871 "let result = curried_fun()()ˇ;",
16872 );
16873
16874 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
16875 assert(
16876 indoc! {"
16877 function test() {
16878 console.log('test')ˇ
16879 }"},
16880 indoc! {"
16881 function test() {
16882 console.logˇ('test')
16883 }"},
16884 );
16885}
16886
16887#[gpui::test]
16888async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
16889 init_test(cx, |_| {});
16890
16891 let fs = FakeFs::new(cx.executor());
16892 fs.insert_tree(
16893 path!("/a"),
16894 json!({
16895 "main.rs": "fn main() { let a = 5; }",
16896 "other.rs": "// Test file",
16897 }),
16898 )
16899 .await;
16900 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
16901
16902 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
16903 language_registry.add(Arc::new(Language::new(
16904 LanguageConfig {
16905 name: "Rust".into(),
16906 matcher: LanguageMatcher {
16907 path_suffixes: vec!["rs".to_string()],
16908 ..Default::default()
16909 },
16910 brackets: BracketPairConfig {
16911 pairs: vec![BracketPair {
16912 start: "{".to_string(),
16913 end: "}".to_string(),
16914 close: true,
16915 surround: true,
16916 newline: true,
16917 }],
16918 disabled_scopes_by_bracket_ix: Vec::new(),
16919 },
16920 ..Default::default()
16921 },
16922 Some(tree_sitter_rust::LANGUAGE.into()),
16923 )));
16924 let mut fake_servers = language_registry.register_fake_lsp(
16925 "Rust",
16926 FakeLspAdapter {
16927 capabilities: lsp::ServerCapabilities {
16928 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
16929 first_trigger_character: "{".to_string(),
16930 more_trigger_character: None,
16931 }),
16932 ..Default::default()
16933 },
16934 ..Default::default()
16935 },
16936 );
16937
16938 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16939
16940 let cx = &mut VisualTestContext::from_window(*workspace, cx);
16941
16942 let worktree_id = workspace
16943 .update(cx, |workspace, _, cx| {
16944 workspace.project().update(cx, |project, cx| {
16945 project.worktrees(cx).next().unwrap().read(cx).id()
16946 })
16947 })
16948 .unwrap();
16949
16950 let buffer = project
16951 .update(cx, |project, cx| {
16952 project.open_local_buffer(path!("/a/main.rs"), cx)
16953 })
16954 .await
16955 .unwrap();
16956 let editor_handle = workspace
16957 .update(cx, |workspace, window, cx| {
16958 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
16959 })
16960 .unwrap()
16961 .await
16962 .unwrap()
16963 .downcast::<Editor>()
16964 .unwrap();
16965
16966 cx.executor().start_waiting();
16967 let fake_server = fake_servers.next().await.unwrap();
16968
16969 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
16970 |params, _| async move {
16971 assert_eq!(
16972 params.text_document_position.text_document.uri,
16973 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
16974 );
16975 assert_eq!(
16976 params.text_document_position.position,
16977 lsp::Position::new(0, 21),
16978 );
16979
16980 Ok(Some(vec![lsp::TextEdit {
16981 new_text: "]".to_string(),
16982 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
16983 }]))
16984 },
16985 );
16986
16987 editor_handle.update_in(cx, |editor, window, cx| {
16988 window.focus(&editor.focus_handle(cx));
16989 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16990 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
16991 });
16992 editor.handle_input("{", window, cx);
16993 });
16994
16995 cx.executor().run_until_parked();
16996
16997 buffer.update(cx, |buffer, _| {
16998 assert_eq!(
16999 buffer.text(),
17000 "fn main() { let a = {5}; }",
17001 "No extra braces from on type formatting should appear in the buffer"
17002 )
17003 });
17004}
17005
17006#[gpui::test(iterations = 20, seeds(31))]
17007async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17008 init_test(cx, |_| {});
17009
17010 let mut cx = EditorLspTestContext::new_rust(
17011 lsp::ServerCapabilities {
17012 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17013 first_trigger_character: ".".to_string(),
17014 more_trigger_character: None,
17015 }),
17016 ..Default::default()
17017 },
17018 cx,
17019 )
17020 .await;
17021
17022 cx.update_buffer(|buffer, _| {
17023 // This causes autoindent to be async.
17024 buffer.set_sync_parse_timeout(Duration::ZERO)
17025 });
17026
17027 cx.set_state("fn c() {\n d()ˇ\n}\n");
17028 cx.simulate_keystroke("\n");
17029 cx.run_until_parked();
17030
17031 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17032 let mut request =
17033 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17034 let buffer_cloned = buffer_cloned.clone();
17035 async move {
17036 buffer_cloned.update(&mut cx, |buffer, _| {
17037 assert_eq!(
17038 buffer.text(),
17039 "fn c() {\n d()\n .\n}\n",
17040 "OnTypeFormatting should triggered after autoindent applied"
17041 )
17042 })?;
17043
17044 Ok(Some(vec![]))
17045 }
17046 });
17047
17048 cx.simulate_keystroke(".");
17049 cx.run_until_parked();
17050
17051 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17052 assert!(request.next().await.is_some());
17053 request.close();
17054 assert!(request.next().await.is_none());
17055}
17056
17057#[gpui::test]
17058async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17059 init_test(cx, |_| {});
17060
17061 let fs = FakeFs::new(cx.executor());
17062 fs.insert_tree(
17063 path!("/a"),
17064 json!({
17065 "main.rs": "fn main() { let a = 5; }",
17066 "other.rs": "// Test file",
17067 }),
17068 )
17069 .await;
17070
17071 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17072
17073 let server_restarts = Arc::new(AtomicUsize::new(0));
17074 let closure_restarts = Arc::clone(&server_restarts);
17075 let language_server_name = "test language server";
17076 let language_name: LanguageName = "Rust".into();
17077
17078 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17079 language_registry.add(Arc::new(Language::new(
17080 LanguageConfig {
17081 name: language_name.clone(),
17082 matcher: LanguageMatcher {
17083 path_suffixes: vec!["rs".to_string()],
17084 ..Default::default()
17085 },
17086 ..Default::default()
17087 },
17088 Some(tree_sitter_rust::LANGUAGE.into()),
17089 )));
17090 let mut fake_servers = language_registry.register_fake_lsp(
17091 "Rust",
17092 FakeLspAdapter {
17093 name: language_server_name,
17094 initialization_options: Some(json!({
17095 "testOptionValue": true
17096 })),
17097 initializer: Some(Box::new(move |fake_server| {
17098 let task_restarts = Arc::clone(&closure_restarts);
17099 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17100 task_restarts.fetch_add(1, atomic::Ordering::Release);
17101 futures::future::ready(Ok(()))
17102 });
17103 })),
17104 ..Default::default()
17105 },
17106 );
17107
17108 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17109 let _buffer = project
17110 .update(cx, |project, cx| {
17111 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17112 })
17113 .await
17114 .unwrap();
17115 let _fake_server = fake_servers.next().await.unwrap();
17116 update_test_language_settings(cx, |language_settings| {
17117 language_settings.languages.0.insert(
17118 language_name.clone().0,
17119 LanguageSettingsContent {
17120 tab_size: NonZeroU32::new(8),
17121 ..Default::default()
17122 },
17123 );
17124 });
17125 cx.executor().run_until_parked();
17126 assert_eq!(
17127 server_restarts.load(atomic::Ordering::Acquire),
17128 0,
17129 "Should not restart LSP server on an unrelated change"
17130 );
17131
17132 update_test_project_settings(cx, |project_settings| {
17133 project_settings.lsp.insert(
17134 "Some other server name".into(),
17135 LspSettings {
17136 binary: None,
17137 settings: None,
17138 initialization_options: Some(json!({
17139 "some other init value": false
17140 })),
17141 enable_lsp_tasks: false,
17142 fetch: None,
17143 },
17144 );
17145 });
17146 cx.executor().run_until_parked();
17147 assert_eq!(
17148 server_restarts.load(atomic::Ordering::Acquire),
17149 0,
17150 "Should not restart LSP server on an unrelated LSP settings change"
17151 );
17152
17153 update_test_project_settings(cx, |project_settings| {
17154 project_settings.lsp.insert(
17155 language_server_name.into(),
17156 LspSettings {
17157 binary: None,
17158 settings: None,
17159 initialization_options: Some(json!({
17160 "anotherInitValue": false
17161 })),
17162 enable_lsp_tasks: false,
17163 fetch: None,
17164 },
17165 );
17166 });
17167 cx.executor().run_until_parked();
17168 assert_eq!(
17169 server_restarts.load(atomic::Ordering::Acquire),
17170 1,
17171 "Should restart LSP server on a related LSP settings change"
17172 );
17173
17174 update_test_project_settings(cx, |project_settings| {
17175 project_settings.lsp.insert(
17176 language_server_name.into(),
17177 LspSettings {
17178 binary: None,
17179 settings: None,
17180 initialization_options: Some(json!({
17181 "anotherInitValue": false
17182 })),
17183 enable_lsp_tasks: false,
17184 fetch: None,
17185 },
17186 );
17187 });
17188 cx.executor().run_until_parked();
17189 assert_eq!(
17190 server_restarts.load(atomic::Ordering::Acquire),
17191 1,
17192 "Should not restart LSP server on a related LSP settings change that is the same"
17193 );
17194
17195 update_test_project_settings(cx, |project_settings| {
17196 project_settings.lsp.insert(
17197 language_server_name.into(),
17198 LspSettings {
17199 binary: None,
17200 settings: None,
17201 initialization_options: None,
17202 enable_lsp_tasks: false,
17203 fetch: None,
17204 },
17205 );
17206 });
17207 cx.executor().run_until_parked();
17208 assert_eq!(
17209 server_restarts.load(atomic::Ordering::Acquire),
17210 2,
17211 "Should restart LSP server on another related LSP settings change"
17212 );
17213}
17214
17215#[gpui::test]
17216async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17217 init_test(cx, |_| {});
17218
17219 let mut cx = EditorLspTestContext::new_rust(
17220 lsp::ServerCapabilities {
17221 completion_provider: Some(lsp::CompletionOptions {
17222 trigger_characters: Some(vec![".".to_string()]),
17223 resolve_provider: Some(true),
17224 ..Default::default()
17225 }),
17226 ..Default::default()
17227 },
17228 cx,
17229 )
17230 .await;
17231
17232 cx.set_state("fn main() { let a = 2ˇ; }");
17233 cx.simulate_keystroke(".");
17234 let completion_item = lsp::CompletionItem {
17235 label: "some".into(),
17236 kind: Some(lsp::CompletionItemKind::SNIPPET),
17237 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17238 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17239 kind: lsp::MarkupKind::Markdown,
17240 value: "```rust\nSome(2)\n```".to_string(),
17241 })),
17242 deprecated: Some(false),
17243 sort_text: Some("fffffff2".to_string()),
17244 filter_text: Some("some".to_string()),
17245 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17246 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17247 range: lsp::Range {
17248 start: lsp::Position {
17249 line: 0,
17250 character: 22,
17251 },
17252 end: lsp::Position {
17253 line: 0,
17254 character: 22,
17255 },
17256 },
17257 new_text: "Some(2)".to_string(),
17258 })),
17259 additional_text_edits: Some(vec![lsp::TextEdit {
17260 range: lsp::Range {
17261 start: lsp::Position {
17262 line: 0,
17263 character: 20,
17264 },
17265 end: lsp::Position {
17266 line: 0,
17267 character: 22,
17268 },
17269 },
17270 new_text: "".to_string(),
17271 }]),
17272 ..Default::default()
17273 };
17274
17275 let closure_completion_item = completion_item.clone();
17276 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17277 let task_completion_item = closure_completion_item.clone();
17278 async move {
17279 Ok(Some(lsp::CompletionResponse::Array(vec![
17280 task_completion_item,
17281 ])))
17282 }
17283 });
17284
17285 request.next().await;
17286
17287 cx.condition(|editor, _| editor.context_menu_visible())
17288 .await;
17289 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17290 editor
17291 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17292 .unwrap()
17293 });
17294 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17295
17296 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17297 let task_completion_item = completion_item.clone();
17298 async move { Ok(task_completion_item) }
17299 })
17300 .next()
17301 .await
17302 .unwrap();
17303 apply_additional_edits.await.unwrap();
17304 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17305}
17306
17307#[gpui::test]
17308async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17309 init_test(cx, |_| {});
17310
17311 let mut cx = EditorLspTestContext::new_rust(
17312 lsp::ServerCapabilities {
17313 completion_provider: Some(lsp::CompletionOptions {
17314 trigger_characters: Some(vec![".".to_string()]),
17315 resolve_provider: Some(true),
17316 ..Default::default()
17317 }),
17318 ..Default::default()
17319 },
17320 cx,
17321 )
17322 .await;
17323
17324 cx.set_state("fn main() { let a = 2ˇ; }");
17325 cx.simulate_keystroke(".");
17326
17327 let item1 = lsp::CompletionItem {
17328 label: "method id()".to_string(),
17329 filter_text: Some("id".to_string()),
17330 detail: None,
17331 documentation: None,
17332 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17333 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17334 new_text: ".id".to_string(),
17335 })),
17336 ..lsp::CompletionItem::default()
17337 };
17338
17339 let item2 = lsp::CompletionItem {
17340 label: "other".to_string(),
17341 filter_text: Some("other".to_string()),
17342 detail: None,
17343 documentation: None,
17344 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17345 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17346 new_text: ".other".to_string(),
17347 })),
17348 ..lsp::CompletionItem::default()
17349 };
17350
17351 let item1 = item1.clone();
17352 cx.set_request_handler::<lsp::request::Completion, _, _>({
17353 let item1 = item1.clone();
17354 move |_, _, _| {
17355 let item1 = item1.clone();
17356 let item2 = item2.clone();
17357 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17358 }
17359 })
17360 .next()
17361 .await;
17362
17363 cx.condition(|editor, _| editor.context_menu_visible())
17364 .await;
17365 cx.update_editor(|editor, _, _| {
17366 let context_menu = editor.context_menu.borrow_mut();
17367 let context_menu = context_menu
17368 .as_ref()
17369 .expect("Should have the context menu deployed");
17370 match context_menu {
17371 CodeContextMenu::Completions(completions_menu) => {
17372 let completions = completions_menu.completions.borrow_mut();
17373 assert_eq!(
17374 completions
17375 .iter()
17376 .map(|completion| &completion.label.text)
17377 .collect::<Vec<_>>(),
17378 vec!["method id()", "other"]
17379 )
17380 }
17381 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17382 }
17383 });
17384
17385 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17386 let item1 = item1.clone();
17387 move |_, item_to_resolve, _| {
17388 let item1 = item1.clone();
17389 async move {
17390 if item1 == item_to_resolve {
17391 Ok(lsp::CompletionItem {
17392 label: "method id()".to_string(),
17393 filter_text: Some("id".to_string()),
17394 detail: Some("Now resolved!".to_string()),
17395 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17396 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17397 range: lsp::Range::new(
17398 lsp::Position::new(0, 22),
17399 lsp::Position::new(0, 22),
17400 ),
17401 new_text: ".id".to_string(),
17402 })),
17403 ..lsp::CompletionItem::default()
17404 })
17405 } else {
17406 Ok(item_to_resolve)
17407 }
17408 }
17409 }
17410 })
17411 .next()
17412 .await
17413 .unwrap();
17414 cx.run_until_parked();
17415
17416 cx.update_editor(|editor, window, cx| {
17417 editor.context_menu_next(&Default::default(), window, cx);
17418 });
17419
17420 cx.update_editor(|editor, _, _| {
17421 let context_menu = editor.context_menu.borrow_mut();
17422 let context_menu = context_menu
17423 .as_ref()
17424 .expect("Should have the context menu deployed");
17425 match context_menu {
17426 CodeContextMenu::Completions(completions_menu) => {
17427 let completions = completions_menu.completions.borrow_mut();
17428 assert_eq!(
17429 completions
17430 .iter()
17431 .map(|completion| &completion.label.text)
17432 .collect::<Vec<_>>(),
17433 vec!["method id() Now resolved!", "other"],
17434 "Should update first completion label, but not second as the filter text did not match."
17435 );
17436 }
17437 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17438 }
17439 });
17440}
17441
17442#[gpui::test]
17443async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17444 init_test(cx, |_| {});
17445 let mut cx = EditorLspTestContext::new_rust(
17446 lsp::ServerCapabilities {
17447 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17448 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17449 completion_provider: Some(lsp::CompletionOptions {
17450 resolve_provider: Some(true),
17451 ..Default::default()
17452 }),
17453 ..Default::default()
17454 },
17455 cx,
17456 )
17457 .await;
17458 cx.set_state(indoc! {"
17459 struct TestStruct {
17460 field: i32
17461 }
17462
17463 fn mainˇ() {
17464 let unused_var = 42;
17465 let test_struct = TestStruct { field: 42 };
17466 }
17467 "});
17468 let symbol_range = cx.lsp_range(indoc! {"
17469 struct TestStruct {
17470 field: i32
17471 }
17472
17473 «fn main»() {
17474 let unused_var = 42;
17475 let test_struct = TestStruct { field: 42 };
17476 }
17477 "});
17478 let mut hover_requests =
17479 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17480 Ok(Some(lsp::Hover {
17481 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17482 kind: lsp::MarkupKind::Markdown,
17483 value: "Function documentation".to_string(),
17484 }),
17485 range: Some(symbol_range),
17486 }))
17487 });
17488
17489 // Case 1: Test that code action menu hide hover popover
17490 cx.dispatch_action(Hover);
17491 hover_requests.next().await;
17492 cx.condition(|editor, _| editor.hover_state.visible()).await;
17493 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17494 move |_, _, _| async move {
17495 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17496 lsp::CodeAction {
17497 title: "Remove unused variable".to_string(),
17498 kind: Some(CodeActionKind::QUICKFIX),
17499 edit: Some(lsp::WorkspaceEdit {
17500 changes: Some(
17501 [(
17502 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17503 vec![lsp::TextEdit {
17504 range: lsp::Range::new(
17505 lsp::Position::new(5, 4),
17506 lsp::Position::new(5, 27),
17507 ),
17508 new_text: "".to_string(),
17509 }],
17510 )]
17511 .into_iter()
17512 .collect(),
17513 ),
17514 ..Default::default()
17515 }),
17516 ..Default::default()
17517 },
17518 )]))
17519 },
17520 );
17521 cx.update_editor(|editor, window, cx| {
17522 editor.toggle_code_actions(
17523 &ToggleCodeActions {
17524 deployed_from: None,
17525 quick_launch: false,
17526 },
17527 window,
17528 cx,
17529 );
17530 });
17531 code_action_requests.next().await;
17532 cx.run_until_parked();
17533 cx.condition(|editor, _| editor.context_menu_visible())
17534 .await;
17535 cx.update_editor(|editor, _, _| {
17536 assert!(
17537 !editor.hover_state.visible(),
17538 "Hover popover should be hidden when code action menu is shown"
17539 );
17540 // Hide code actions
17541 editor.context_menu.take();
17542 });
17543
17544 // Case 2: Test that code completions hide hover popover
17545 cx.dispatch_action(Hover);
17546 hover_requests.next().await;
17547 cx.condition(|editor, _| editor.hover_state.visible()).await;
17548 let counter = Arc::new(AtomicUsize::new(0));
17549 let mut completion_requests =
17550 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17551 let counter = counter.clone();
17552 async move {
17553 counter.fetch_add(1, atomic::Ordering::Release);
17554 Ok(Some(lsp::CompletionResponse::Array(vec![
17555 lsp::CompletionItem {
17556 label: "main".into(),
17557 kind: Some(lsp::CompletionItemKind::FUNCTION),
17558 detail: Some("() -> ()".to_string()),
17559 ..Default::default()
17560 },
17561 lsp::CompletionItem {
17562 label: "TestStruct".into(),
17563 kind: Some(lsp::CompletionItemKind::STRUCT),
17564 detail: Some("struct TestStruct".to_string()),
17565 ..Default::default()
17566 },
17567 ])))
17568 }
17569 });
17570 cx.update_editor(|editor, window, cx| {
17571 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
17572 });
17573 completion_requests.next().await;
17574 cx.condition(|editor, _| editor.context_menu_visible())
17575 .await;
17576 cx.update_editor(|editor, _, _| {
17577 assert!(
17578 !editor.hover_state.visible(),
17579 "Hover popover should be hidden when completion menu is shown"
17580 );
17581 });
17582}
17583
17584#[gpui::test]
17585async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
17586 init_test(cx, |_| {});
17587
17588 let mut cx = EditorLspTestContext::new_rust(
17589 lsp::ServerCapabilities {
17590 completion_provider: Some(lsp::CompletionOptions {
17591 trigger_characters: Some(vec![".".to_string()]),
17592 resolve_provider: Some(true),
17593 ..Default::default()
17594 }),
17595 ..Default::default()
17596 },
17597 cx,
17598 )
17599 .await;
17600
17601 cx.set_state("fn main() { let a = 2ˇ; }");
17602 cx.simulate_keystroke(".");
17603
17604 let unresolved_item_1 = lsp::CompletionItem {
17605 label: "id".to_string(),
17606 filter_text: Some("id".to_string()),
17607 detail: None,
17608 documentation: None,
17609 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17610 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17611 new_text: ".id".to_string(),
17612 })),
17613 ..lsp::CompletionItem::default()
17614 };
17615 let resolved_item_1 = lsp::CompletionItem {
17616 additional_text_edits: Some(vec![lsp::TextEdit {
17617 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17618 new_text: "!!".to_string(),
17619 }]),
17620 ..unresolved_item_1.clone()
17621 };
17622 let unresolved_item_2 = lsp::CompletionItem {
17623 label: "other".to_string(),
17624 filter_text: Some("other".to_string()),
17625 detail: None,
17626 documentation: None,
17627 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17628 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17629 new_text: ".other".to_string(),
17630 })),
17631 ..lsp::CompletionItem::default()
17632 };
17633 let resolved_item_2 = lsp::CompletionItem {
17634 additional_text_edits: Some(vec![lsp::TextEdit {
17635 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
17636 new_text: "??".to_string(),
17637 }]),
17638 ..unresolved_item_2.clone()
17639 };
17640
17641 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
17642 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
17643 cx.lsp
17644 .server
17645 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17646 let unresolved_item_1 = unresolved_item_1.clone();
17647 let resolved_item_1 = resolved_item_1.clone();
17648 let unresolved_item_2 = unresolved_item_2.clone();
17649 let resolved_item_2 = resolved_item_2.clone();
17650 let resolve_requests_1 = resolve_requests_1.clone();
17651 let resolve_requests_2 = resolve_requests_2.clone();
17652 move |unresolved_request, _| {
17653 let unresolved_item_1 = unresolved_item_1.clone();
17654 let resolved_item_1 = resolved_item_1.clone();
17655 let unresolved_item_2 = unresolved_item_2.clone();
17656 let resolved_item_2 = resolved_item_2.clone();
17657 let resolve_requests_1 = resolve_requests_1.clone();
17658 let resolve_requests_2 = resolve_requests_2.clone();
17659 async move {
17660 if unresolved_request == unresolved_item_1 {
17661 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
17662 Ok(resolved_item_1.clone())
17663 } else if unresolved_request == unresolved_item_2 {
17664 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
17665 Ok(resolved_item_2.clone())
17666 } else {
17667 panic!("Unexpected completion item {unresolved_request:?}")
17668 }
17669 }
17670 }
17671 })
17672 .detach();
17673
17674 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17675 let unresolved_item_1 = unresolved_item_1.clone();
17676 let unresolved_item_2 = unresolved_item_2.clone();
17677 async move {
17678 Ok(Some(lsp::CompletionResponse::Array(vec![
17679 unresolved_item_1,
17680 unresolved_item_2,
17681 ])))
17682 }
17683 })
17684 .next()
17685 .await;
17686
17687 cx.condition(|editor, _| editor.context_menu_visible())
17688 .await;
17689 cx.update_editor(|editor, _, _| {
17690 let context_menu = editor.context_menu.borrow_mut();
17691 let context_menu = context_menu
17692 .as_ref()
17693 .expect("Should have the context menu deployed");
17694 match context_menu {
17695 CodeContextMenu::Completions(completions_menu) => {
17696 let completions = completions_menu.completions.borrow_mut();
17697 assert_eq!(
17698 completions
17699 .iter()
17700 .map(|completion| &completion.label.text)
17701 .collect::<Vec<_>>(),
17702 vec!["id", "other"]
17703 )
17704 }
17705 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17706 }
17707 });
17708 cx.run_until_parked();
17709
17710 cx.update_editor(|editor, window, cx| {
17711 editor.context_menu_next(&ContextMenuNext, window, cx);
17712 });
17713 cx.run_until_parked();
17714 cx.update_editor(|editor, window, cx| {
17715 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17716 });
17717 cx.run_until_parked();
17718 cx.update_editor(|editor, window, cx| {
17719 editor.context_menu_next(&ContextMenuNext, window, cx);
17720 });
17721 cx.run_until_parked();
17722 cx.update_editor(|editor, window, cx| {
17723 editor
17724 .compose_completion(&ComposeCompletion::default(), window, cx)
17725 .expect("No task returned")
17726 })
17727 .await
17728 .expect("Completion failed");
17729 cx.run_until_parked();
17730
17731 cx.update_editor(|editor, _, cx| {
17732 assert_eq!(
17733 resolve_requests_1.load(atomic::Ordering::Acquire),
17734 1,
17735 "Should always resolve once despite multiple selections"
17736 );
17737 assert_eq!(
17738 resolve_requests_2.load(atomic::Ordering::Acquire),
17739 1,
17740 "Should always resolve once after multiple selections and applying the completion"
17741 );
17742 assert_eq!(
17743 editor.text(cx),
17744 "fn main() { let a = ??.other; }",
17745 "Should use resolved data when applying the completion"
17746 );
17747 });
17748}
17749
17750#[gpui::test]
17751async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
17752 init_test(cx, |_| {});
17753
17754 let item_0 = lsp::CompletionItem {
17755 label: "abs".into(),
17756 insert_text: Some("abs".into()),
17757 data: Some(json!({ "very": "special"})),
17758 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
17759 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
17760 lsp::InsertReplaceEdit {
17761 new_text: "abs".to_string(),
17762 insert: lsp::Range::default(),
17763 replace: lsp::Range::default(),
17764 },
17765 )),
17766 ..lsp::CompletionItem::default()
17767 };
17768 let items = iter::once(item_0.clone())
17769 .chain((11..51).map(|i| lsp::CompletionItem {
17770 label: format!("item_{}", i),
17771 insert_text: Some(format!("item_{}", i)),
17772 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
17773 ..lsp::CompletionItem::default()
17774 }))
17775 .collect::<Vec<_>>();
17776
17777 let default_commit_characters = vec!["?".to_string()];
17778 let default_data = json!({ "default": "data"});
17779 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
17780 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
17781 let default_edit_range = lsp::Range {
17782 start: lsp::Position {
17783 line: 0,
17784 character: 5,
17785 },
17786 end: lsp::Position {
17787 line: 0,
17788 character: 5,
17789 },
17790 };
17791
17792 let mut cx = EditorLspTestContext::new_rust(
17793 lsp::ServerCapabilities {
17794 completion_provider: Some(lsp::CompletionOptions {
17795 trigger_characters: Some(vec![".".to_string()]),
17796 resolve_provider: Some(true),
17797 ..Default::default()
17798 }),
17799 ..Default::default()
17800 },
17801 cx,
17802 )
17803 .await;
17804
17805 cx.set_state("fn main() { let a = 2ˇ; }");
17806 cx.simulate_keystroke(".");
17807
17808 let completion_data = default_data.clone();
17809 let completion_characters = default_commit_characters.clone();
17810 let completion_items = items.clone();
17811 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17812 let default_data = completion_data.clone();
17813 let default_commit_characters = completion_characters.clone();
17814 let items = completion_items.clone();
17815 async move {
17816 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
17817 items,
17818 item_defaults: Some(lsp::CompletionListItemDefaults {
17819 data: Some(default_data.clone()),
17820 commit_characters: Some(default_commit_characters.clone()),
17821 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
17822 default_edit_range,
17823 )),
17824 insert_text_format: Some(default_insert_text_format),
17825 insert_text_mode: Some(default_insert_text_mode),
17826 }),
17827 ..lsp::CompletionList::default()
17828 })))
17829 }
17830 })
17831 .next()
17832 .await;
17833
17834 let resolved_items = Arc::new(Mutex::new(Vec::new()));
17835 cx.lsp
17836 .server
17837 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
17838 let closure_resolved_items = resolved_items.clone();
17839 move |item_to_resolve, _| {
17840 let closure_resolved_items = closure_resolved_items.clone();
17841 async move {
17842 closure_resolved_items.lock().push(item_to_resolve.clone());
17843 Ok(item_to_resolve)
17844 }
17845 }
17846 })
17847 .detach();
17848
17849 cx.condition(|editor, _| editor.context_menu_visible())
17850 .await;
17851 cx.run_until_parked();
17852 cx.update_editor(|editor, _, _| {
17853 let menu = editor.context_menu.borrow_mut();
17854 match menu.as_ref().expect("should have the completions menu") {
17855 CodeContextMenu::Completions(completions_menu) => {
17856 assert_eq!(
17857 completions_menu
17858 .entries
17859 .borrow()
17860 .iter()
17861 .map(|mat| mat.string.clone())
17862 .collect::<Vec<String>>(),
17863 items
17864 .iter()
17865 .map(|completion| completion.label.clone())
17866 .collect::<Vec<String>>()
17867 );
17868 }
17869 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
17870 }
17871 });
17872 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
17873 // with 4 from the end.
17874 assert_eq!(
17875 *resolved_items.lock(),
17876 [&items[0..16], &items[items.len() - 4..items.len()]]
17877 .concat()
17878 .iter()
17879 .cloned()
17880 .map(|mut item| {
17881 if item.data.is_none() {
17882 item.data = Some(default_data.clone());
17883 }
17884 item
17885 })
17886 .collect::<Vec<lsp::CompletionItem>>(),
17887 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
17888 );
17889 resolved_items.lock().clear();
17890
17891 cx.update_editor(|editor, window, cx| {
17892 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
17893 });
17894 cx.run_until_parked();
17895 // Completions that have already been resolved are skipped.
17896 assert_eq!(
17897 *resolved_items.lock(),
17898 items[items.len() - 17..items.len() - 4]
17899 .iter()
17900 .cloned()
17901 .map(|mut item| {
17902 if item.data.is_none() {
17903 item.data = Some(default_data.clone());
17904 }
17905 item
17906 })
17907 .collect::<Vec<lsp::CompletionItem>>()
17908 );
17909 resolved_items.lock().clear();
17910}
17911
17912#[gpui::test]
17913async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
17914 init_test(cx, |_| {});
17915
17916 let mut cx = EditorLspTestContext::new(
17917 Language::new(
17918 LanguageConfig {
17919 matcher: LanguageMatcher {
17920 path_suffixes: vec!["jsx".into()],
17921 ..Default::default()
17922 },
17923 overrides: [(
17924 "element".into(),
17925 LanguageConfigOverride {
17926 completion_query_characters: Override::Set(['-'].into_iter().collect()),
17927 ..Default::default()
17928 },
17929 )]
17930 .into_iter()
17931 .collect(),
17932 ..Default::default()
17933 },
17934 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
17935 )
17936 .with_override_query("(jsx_self_closing_element) @element")
17937 .unwrap(),
17938 lsp::ServerCapabilities {
17939 completion_provider: Some(lsp::CompletionOptions {
17940 trigger_characters: Some(vec![":".to_string()]),
17941 ..Default::default()
17942 }),
17943 ..Default::default()
17944 },
17945 cx,
17946 )
17947 .await;
17948
17949 cx.lsp
17950 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
17951 Ok(Some(lsp::CompletionResponse::Array(vec![
17952 lsp::CompletionItem {
17953 label: "bg-blue".into(),
17954 ..Default::default()
17955 },
17956 lsp::CompletionItem {
17957 label: "bg-red".into(),
17958 ..Default::default()
17959 },
17960 lsp::CompletionItem {
17961 label: "bg-yellow".into(),
17962 ..Default::default()
17963 },
17964 ])))
17965 });
17966
17967 cx.set_state(r#"<p class="bgˇ" />"#);
17968
17969 // Trigger completion when typing a dash, because the dash is an extra
17970 // word character in the 'element' scope, which contains the cursor.
17971 cx.simulate_keystroke("-");
17972 cx.executor().run_until_parked();
17973 cx.update_editor(|editor, _, _| {
17974 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17975 {
17976 assert_eq!(
17977 completion_menu_entries(menu),
17978 &["bg-blue", "bg-red", "bg-yellow"]
17979 );
17980 } else {
17981 panic!("expected completion menu to be open");
17982 }
17983 });
17984
17985 cx.simulate_keystroke("l");
17986 cx.executor().run_until_parked();
17987 cx.update_editor(|editor, _, _| {
17988 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
17989 {
17990 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
17991 } else {
17992 panic!("expected completion menu to be open");
17993 }
17994 });
17995
17996 // When filtering completions, consider the character after the '-' to
17997 // be the start of a subword.
17998 cx.set_state(r#"<p class="yelˇ" />"#);
17999 cx.simulate_keystroke("l");
18000 cx.executor().run_until_parked();
18001 cx.update_editor(|editor, _, _| {
18002 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18003 {
18004 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18005 } else {
18006 panic!("expected completion menu to be open");
18007 }
18008 });
18009}
18010
18011fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18012 let entries = menu.entries.borrow();
18013 entries.iter().map(|mat| mat.string.clone()).collect()
18014}
18015
18016#[gpui::test]
18017async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18018 init_test(cx, |settings| {
18019 settings.defaults.formatter = Some(SelectedFormatter::List(FormatterList::Single(
18020 Formatter::Prettier,
18021 )))
18022 });
18023
18024 let fs = FakeFs::new(cx.executor());
18025 fs.insert_file(path!("/file.ts"), Default::default()).await;
18026
18027 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18028 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18029
18030 language_registry.add(Arc::new(Language::new(
18031 LanguageConfig {
18032 name: "TypeScript".into(),
18033 matcher: LanguageMatcher {
18034 path_suffixes: vec!["ts".to_string()],
18035 ..Default::default()
18036 },
18037 ..Default::default()
18038 },
18039 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18040 )));
18041 update_test_language_settings(cx, |settings| {
18042 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18043 });
18044
18045 let test_plugin = "test_plugin";
18046 let _ = language_registry.register_fake_lsp(
18047 "TypeScript",
18048 FakeLspAdapter {
18049 prettier_plugins: vec![test_plugin],
18050 ..Default::default()
18051 },
18052 );
18053
18054 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18055 let buffer = project
18056 .update(cx, |project, cx| {
18057 project.open_local_buffer(path!("/file.ts"), cx)
18058 })
18059 .await
18060 .unwrap();
18061
18062 let buffer_text = "one\ntwo\nthree\n";
18063 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18064 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18065 editor.update_in(cx, |editor, window, cx| {
18066 editor.set_text(buffer_text, window, cx)
18067 });
18068
18069 editor
18070 .update_in(cx, |editor, window, cx| {
18071 editor.perform_format(
18072 project.clone(),
18073 FormatTrigger::Manual,
18074 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18075 window,
18076 cx,
18077 )
18078 })
18079 .unwrap()
18080 .await;
18081 assert_eq!(
18082 editor.update(cx, |editor, cx| editor.text(cx)),
18083 buffer_text.to_string() + prettier_format_suffix,
18084 "Test prettier formatting was not applied to the original buffer text",
18085 );
18086
18087 update_test_language_settings(cx, |settings| {
18088 settings.defaults.formatter = Some(SelectedFormatter::Auto)
18089 });
18090 let format = editor.update_in(cx, |editor, window, cx| {
18091 editor.perform_format(
18092 project.clone(),
18093 FormatTrigger::Manual,
18094 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18095 window,
18096 cx,
18097 )
18098 });
18099 format.await.unwrap();
18100 assert_eq!(
18101 editor.update(cx, |editor, cx| editor.text(cx)),
18102 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18103 "Autoformatting (via test prettier) was not applied to the original buffer text",
18104 );
18105}
18106
18107#[gpui::test]
18108async fn test_addition_reverts(cx: &mut TestAppContext) {
18109 init_test(cx, |_| {});
18110 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18111 let base_text = indoc! {r#"
18112 struct Row;
18113 struct Row1;
18114 struct Row2;
18115
18116 struct Row4;
18117 struct Row5;
18118 struct Row6;
18119
18120 struct Row8;
18121 struct Row9;
18122 struct Row10;"#};
18123
18124 // When addition hunks are not adjacent to carets, no hunk revert is performed
18125 assert_hunk_revert(
18126 indoc! {r#"struct Row;
18127 struct Row1;
18128 struct Row1.1;
18129 struct Row1.2;
18130 struct Row2;ˇ
18131
18132 struct Row4;
18133 struct Row5;
18134 struct Row6;
18135
18136 struct Row8;
18137 ˇstruct Row9;
18138 struct Row9.1;
18139 struct Row9.2;
18140 struct Row9.3;
18141 struct Row10;"#},
18142 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18143 indoc! {r#"struct Row;
18144 struct Row1;
18145 struct Row1.1;
18146 struct Row1.2;
18147 struct Row2;ˇ
18148
18149 struct Row4;
18150 struct Row5;
18151 struct Row6;
18152
18153 struct Row8;
18154 ˇstruct Row9;
18155 struct Row9.1;
18156 struct Row9.2;
18157 struct Row9.3;
18158 struct Row10;"#},
18159 base_text,
18160 &mut cx,
18161 );
18162 // Same for selections
18163 assert_hunk_revert(
18164 indoc! {r#"struct Row;
18165 struct Row1;
18166 struct Row2;
18167 struct Row2.1;
18168 struct Row2.2;
18169 «ˇ
18170 struct Row4;
18171 struct» Row5;
18172 «struct Row6;
18173 ˇ»
18174 struct Row9.1;
18175 struct Row9.2;
18176 struct Row9.3;
18177 struct Row8;
18178 struct Row9;
18179 struct Row10;"#},
18180 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18181 indoc! {r#"struct Row;
18182 struct Row1;
18183 struct Row2;
18184 struct Row2.1;
18185 struct Row2.2;
18186 «ˇ
18187 struct Row4;
18188 struct» Row5;
18189 «struct Row6;
18190 ˇ»
18191 struct Row9.1;
18192 struct Row9.2;
18193 struct Row9.3;
18194 struct Row8;
18195 struct Row9;
18196 struct Row10;"#},
18197 base_text,
18198 &mut cx,
18199 );
18200
18201 // When carets and selections intersect the addition hunks, those are reverted.
18202 // Adjacent carets got merged.
18203 assert_hunk_revert(
18204 indoc! {r#"struct Row;
18205 ˇ// something on the top
18206 struct Row1;
18207 struct Row2;
18208 struct Roˇw3.1;
18209 struct Row2.2;
18210 struct Row2.3;ˇ
18211
18212 struct Row4;
18213 struct ˇRow5.1;
18214 struct Row5.2;
18215 struct «Rowˇ»5.3;
18216 struct Row5;
18217 struct Row6;
18218 ˇ
18219 struct Row9.1;
18220 struct «Rowˇ»9.2;
18221 struct «ˇRow»9.3;
18222 struct Row8;
18223 struct Row9;
18224 «ˇ// something on bottom»
18225 struct Row10;"#},
18226 vec![
18227 DiffHunkStatusKind::Added,
18228 DiffHunkStatusKind::Added,
18229 DiffHunkStatusKind::Added,
18230 DiffHunkStatusKind::Added,
18231 DiffHunkStatusKind::Added,
18232 ],
18233 indoc! {r#"struct Row;
18234 ˇstruct Row1;
18235 struct Row2;
18236 ˇ
18237 struct Row4;
18238 ˇstruct Row5;
18239 struct Row6;
18240 ˇ
18241 ˇstruct Row8;
18242 struct Row9;
18243 ˇstruct Row10;"#},
18244 base_text,
18245 &mut cx,
18246 );
18247}
18248
18249#[gpui::test]
18250async fn test_modification_reverts(cx: &mut TestAppContext) {
18251 init_test(cx, |_| {});
18252 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18253 let base_text = indoc! {r#"
18254 struct Row;
18255 struct Row1;
18256 struct Row2;
18257
18258 struct Row4;
18259 struct Row5;
18260 struct Row6;
18261
18262 struct Row8;
18263 struct Row9;
18264 struct Row10;"#};
18265
18266 // Modification hunks behave the same as the addition ones.
18267 assert_hunk_revert(
18268 indoc! {r#"struct Row;
18269 struct Row1;
18270 struct Row33;
18271 ˇ
18272 struct Row4;
18273 struct Row5;
18274 struct Row6;
18275 ˇ
18276 struct Row99;
18277 struct Row9;
18278 struct Row10;"#},
18279 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18280 indoc! {r#"struct Row;
18281 struct Row1;
18282 struct Row33;
18283 ˇ
18284 struct Row4;
18285 struct Row5;
18286 struct Row6;
18287 ˇ
18288 struct Row99;
18289 struct Row9;
18290 struct Row10;"#},
18291 base_text,
18292 &mut cx,
18293 );
18294 assert_hunk_revert(
18295 indoc! {r#"struct Row;
18296 struct Row1;
18297 struct Row33;
18298 «ˇ
18299 struct Row4;
18300 struct» Row5;
18301 «struct Row6;
18302 ˇ»
18303 struct Row99;
18304 struct Row9;
18305 struct Row10;"#},
18306 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18307 indoc! {r#"struct Row;
18308 struct Row1;
18309 struct Row33;
18310 «ˇ
18311 struct Row4;
18312 struct» Row5;
18313 «struct Row6;
18314 ˇ»
18315 struct Row99;
18316 struct Row9;
18317 struct Row10;"#},
18318 base_text,
18319 &mut cx,
18320 );
18321
18322 assert_hunk_revert(
18323 indoc! {r#"ˇstruct Row1.1;
18324 struct Row1;
18325 «ˇstr»uct Row22;
18326
18327 struct ˇRow44;
18328 struct Row5;
18329 struct «Rˇ»ow66;ˇ
18330
18331 «struˇ»ct Row88;
18332 struct Row9;
18333 struct Row1011;ˇ"#},
18334 vec![
18335 DiffHunkStatusKind::Modified,
18336 DiffHunkStatusKind::Modified,
18337 DiffHunkStatusKind::Modified,
18338 DiffHunkStatusKind::Modified,
18339 DiffHunkStatusKind::Modified,
18340 DiffHunkStatusKind::Modified,
18341 ],
18342 indoc! {r#"struct Row;
18343 ˇstruct Row1;
18344 struct Row2;
18345 ˇ
18346 struct Row4;
18347 ˇstruct Row5;
18348 struct Row6;
18349 ˇ
18350 struct Row8;
18351 ˇstruct Row9;
18352 struct Row10;ˇ"#},
18353 base_text,
18354 &mut cx,
18355 );
18356}
18357
18358#[gpui::test]
18359async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18360 init_test(cx, |_| {});
18361 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18362 let base_text = indoc! {r#"
18363 one
18364
18365 two
18366 three
18367 "#};
18368
18369 cx.set_head_text(base_text);
18370 cx.set_state("\nˇ\n");
18371 cx.executor().run_until_parked();
18372 cx.update_editor(|editor, _window, cx| {
18373 editor.expand_selected_diff_hunks(cx);
18374 });
18375 cx.executor().run_until_parked();
18376 cx.update_editor(|editor, window, cx| {
18377 editor.backspace(&Default::default(), window, cx);
18378 });
18379 cx.run_until_parked();
18380 cx.assert_state_with_diff(
18381 indoc! {r#"
18382
18383 - two
18384 - threeˇ
18385 +
18386 "#}
18387 .to_string(),
18388 );
18389}
18390
18391#[gpui::test]
18392async fn test_deletion_reverts(cx: &mut TestAppContext) {
18393 init_test(cx, |_| {});
18394 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18395 let base_text = indoc! {r#"struct Row;
18396struct Row1;
18397struct Row2;
18398
18399struct Row4;
18400struct Row5;
18401struct Row6;
18402
18403struct Row8;
18404struct Row9;
18405struct Row10;"#};
18406
18407 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18408 assert_hunk_revert(
18409 indoc! {r#"struct Row;
18410 struct Row2;
18411
18412 ˇstruct Row4;
18413 struct Row5;
18414 struct Row6;
18415 ˇ
18416 struct Row8;
18417 struct Row10;"#},
18418 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18419 indoc! {r#"struct Row;
18420 struct Row2;
18421
18422 ˇstruct Row4;
18423 struct Row5;
18424 struct Row6;
18425 ˇ
18426 struct Row8;
18427 struct Row10;"#},
18428 base_text,
18429 &mut cx,
18430 );
18431 assert_hunk_revert(
18432 indoc! {r#"struct Row;
18433 struct Row2;
18434
18435 «ˇstruct Row4;
18436 struct» Row5;
18437 «struct Row6;
18438 ˇ»
18439 struct Row8;
18440 struct Row10;"#},
18441 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18442 indoc! {r#"struct Row;
18443 struct Row2;
18444
18445 «ˇstruct Row4;
18446 struct» Row5;
18447 «struct Row6;
18448 ˇ»
18449 struct Row8;
18450 struct Row10;"#},
18451 base_text,
18452 &mut cx,
18453 );
18454
18455 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18456 assert_hunk_revert(
18457 indoc! {r#"struct Row;
18458 ˇstruct Row2;
18459
18460 struct Row4;
18461 struct Row5;
18462 struct Row6;
18463
18464 struct Row8;ˇ
18465 struct Row10;"#},
18466 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18467 indoc! {r#"struct Row;
18468 struct Row1;
18469 ˇstruct Row2;
18470
18471 struct Row4;
18472 struct Row5;
18473 struct Row6;
18474
18475 struct Row8;ˇ
18476 struct Row9;
18477 struct Row10;"#},
18478 base_text,
18479 &mut cx,
18480 );
18481 assert_hunk_revert(
18482 indoc! {r#"struct Row;
18483 struct Row2«ˇ;
18484 struct Row4;
18485 struct» Row5;
18486 «struct Row6;
18487
18488 struct Row8;ˇ»
18489 struct Row10;"#},
18490 vec![
18491 DiffHunkStatusKind::Deleted,
18492 DiffHunkStatusKind::Deleted,
18493 DiffHunkStatusKind::Deleted,
18494 ],
18495 indoc! {r#"struct Row;
18496 struct Row1;
18497 struct Row2«ˇ;
18498
18499 struct Row4;
18500 struct» Row5;
18501 «struct Row6;
18502
18503 struct Row8;ˇ»
18504 struct Row9;
18505 struct Row10;"#},
18506 base_text,
18507 &mut cx,
18508 );
18509}
18510
18511#[gpui::test]
18512async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18513 init_test(cx, |_| {});
18514
18515 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18516 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18517 let base_text_3 =
18518 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18519
18520 let text_1 = edit_first_char_of_every_line(base_text_1);
18521 let text_2 = edit_first_char_of_every_line(base_text_2);
18522 let text_3 = edit_first_char_of_every_line(base_text_3);
18523
18524 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18525 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18526 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18527
18528 let multibuffer = cx.new(|cx| {
18529 let mut multibuffer = MultiBuffer::new(ReadWrite);
18530 multibuffer.push_excerpts(
18531 buffer_1.clone(),
18532 [
18533 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18534 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18535 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18536 ],
18537 cx,
18538 );
18539 multibuffer.push_excerpts(
18540 buffer_2.clone(),
18541 [
18542 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18543 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18544 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18545 ],
18546 cx,
18547 );
18548 multibuffer.push_excerpts(
18549 buffer_3.clone(),
18550 [
18551 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18552 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18553 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18554 ],
18555 cx,
18556 );
18557 multibuffer
18558 });
18559
18560 let fs = FakeFs::new(cx.executor());
18561 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
18562 let (editor, cx) = cx
18563 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
18564 editor.update_in(cx, |editor, _window, cx| {
18565 for (buffer, diff_base) in [
18566 (buffer_1.clone(), base_text_1),
18567 (buffer_2.clone(), base_text_2),
18568 (buffer_3.clone(), base_text_3),
18569 ] {
18570 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
18571 editor
18572 .buffer
18573 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
18574 }
18575 });
18576 cx.executor().run_until_parked();
18577
18578 editor.update_in(cx, |editor, window, cx| {
18579 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}");
18580 editor.select_all(&SelectAll, window, cx);
18581 editor.git_restore(&Default::default(), window, cx);
18582 });
18583 cx.executor().run_until_parked();
18584
18585 // When all ranges are selected, all buffer hunks are reverted.
18586 editor.update(cx, |editor, cx| {
18587 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");
18588 });
18589 buffer_1.update(cx, |buffer, _| {
18590 assert_eq!(buffer.text(), base_text_1);
18591 });
18592 buffer_2.update(cx, |buffer, _| {
18593 assert_eq!(buffer.text(), base_text_2);
18594 });
18595 buffer_3.update(cx, |buffer, _| {
18596 assert_eq!(buffer.text(), base_text_3);
18597 });
18598
18599 editor.update_in(cx, |editor, window, cx| {
18600 editor.undo(&Default::default(), window, cx);
18601 });
18602
18603 editor.update_in(cx, |editor, window, cx| {
18604 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
18605 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
18606 });
18607 editor.git_restore(&Default::default(), window, cx);
18608 });
18609
18610 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
18611 // but not affect buffer_2 and its related excerpts.
18612 editor.update(cx, |editor, cx| {
18613 assert_eq!(
18614 editor.text(cx),
18615 "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}"
18616 );
18617 });
18618 buffer_1.update(cx, |buffer, _| {
18619 assert_eq!(buffer.text(), base_text_1);
18620 });
18621 buffer_2.update(cx, |buffer, _| {
18622 assert_eq!(
18623 buffer.text(),
18624 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
18625 );
18626 });
18627 buffer_3.update(cx, |buffer, _| {
18628 assert_eq!(
18629 buffer.text(),
18630 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
18631 );
18632 });
18633
18634 fn edit_first_char_of_every_line(text: &str) -> String {
18635 text.split('\n')
18636 .map(|line| format!("X{}", &line[1..]))
18637 .collect::<Vec<_>>()
18638 .join("\n")
18639 }
18640}
18641
18642#[gpui::test]
18643async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
18644 init_test(cx, |_| {});
18645
18646 let cols = 4;
18647 let rows = 10;
18648 let sample_text_1 = sample_text(rows, cols, 'a');
18649 assert_eq!(
18650 sample_text_1,
18651 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
18652 );
18653 let sample_text_2 = sample_text(rows, cols, 'l');
18654 assert_eq!(
18655 sample_text_2,
18656 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
18657 );
18658 let sample_text_3 = sample_text(rows, cols, 'v');
18659 assert_eq!(
18660 sample_text_3,
18661 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
18662 );
18663
18664 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
18665 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
18666 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
18667
18668 let multi_buffer = cx.new(|cx| {
18669 let mut multibuffer = MultiBuffer::new(ReadWrite);
18670 multibuffer.push_excerpts(
18671 buffer_1.clone(),
18672 [
18673 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18674 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18675 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18676 ],
18677 cx,
18678 );
18679 multibuffer.push_excerpts(
18680 buffer_2.clone(),
18681 [
18682 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18683 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18684 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18685 ],
18686 cx,
18687 );
18688 multibuffer.push_excerpts(
18689 buffer_3.clone(),
18690 [
18691 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18692 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18693 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18694 ],
18695 cx,
18696 );
18697 multibuffer
18698 });
18699
18700 let fs = FakeFs::new(cx.executor());
18701 fs.insert_tree(
18702 "/a",
18703 json!({
18704 "main.rs": sample_text_1,
18705 "other.rs": sample_text_2,
18706 "lib.rs": sample_text_3,
18707 }),
18708 )
18709 .await;
18710 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18711 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18712 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18713 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18714 Editor::new(
18715 EditorMode::full(),
18716 multi_buffer,
18717 Some(project.clone()),
18718 window,
18719 cx,
18720 )
18721 });
18722 let multibuffer_item_id = workspace
18723 .update(cx, |workspace, window, cx| {
18724 assert!(
18725 workspace.active_item(cx).is_none(),
18726 "active item should be None before the first item is added"
18727 );
18728 workspace.add_item_to_active_pane(
18729 Box::new(multi_buffer_editor.clone()),
18730 None,
18731 true,
18732 window,
18733 cx,
18734 );
18735 let active_item = workspace
18736 .active_item(cx)
18737 .expect("should have an active item after adding the multi buffer");
18738 assert!(
18739 !active_item.is_singleton(cx),
18740 "A multi buffer was expected to active after adding"
18741 );
18742 active_item.item_id()
18743 })
18744 .unwrap();
18745 cx.executor().run_until_parked();
18746
18747 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18748 editor.change_selections(
18749 SelectionEffects::scroll(Autoscroll::Next),
18750 window,
18751 cx,
18752 |s| s.select_ranges(Some(1..2)),
18753 );
18754 editor.open_excerpts(&OpenExcerpts, window, cx);
18755 });
18756 cx.executor().run_until_parked();
18757 let first_item_id = workspace
18758 .update(cx, |workspace, window, cx| {
18759 let active_item = workspace
18760 .active_item(cx)
18761 .expect("should have an active item after navigating into the 1st buffer");
18762 let first_item_id = active_item.item_id();
18763 assert_ne!(
18764 first_item_id, multibuffer_item_id,
18765 "Should navigate into the 1st buffer and activate it"
18766 );
18767 assert!(
18768 active_item.is_singleton(cx),
18769 "New active item should be a singleton buffer"
18770 );
18771 assert_eq!(
18772 active_item
18773 .act_as::<Editor>(cx)
18774 .expect("should have navigated into an editor for the 1st buffer")
18775 .read(cx)
18776 .text(cx),
18777 sample_text_1
18778 );
18779
18780 workspace
18781 .go_back(workspace.active_pane().downgrade(), window, cx)
18782 .detach_and_log_err(cx);
18783
18784 first_item_id
18785 })
18786 .unwrap();
18787 cx.executor().run_until_parked();
18788 workspace
18789 .update(cx, |workspace, _, cx| {
18790 let active_item = workspace
18791 .active_item(cx)
18792 .expect("should have an active item after navigating back");
18793 assert_eq!(
18794 active_item.item_id(),
18795 multibuffer_item_id,
18796 "Should navigate back to the multi buffer"
18797 );
18798 assert!(!active_item.is_singleton(cx));
18799 })
18800 .unwrap();
18801
18802 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18803 editor.change_selections(
18804 SelectionEffects::scroll(Autoscroll::Next),
18805 window,
18806 cx,
18807 |s| s.select_ranges(Some(39..40)),
18808 );
18809 editor.open_excerpts(&OpenExcerpts, window, cx);
18810 });
18811 cx.executor().run_until_parked();
18812 let second_item_id = workspace
18813 .update(cx, |workspace, window, cx| {
18814 let active_item = workspace
18815 .active_item(cx)
18816 .expect("should have an active item after navigating into the 2nd buffer");
18817 let second_item_id = active_item.item_id();
18818 assert_ne!(
18819 second_item_id, multibuffer_item_id,
18820 "Should navigate away from the multibuffer"
18821 );
18822 assert_ne!(
18823 second_item_id, first_item_id,
18824 "Should navigate into the 2nd buffer and activate it"
18825 );
18826 assert!(
18827 active_item.is_singleton(cx),
18828 "New active item should be a singleton buffer"
18829 );
18830 assert_eq!(
18831 active_item
18832 .act_as::<Editor>(cx)
18833 .expect("should have navigated into an editor")
18834 .read(cx)
18835 .text(cx),
18836 sample_text_2
18837 );
18838
18839 workspace
18840 .go_back(workspace.active_pane().downgrade(), window, cx)
18841 .detach_and_log_err(cx);
18842
18843 second_item_id
18844 })
18845 .unwrap();
18846 cx.executor().run_until_parked();
18847 workspace
18848 .update(cx, |workspace, _, cx| {
18849 let active_item = workspace
18850 .active_item(cx)
18851 .expect("should have an active item after navigating back from the 2nd buffer");
18852 assert_eq!(
18853 active_item.item_id(),
18854 multibuffer_item_id,
18855 "Should navigate back from the 2nd buffer to the multi buffer"
18856 );
18857 assert!(!active_item.is_singleton(cx));
18858 })
18859 .unwrap();
18860
18861 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18862 editor.change_selections(
18863 SelectionEffects::scroll(Autoscroll::Next),
18864 window,
18865 cx,
18866 |s| s.select_ranges(Some(70..70)),
18867 );
18868 editor.open_excerpts(&OpenExcerpts, window, cx);
18869 });
18870 cx.executor().run_until_parked();
18871 workspace
18872 .update(cx, |workspace, window, cx| {
18873 let active_item = workspace
18874 .active_item(cx)
18875 .expect("should have an active item after navigating into the 3rd buffer");
18876 let third_item_id = active_item.item_id();
18877 assert_ne!(
18878 third_item_id, multibuffer_item_id,
18879 "Should navigate into the 3rd buffer and activate it"
18880 );
18881 assert_ne!(third_item_id, first_item_id);
18882 assert_ne!(third_item_id, second_item_id);
18883 assert!(
18884 active_item.is_singleton(cx),
18885 "New active item should be a singleton buffer"
18886 );
18887 assert_eq!(
18888 active_item
18889 .act_as::<Editor>(cx)
18890 .expect("should have navigated into an editor")
18891 .read(cx)
18892 .text(cx),
18893 sample_text_3
18894 );
18895
18896 workspace
18897 .go_back(workspace.active_pane().downgrade(), window, cx)
18898 .detach_and_log_err(cx);
18899 })
18900 .unwrap();
18901 cx.executor().run_until_parked();
18902 workspace
18903 .update(cx, |workspace, _, cx| {
18904 let active_item = workspace
18905 .active_item(cx)
18906 .expect("should have an active item after navigating back from the 3rd buffer");
18907 assert_eq!(
18908 active_item.item_id(),
18909 multibuffer_item_id,
18910 "Should navigate back from the 3rd buffer to the multi buffer"
18911 );
18912 assert!(!active_item.is_singleton(cx));
18913 })
18914 .unwrap();
18915}
18916
18917#[gpui::test]
18918async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
18919 init_test(cx, |_| {});
18920
18921 let mut cx = EditorTestContext::new(cx).await;
18922
18923 let diff_base = r#"
18924 use some::mod;
18925
18926 const A: u32 = 42;
18927
18928 fn main() {
18929 println!("hello");
18930
18931 println!("world");
18932 }
18933 "#
18934 .unindent();
18935
18936 cx.set_state(
18937 &r#"
18938 use some::modified;
18939
18940 ˇ
18941 fn main() {
18942 println!("hello there");
18943
18944 println!("around the");
18945 println!("world");
18946 }
18947 "#
18948 .unindent(),
18949 );
18950
18951 cx.set_head_text(&diff_base);
18952 executor.run_until_parked();
18953
18954 cx.update_editor(|editor, window, cx| {
18955 editor.go_to_next_hunk(&GoToHunk, window, cx);
18956 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18957 });
18958 executor.run_until_parked();
18959 cx.assert_state_with_diff(
18960 r#"
18961 use some::modified;
18962
18963
18964 fn main() {
18965 - println!("hello");
18966 + ˇ println!("hello there");
18967
18968 println!("around the");
18969 println!("world");
18970 }
18971 "#
18972 .unindent(),
18973 );
18974
18975 cx.update_editor(|editor, window, cx| {
18976 for _ in 0..2 {
18977 editor.go_to_next_hunk(&GoToHunk, window, cx);
18978 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
18979 }
18980 });
18981 executor.run_until_parked();
18982 cx.assert_state_with_diff(
18983 r#"
18984 - use some::mod;
18985 + ˇuse some::modified;
18986
18987
18988 fn main() {
18989 - println!("hello");
18990 + println!("hello there");
18991
18992 + println!("around the");
18993 println!("world");
18994 }
18995 "#
18996 .unindent(),
18997 );
18998
18999 cx.update_editor(|editor, window, cx| {
19000 editor.go_to_next_hunk(&GoToHunk, window, cx);
19001 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19002 });
19003 executor.run_until_parked();
19004 cx.assert_state_with_diff(
19005 r#"
19006 - use some::mod;
19007 + use some::modified;
19008
19009 - const A: u32 = 42;
19010 ˇ
19011 fn main() {
19012 - println!("hello");
19013 + println!("hello there");
19014
19015 + println!("around the");
19016 println!("world");
19017 }
19018 "#
19019 .unindent(),
19020 );
19021
19022 cx.update_editor(|editor, window, cx| {
19023 editor.cancel(&Cancel, window, cx);
19024 });
19025
19026 cx.assert_state_with_diff(
19027 r#"
19028 use some::modified;
19029
19030 ˇ
19031 fn main() {
19032 println!("hello there");
19033
19034 println!("around the");
19035 println!("world");
19036 }
19037 "#
19038 .unindent(),
19039 );
19040}
19041
19042#[gpui::test]
19043async fn test_diff_base_change_with_expanded_diff_hunks(
19044 executor: BackgroundExecutor,
19045 cx: &mut TestAppContext,
19046) {
19047 init_test(cx, |_| {});
19048
19049 let mut cx = EditorTestContext::new(cx).await;
19050
19051 let diff_base = r#"
19052 use some::mod1;
19053 use some::mod2;
19054
19055 const A: u32 = 42;
19056 const B: u32 = 42;
19057 const C: u32 = 42;
19058
19059 fn main() {
19060 println!("hello");
19061
19062 println!("world");
19063 }
19064 "#
19065 .unindent();
19066
19067 cx.set_state(
19068 &r#"
19069 use some::mod2;
19070
19071 const A: u32 = 42;
19072 const C: u32 = 42;
19073
19074 fn main(ˇ) {
19075 //println!("hello");
19076
19077 println!("world");
19078 //
19079 //
19080 }
19081 "#
19082 .unindent(),
19083 );
19084
19085 cx.set_head_text(&diff_base);
19086 executor.run_until_parked();
19087
19088 cx.update_editor(|editor, window, cx| {
19089 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19090 });
19091 executor.run_until_parked();
19092 cx.assert_state_with_diff(
19093 r#"
19094 - use some::mod1;
19095 use some::mod2;
19096
19097 const A: u32 = 42;
19098 - const B: u32 = 42;
19099 const C: u32 = 42;
19100
19101 fn main(ˇ) {
19102 - println!("hello");
19103 + //println!("hello");
19104
19105 println!("world");
19106 + //
19107 + //
19108 }
19109 "#
19110 .unindent(),
19111 );
19112
19113 cx.set_head_text("new diff base!");
19114 executor.run_until_parked();
19115 cx.assert_state_with_diff(
19116 r#"
19117 - new diff base!
19118 + use some::mod2;
19119 +
19120 + const A: u32 = 42;
19121 + const C: u32 = 42;
19122 +
19123 + fn main(ˇ) {
19124 + //println!("hello");
19125 +
19126 + println!("world");
19127 + //
19128 + //
19129 + }
19130 "#
19131 .unindent(),
19132 );
19133}
19134
19135#[gpui::test]
19136async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19137 init_test(cx, |_| {});
19138
19139 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19140 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19141 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19142 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19143 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19144 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19145
19146 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19147 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19148 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19149
19150 let multi_buffer = cx.new(|cx| {
19151 let mut multibuffer = MultiBuffer::new(ReadWrite);
19152 multibuffer.push_excerpts(
19153 buffer_1.clone(),
19154 [
19155 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19156 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19157 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19158 ],
19159 cx,
19160 );
19161 multibuffer.push_excerpts(
19162 buffer_2.clone(),
19163 [
19164 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19165 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19166 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19167 ],
19168 cx,
19169 );
19170 multibuffer.push_excerpts(
19171 buffer_3.clone(),
19172 [
19173 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19174 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19175 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19176 ],
19177 cx,
19178 );
19179 multibuffer
19180 });
19181
19182 let editor =
19183 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19184 editor
19185 .update(cx, |editor, _window, cx| {
19186 for (buffer, diff_base) in [
19187 (buffer_1.clone(), file_1_old),
19188 (buffer_2.clone(), file_2_old),
19189 (buffer_3.clone(), file_3_old),
19190 ] {
19191 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19192 editor
19193 .buffer
19194 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19195 }
19196 })
19197 .unwrap();
19198
19199 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19200 cx.run_until_parked();
19201
19202 cx.assert_editor_state(
19203 &"
19204 ˇaaa
19205 ccc
19206 ddd
19207
19208 ggg
19209 hhh
19210
19211
19212 lll
19213 mmm
19214 NNN
19215
19216 qqq
19217 rrr
19218
19219 uuu
19220 111
19221 222
19222 333
19223
19224 666
19225 777
19226
19227 000
19228 !!!"
19229 .unindent(),
19230 );
19231
19232 cx.update_editor(|editor, window, cx| {
19233 editor.select_all(&SelectAll, window, cx);
19234 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19235 });
19236 cx.executor().run_until_parked();
19237
19238 cx.assert_state_with_diff(
19239 "
19240 «aaa
19241 - bbb
19242 ccc
19243 ddd
19244
19245 ggg
19246 hhh
19247
19248
19249 lll
19250 mmm
19251 - nnn
19252 + NNN
19253
19254 qqq
19255 rrr
19256
19257 uuu
19258 111
19259 222
19260 333
19261
19262 + 666
19263 777
19264
19265 000
19266 !!!ˇ»"
19267 .unindent(),
19268 );
19269}
19270
19271#[gpui::test]
19272async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19273 init_test(cx, |_| {});
19274
19275 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19276 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19277
19278 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19279 let multi_buffer = cx.new(|cx| {
19280 let mut multibuffer = MultiBuffer::new(ReadWrite);
19281 multibuffer.push_excerpts(
19282 buffer.clone(),
19283 [
19284 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19285 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19286 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19287 ],
19288 cx,
19289 );
19290 multibuffer
19291 });
19292
19293 let editor =
19294 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19295 editor
19296 .update(cx, |editor, _window, cx| {
19297 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19298 editor
19299 .buffer
19300 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19301 })
19302 .unwrap();
19303
19304 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19305 cx.run_until_parked();
19306
19307 cx.update_editor(|editor, window, cx| {
19308 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19309 });
19310 cx.executor().run_until_parked();
19311
19312 // When the start of a hunk coincides with the start of its excerpt,
19313 // the hunk is expanded. When the start of a hunk is earlier than
19314 // the start of its excerpt, the hunk is not expanded.
19315 cx.assert_state_with_diff(
19316 "
19317 ˇaaa
19318 - bbb
19319 + BBB
19320
19321 - ddd
19322 - eee
19323 + DDD
19324 + EEE
19325 fff
19326
19327 iii
19328 "
19329 .unindent(),
19330 );
19331}
19332
19333#[gpui::test]
19334async fn test_edits_around_expanded_insertion_hunks(
19335 executor: BackgroundExecutor,
19336 cx: &mut TestAppContext,
19337) {
19338 init_test(cx, |_| {});
19339
19340 let mut cx = EditorTestContext::new(cx).await;
19341
19342 let diff_base = r#"
19343 use some::mod1;
19344 use some::mod2;
19345
19346 const A: u32 = 42;
19347
19348 fn main() {
19349 println!("hello");
19350
19351 println!("world");
19352 }
19353 "#
19354 .unindent();
19355 executor.run_until_parked();
19356 cx.set_state(
19357 &r#"
19358 use some::mod1;
19359 use some::mod2;
19360
19361 const A: u32 = 42;
19362 const B: u32 = 42;
19363 const C: u32 = 42;
19364 ˇ
19365
19366 fn main() {
19367 println!("hello");
19368
19369 println!("world");
19370 }
19371 "#
19372 .unindent(),
19373 );
19374
19375 cx.set_head_text(&diff_base);
19376 executor.run_until_parked();
19377
19378 cx.update_editor(|editor, window, cx| {
19379 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19380 });
19381 executor.run_until_parked();
19382
19383 cx.assert_state_with_diff(
19384 r#"
19385 use some::mod1;
19386 use some::mod2;
19387
19388 const A: u32 = 42;
19389 + const B: u32 = 42;
19390 + const C: u32 = 42;
19391 + ˇ
19392
19393 fn main() {
19394 println!("hello");
19395
19396 println!("world");
19397 }
19398 "#
19399 .unindent(),
19400 );
19401
19402 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19403 executor.run_until_parked();
19404
19405 cx.assert_state_with_diff(
19406 r#"
19407 use some::mod1;
19408 use some::mod2;
19409
19410 const A: u32 = 42;
19411 + const B: u32 = 42;
19412 + const C: u32 = 42;
19413 + const D: u32 = 42;
19414 + ˇ
19415
19416 fn main() {
19417 println!("hello");
19418
19419 println!("world");
19420 }
19421 "#
19422 .unindent(),
19423 );
19424
19425 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19426 executor.run_until_parked();
19427
19428 cx.assert_state_with_diff(
19429 r#"
19430 use some::mod1;
19431 use some::mod2;
19432
19433 const A: u32 = 42;
19434 + const B: u32 = 42;
19435 + const C: u32 = 42;
19436 + const D: u32 = 42;
19437 + const E: u32 = 42;
19438 + ˇ
19439
19440 fn main() {
19441 println!("hello");
19442
19443 println!("world");
19444 }
19445 "#
19446 .unindent(),
19447 );
19448
19449 cx.update_editor(|editor, window, cx| {
19450 editor.delete_line(&DeleteLine, window, cx);
19451 });
19452 executor.run_until_parked();
19453
19454 cx.assert_state_with_diff(
19455 r#"
19456 use some::mod1;
19457 use some::mod2;
19458
19459 const A: u32 = 42;
19460 + const B: u32 = 42;
19461 + const C: u32 = 42;
19462 + const D: u32 = 42;
19463 + const E: u32 = 42;
19464 ˇ
19465 fn main() {
19466 println!("hello");
19467
19468 println!("world");
19469 }
19470 "#
19471 .unindent(),
19472 );
19473
19474 cx.update_editor(|editor, window, cx| {
19475 editor.move_up(&MoveUp, window, cx);
19476 editor.delete_line(&DeleteLine, window, cx);
19477 editor.move_up(&MoveUp, window, cx);
19478 editor.delete_line(&DeleteLine, window, cx);
19479 editor.move_up(&MoveUp, window, cx);
19480 editor.delete_line(&DeleteLine, window, cx);
19481 });
19482 executor.run_until_parked();
19483 cx.assert_state_with_diff(
19484 r#"
19485 use some::mod1;
19486 use some::mod2;
19487
19488 const A: u32 = 42;
19489 + const B: u32 = 42;
19490 ˇ
19491 fn main() {
19492 println!("hello");
19493
19494 println!("world");
19495 }
19496 "#
19497 .unindent(),
19498 );
19499
19500 cx.update_editor(|editor, window, cx| {
19501 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19502 editor.delete_line(&DeleteLine, window, cx);
19503 });
19504 executor.run_until_parked();
19505 cx.assert_state_with_diff(
19506 r#"
19507 ˇ
19508 fn main() {
19509 println!("hello");
19510
19511 println!("world");
19512 }
19513 "#
19514 .unindent(),
19515 );
19516}
19517
19518#[gpui::test]
19519async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19520 init_test(cx, |_| {});
19521
19522 let mut cx = EditorTestContext::new(cx).await;
19523 cx.set_head_text(indoc! { "
19524 one
19525 two
19526 three
19527 four
19528 five
19529 "
19530 });
19531 cx.set_state(indoc! { "
19532 one
19533 ˇthree
19534 five
19535 "});
19536 cx.run_until_parked();
19537 cx.update_editor(|editor, window, cx| {
19538 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19539 });
19540 cx.assert_state_with_diff(
19541 indoc! { "
19542 one
19543 - two
19544 ˇthree
19545 - four
19546 five
19547 "}
19548 .to_string(),
19549 );
19550 cx.update_editor(|editor, window, cx| {
19551 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19552 });
19553
19554 cx.assert_state_with_diff(
19555 indoc! { "
19556 one
19557 ˇthree
19558 five
19559 "}
19560 .to_string(),
19561 );
19562
19563 cx.set_state(indoc! { "
19564 one
19565 ˇTWO
19566 three
19567 four
19568 five
19569 "});
19570 cx.run_until_parked();
19571 cx.update_editor(|editor, window, cx| {
19572 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19573 });
19574
19575 cx.assert_state_with_diff(
19576 indoc! { "
19577 one
19578 - two
19579 + ˇTWO
19580 three
19581 four
19582 five
19583 "}
19584 .to_string(),
19585 );
19586 cx.update_editor(|editor, window, cx| {
19587 editor.move_up(&Default::default(), window, cx);
19588 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19589 });
19590 cx.assert_state_with_diff(
19591 indoc! { "
19592 one
19593 ˇTWO
19594 three
19595 four
19596 five
19597 "}
19598 .to_string(),
19599 );
19600}
19601
19602#[gpui::test]
19603async fn test_edits_around_expanded_deletion_hunks(
19604 executor: BackgroundExecutor,
19605 cx: &mut TestAppContext,
19606) {
19607 init_test(cx, |_| {});
19608
19609 let mut cx = EditorTestContext::new(cx).await;
19610
19611 let diff_base = r#"
19612 use some::mod1;
19613 use some::mod2;
19614
19615 const A: u32 = 42;
19616 const B: u32 = 42;
19617 const C: u32 = 42;
19618
19619
19620 fn main() {
19621 println!("hello");
19622
19623 println!("world");
19624 }
19625 "#
19626 .unindent();
19627 executor.run_until_parked();
19628 cx.set_state(
19629 &r#"
19630 use some::mod1;
19631 use some::mod2;
19632
19633 ˇconst B: u32 = 42;
19634 const C: u32 = 42;
19635
19636
19637 fn main() {
19638 println!("hello");
19639
19640 println!("world");
19641 }
19642 "#
19643 .unindent(),
19644 );
19645
19646 cx.set_head_text(&diff_base);
19647 executor.run_until_parked();
19648
19649 cx.update_editor(|editor, window, cx| {
19650 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19651 });
19652 executor.run_until_parked();
19653
19654 cx.assert_state_with_diff(
19655 r#"
19656 use some::mod1;
19657 use some::mod2;
19658
19659 - const A: u32 = 42;
19660 ˇconst B: u32 = 42;
19661 const C: u32 = 42;
19662
19663
19664 fn main() {
19665 println!("hello");
19666
19667 println!("world");
19668 }
19669 "#
19670 .unindent(),
19671 );
19672
19673 cx.update_editor(|editor, window, cx| {
19674 editor.delete_line(&DeleteLine, window, cx);
19675 });
19676 executor.run_until_parked();
19677 cx.assert_state_with_diff(
19678 r#"
19679 use some::mod1;
19680 use some::mod2;
19681
19682 - const A: u32 = 42;
19683 - const B: u32 = 42;
19684 ˇconst C: u32 = 42;
19685
19686
19687 fn main() {
19688 println!("hello");
19689
19690 println!("world");
19691 }
19692 "#
19693 .unindent(),
19694 );
19695
19696 cx.update_editor(|editor, window, cx| {
19697 editor.delete_line(&DeleteLine, window, cx);
19698 });
19699 executor.run_until_parked();
19700 cx.assert_state_with_diff(
19701 r#"
19702 use some::mod1;
19703 use some::mod2;
19704
19705 - const A: u32 = 42;
19706 - const B: u32 = 42;
19707 - const C: u32 = 42;
19708 ˇ
19709
19710 fn main() {
19711 println!("hello");
19712
19713 println!("world");
19714 }
19715 "#
19716 .unindent(),
19717 );
19718
19719 cx.update_editor(|editor, window, cx| {
19720 editor.handle_input("replacement", window, cx);
19721 });
19722 executor.run_until_parked();
19723 cx.assert_state_with_diff(
19724 r#"
19725 use some::mod1;
19726 use some::mod2;
19727
19728 - const A: u32 = 42;
19729 - const B: u32 = 42;
19730 - const C: u32 = 42;
19731 -
19732 + replacementˇ
19733
19734 fn main() {
19735 println!("hello");
19736
19737 println!("world");
19738 }
19739 "#
19740 .unindent(),
19741 );
19742}
19743
19744#[gpui::test]
19745async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19746 init_test(cx, |_| {});
19747
19748 let mut cx = EditorTestContext::new(cx).await;
19749
19750 let base_text = r#"
19751 one
19752 two
19753 three
19754 four
19755 five
19756 "#
19757 .unindent();
19758 executor.run_until_parked();
19759 cx.set_state(
19760 &r#"
19761 one
19762 two
19763 fˇour
19764 five
19765 "#
19766 .unindent(),
19767 );
19768
19769 cx.set_head_text(&base_text);
19770 executor.run_until_parked();
19771
19772 cx.update_editor(|editor, window, cx| {
19773 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19774 });
19775 executor.run_until_parked();
19776
19777 cx.assert_state_with_diff(
19778 r#"
19779 one
19780 two
19781 - three
19782 fˇour
19783 five
19784 "#
19785 .unindent(),
19786 );
19787
19788 cx.update_editor(|editor, window, cx| {
19789 editor.backspace(&Backspace, window, cx);
19790 editor.backspace(&Backspace, window, cx);
19791 });
19792 executor.run_until_parked();
19793 cx.assert_state_with_diff(
19794 r#"
19795 one
19796 two
19797 - threeˇ
19798 - four
19799 + our
19800 five
19801 "#
19802 .unindent(),
19803 );
19804}
19805
19806#[gpui::test]
19807async fn test_edit_after_expanded_modification_hunk(
19808 executor: BackgroundExecutor,
19809 cx: &mut TestAppContext,
19810) {
19811 init_test(cx, |_| {});
19812
19813 let mut cx = EditorTestContext::new(cx).await;
19814
19815 let diff_base = r#"
19816 use some::mod1;
19817 use some::mod2;
19818
19819 const A: u32 = 42;
19820 const B: u32 = 42;
19821 const C: u32 = 42;
19822 const D: u32 = 42;
19823
19824
19825 fn main() {
19826 println!("hello");
19827
19828 println!("world");
19829 }"#
19830 .unindent();
19831
19832 cx.set_state(
19833 &r#"
19834 use some::mod1;
19835 use some::mod2;
19836
19837 const A: u32 = 42;
19838 const B: u32 = 42;
19839 const C: u32 = 43ˇ
19840 const D: u32 = 42;
19841
19842
19843 fn main() {
19844 println!("hello");
19845
19846 println!("world");
19847 }"#
19848 .unindent(),
19849 );
19850
19851 cx.set_head_text(&diff_base);
19852 executor.run_until_parked();
19853 cx.update_editor(|editor, window, cx| {
19854 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19855 });
19856 executor.run_until_parked();
19857
19858 cx.assert_state_with_diff(
19859 r#"
19860 use some::mod1;
19861 use some::mod2;
19862
19863 const A: u32 = 42;
19864 const B: u32 = 42;
19865 - const C: u32 = 42;
19866 + const C: u32 = 43ˇ
19867 const D: u32 = 42;
19868
19869
19870 fn main() {
19871 println!("hello");
19872
19873 println!("world");
19874 }"#
19875 .unindent(),
19876 );
19877
19878 cx.update_editor(|editor, window, cx| {
19879 editor.handle_input("\nnew_line\n", window, cx);
19880 });
19881 executor.run_until_parked();
19882
19883 cx.assert_state_with_diff(
19884 r#"
19885 use some::mod1;
19886 use some::mod2;
19887
19888 const A: u32 = 42;
19889 const B: u32 = 42;
19890 - const C: u32 = 42;
19891 + const C: u32 = 43
19892 + new_line
19893 + ˇ
19894 const D: u32 = 42;
19895
19896
19897 fn main() {
19898 println!("hello");
19899
19900 println!("world");
19901 }"#
19902 .unindent(),
19903 );
19904}
19905
19906#[gpui::test]
19907async fn test_stage_and_unstage_added_file_hunk(
19908 executor: BackgroundExecutor,
19909 cx: &mut TestAppContext,
19910) {
19911 init_test(cx, |_| {});
19912
19913 let mut cx = EditorTestContext::new(cx).await;
19914 cx.update_editor(|editor, _, cx| {
19915 editor.set_expand_all_diff_hunks(cx);
19916 });
19917
19918 let working_copy = r#"
19919 ˇfn main() {
19920 println!("hello, world!");
19921 }
19922 "#
19923 .unindent();
19924
19925 cx.set_state(&working_copy);
19926 executor.run_until_parked();
19927
19928 cx.assert_state_with_diff(
19929 r#"
19930 + ˇfn main() {
19931 + println!("hello, world!");
19932 + }
19933 "#
19934 .unindent(),
19935 );
19936 cx.assert_index_text(None);
19937
19938 cx.update_editor(|editor, window, cx| {
19939 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19940 });
19941 executor.run_until_parked();
19942 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
19943 cx.assert_state_with_diff(
19944 r#"
19945 + ˇfn main() {
19946 + println!("hello, world!");
19947 + }
19948 "#
19949 .unindent(),
19950 );
19951
19952 cx.update_editor(|editor, window, cx| {
19953 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
19954 });
19955 executor.run_until_parked();
19956 cx.assert_index_text(None);
19957}
19958
19959async fn setup_indent_guides_editor(
19960 text: &str,
19961 cx: &mut TestAppContext,
19962) -> (BufferId, EditorTestContext) {
19963 init_test(cx, |_| {});
19964
19965 let mut cx = EditorTestContext::new(cx).await;
19966
19967 let buffer_id = cx.update_editor(|editor, window, cx| {
19968 editor.set_text(text, window, cx);
19969 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
19970
19971 buffer_ids[0]
19972 });
19973
19974 (buffer_id, cx)
19975}
19976
19977fn assert_indent_guides(
19978 range: Range<u32>,
19979 expected: Vec<IndentGuide>,
19980 active_indices: Option<Vec<usize>>,
19981 cx: &mut EditorTestContext,
19982) {
19983 let indent_guides = cx.update_editor(|editor, window, cx| {
19984 let snapshot = editor.snapshot(window, cx).display_snapshot;
19985 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
19986 editor,
19987 MultiBufferRow(range.start)..MultiBufferRow(range.end),
19988 true,
19989 &snapshot,
19990 cx,
19991 );
19992
19993 indent_guides.sort_by(|a, b| {
19994 a.depth.cmp(&b.depth).then(
19995 a.start_row
19996 .cmp(&b.start_row)
19997 .then(a.end_row.cmp(&b.end_row)),
19998 )
19999 });
20000 indent_guides
20001 });
20002
20003 if let Some(expected) = active_indices {
20004 let active_indices = cx.update_editor(|editor, window, cx| {
20005 let snapshot = editor.snapshot(window, cx).display_snapshot;
20006 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20007 });
20008
20009 assert_eq!(
20010 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20011 expected,
20012 "Active indent guide indices do not match"
20013 );
20014 }
20015
20016 assert_eq!(indent_guides, expected, "Indent guides do not match");
20017}
20018
20019fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20020 IndentGuide {
20021 buffer_id,
20022 start_row: MultiBufferRow(start_row),
20023 end_row: MultiBufferRow(end_row),
20024 depth,
20025 tab_size: 4,
20026 settings: IndentGuideSettings {
20027 enabled: true,
20028 line_width: 1,
20029 active_line_width: 1,
20030 coloring: IndentGuideColoring::default(),
20031 background_coloring: IndentGuideBackgroundColoring::default(),
20032 },
20033 }
20034}
20035
20036#[gpui::test]
20037async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20038 let (buffer_id, mut cx) = setup_indent_guides_editor(
20039 &"
20040 fn main() {
20041 let a = 1;
20042 }"
20043 .unindent(),
20044 cx,
20045 )
20046 .await;
20047
20048 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20049}
20050
20051#[gpui::test]
20052async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20053 let (buffer_id, mut cx) = setup_indent_guides_editor(
20054 &"
20055 fn main() {
20056 let a = 1;
20057 let b = 2;
20058 }"
20059 .unindent(),
20060 cx,
20061 )
20062 .await;
20063
20064 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20065}
20066
20067#[gpui::test]
20068async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20069 let (buffer_id, mut cx) = setup_indent_guides_editor(
20070 &"
20071 fn main() {
20072 let a = 1;
20073 if a == 3 {
20074 let b = 2;
20075 } else {
20076 let c = 3;
20077 }
20078 }"
20079 .unindent(),
20080 cx,
20081 )
20082 .await;
20083
20084 assert_indent_guides(
20085 0..8,
20086 vec![
20087 indent_guide(buffer_id, 1, 6, 0),
20088 indent_guide(buffer_id, 3, 3, 1),
20089 indent_guide(buffer_id, 5, 5, 1),
20090 ],
20091 None,
20092 &mut cx,
20093 );
20094}
20095
20096#[gpui::test]
20097async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20098 let (buffer_id, mut cx) = setup_indent_guides_editor(
20099 &"
20100 fn main() {
20101 let a = 1;
20102 let b = 2;
20103 let c = 3;
20104 }"
20105 .unindent(),
20106 cx,
20107 )
20108 .await;
20109
20110 assert_indent_guides(
20111 0..5,
20112 vec![
20113 indent_guide(buffer_id, 1, 3, 0),
20114 indent_guide(buffer_id, 2, 2, 1),
20115 ],
20116 None,
20117 &mut cx,
20118 );
20119}
20120
20121#[gpui::test]
20122async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20123 let (buffer_id, mut cx) = setup_indent_guides_editor(
20124 &"
20125 fn main() {
20126 let a = 1;
20127
20128 let c = 3;
20129 }"
20130 .unindent(),
20131 cx,
20132 )
20133 .await;
20134
20135 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20136}
20137
20138#[gpui::test]
20139async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20140 let (buffer_id, mut cx) = setup_indent_guides_editor(
20141 &"
20142 fn main() {
20143 let a = 1;
20144
20145 let c = 3;
20146
20147 if a == 3 {
20148 let b = 2;
20149 } else {
20150 let c = 3;
20151 }
20152 }"
20153 .unindent(),
20154 cx,
20155 )
20156 .await;
20157
20158 assert_indent_guides(
20159 0..11,
20160 vec![
20161 indent_guide(buffer_id, 1, 9, 0),
20162 indent_guide(buffer_id, 6, 6, 1),
20163 indent_guide(buffer_id, 8, 8, 1),
20164 ],
20165 None,
20166 &mut cx,
20167 );
20168}
20169
20170#[gpui::test]
20171async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20172 let (buffer_id, mut cx) = setup_indent_guides_editor(
20173 &"
20174 fn main() {
20175 let a = 1;
20176
20177 let c = 3;
20178
20179 if a == 3 {
20180 let b = 2;
20181 } else {
20182 let c = 3;
20183 }
20184 }"
20185 .unindent(),
20186 cx,
20187 )
20188 .await;
20189
20190 assert_indent_guides(
20191 1..11,
20192 vec![
20193 indent_guide(buffer_id, 1, 9, 0),
20194 indent_guide(buffer_id, 6, 6, 1),
20195 indent_guide(buffer_id, 8, 8, 1),
20196 ],
20197 None,
20198 &mut cx,
20199 );
20200}
20201
20202#[gpui::test]
20203async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20204 let (buffer_id, mut cx) = setup_indent_guides_editor(
20205 &"
20206 fn main() {
20207 let a = 1;
20208
20209 let c = 3;
20210
20211 if a == 3 {
20212 let b = 2;
20213 } else {
20214 let c = 3;
20215 }
20216 }"
20217 .unindent(),
20218 cx,
20219 )
20220 .await;
20221
20222 assert_indent_guides(
20223 1..10,
20224 vec![
20225 indent_guide(buffer_id, 1, 9, 0),
20226 indent_guide(buffer_id, 6, 6, 1),
20227 indent_guide(buffer_id, 8, 8, 1),
20228 ],
20229 None,
20230 &mut cx,
20231 );
20232}
20233
20234#[gpui::test]
20235async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20236 let (buffer_id, mut cx) = setup_indent_guides_editor(
20237 &"
20238 fn main() {
20239 if a {
20240 b(
20241 c,
20242 d,
20243 )
20244 } else {
20245 e(
20246 f
20247 )
20248 }
20249 }"
20250 .unindent(),
20251 cx,
20252 )
20253 .await;
20254
20255 assert_indent_guides(
20256 0..11,
20257 vec![
20258 indent_guide(buffer_id, 1, 10, 0),
20259 indent_guide(buffer_id, 2, 5, 1),
20260 indent_guide(buffer_id, 7, 9, 1),
20261 indent_guide(buffer_id, 3, 4, 2),
20262 indent_guide(buffer_id, 8, 8, 2),
20263 ],
20264 None,
20265 &mut cx,
20266 );
20267
20268 cx.update_editor(|editor, window, cx| {
20269 editor.fold_at(MultiBufferRow(2), window, cx);
20270 assert_eq!(
20271 editor.display_text(cx),
20272 "
20273 fn main() {
20274 if a {
20275 b(⋯
20276 )
20277 } else {
20278 e(
20279 f
20280 )
20281 }
20282 }"
20283 .unindent()
20284 );
20285 });
20286
20287 assert_indent_guides(
20288 0..11,
20289 vec![
20290 indent_guide(buffer_id, 1, 10, 0),
20291 indent_guide(buffer_id, 2, 5, 1),
20292 indent_guide(buffer_id, 7, 9, 1),
20293 indent_guide(buffer_id, 8, 8, 2),
20294 ],
20295 None,
20296 &mut cx,
20297 );
20298}
20299
20300#[gpui::test]
20301async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20302 let (buffer_id, mut cx) = setup_indent_guides_editor(
20303 &"
20304 block1
20305 block2
20306 block3
20307 block4
20308 block2
20309 block1
20310 block1"
20311 .unindent(),
20312 cx,
20313 )
20314 .await;
20315
20316 assert_indent_guides(
20317 1..10,
20318 vec![
20319 indent_guide(buffer_id, 1, 4, 0),
20320 indent_guide(buffer_id, 2, 3, 1),
20321 indent_guide(buffer_id, 3, 3, 2),
20322 ],
20323 None,
20324 &mut cx,
20325 );
20326}
20327
20328#[gpui::test]
20329async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20330 let (buffer_id, mut cx) = setup_indent_guides_editor(
20331 &"
20332 block1
20333 block2
20334 block3
20335
20336 block1
20337 block1"
20338 .unindent(),
20339 cx,
20340 )
20341 .await;
20342
20343 assert_indent_guides(
20344 0..6,
20345 vec![
20346 indent_guide(buffer_id, 1, 2, 0),
20347 indent_guide(buffer_id, 2, 2, 1),
20348 ],
20349 None,
20350 &mut cx,
20351 );
20352}
20353
20354#[gpui::test]
20355async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20356 let (buffer_id, mut cx) = setup_indent_guides_editor(
20357 &"
20358 function component() {
20359 \treturn (
20360 \t\t\t
20361 \t\t<div>
20362 \t\t\t<abc></abc>
20363 \t\t</div>
20364 \t)
20365 }"
20366 .unindent(),
20367 cx,
20368 )
20369 .await;
20370
20371 assert_indent_guides(
20372 0..8,
20373 vec![
20374 indent_guide(buffer_id, 1, 6, 0),
20375 indent_guide(buffer_id, 2, 5, 1),
20376 indent_guide(buffer_id, 4, 4, 2),
20377 ],
20378 None,
20379 &mut cx,
20380 );
20381}
20382
20383#[gpui::test]
20384async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20385 let (buffer_id, mut cx) = setup_indent_guides_editor(
20386 &"
20387 function component() {
20388 \treturn (
20389 \t
20390 \t\t<div>
20391 \t\t\t<abc></abc>
20392 \t\t</div>
20393 \t)
20394 }"
20395 .unindent(),
20396 cx,
20397 )
20398 .await;
20399
20400 assert_indent_guides(
20401 0..8,
20402 vec![
20403 indent_guide(buffer_id, 1, 6, 0),
20404 indent_guide(buffer_id, 2, 5, 1),
20405 indent_guide(buffer_id, 4, 4, 2),
20406 ],
20407 None,
20408 &mut cx,
20409 );
20410}
20411
20412#[gpui::test]
20413async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20414 let (buffer_id, mut cx) = setup_indent_guides_editor(
20415 &"
20416 block1
20417
20418
20419
20420 block2
20421 "
20422 .unindent(),
20423 cx,
20424 )
20425 .await;
20426
20427 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20428}
20429
20430#[gpui::test]
20431async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20432 let (buffer_id, mut cx) = setup_indent_guides_editor(
20433 &"
20434 def a:
20435 \tb = 3
20436 \tif True:
20437 \t\tc = 4
20438 \t\td = 5
20439 \tprint(b)
20440 "
20441 .unindent(),
20442 cx,
20443 )
20444 .await;
20445
20446 assert_indent_guides(
20447 0..6,
20448 vec![
20449 indent_guide(buffer_id, 1, 5, 0),
20450 indent_guide(buffer_id, 3, 4, 1),
20451 ],
20452 None,
20453 &mut cx,
20454 );
20455}
20456
20457#[gpui::test]
20458async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20459 let (buffer_id, mut cx) = setup_indent_guides_editor(
20460 &"
20461 fn main() {
20462 let a = 1;
20463 }"
20464 .unindent(),
20465 cx,
20466 )
20467 .await;
20468
20469 cx.update_editor(|editor, window, cx| {
20470 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20471 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20472 });
20473 });
20474
20475 assert_indent_guides(
20476 0..3,
20477 vec![indent_guide(buffer_id, 1, 1, 0)],
20478 Some(vec![0]),
20479 &mut cx,
20480 );
20481}
20482
20483#[gpui::test]
20484async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20485 let (buffer_id, mut cx) = setup_indent_guides_editor(
20486 &"
20487 fn main() {
20488 if 1 == 2 {
20489 let a = 1;
20490 }
20491 }"
20492 .unindent(),
20493 cx,
20494 )
20495 .await;
20496
20497 cx.update_editor(|editor, window, cx| {
20498 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20499 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20500 });
20501 });
20502
20503 assert_indent_guides(
20504 0..4,
20505 vec![
20506 indent_guide(buffer_id, 1, 3, 0),
20507 indent_guide(buffer_id, 2, 2, 1),
20508 ],
20509 Some(vec![1]),
20510 &mut cx,
20511 );
20512
20513 cx.update_editor(|editor, window, cx| {
20514 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20515 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20516 });
20517 });
20518
20519 assert_indent_guides(
20520 0..4,
20521 vec![
20522 indent_guide(buffer_id, 1, 3, 0),
20523 indent_guide(buffer_id, 2, 2, 1),
20524 ],
20525 Some(vec![1]),
20526 &mut cx,
20527 );
20528
20529 cx.update_editor(|editor, window, cx| {
20530 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20531 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20532 });
20533 });
20534
20535 assert_indent_guides(
20536 0..4,
20537 vec![
20538 indent_guide(buffer_id, 1, 3, 0),
20539 indent_guide(buffer_id, 2, 2, 1),
20540 ],
20541 Some(vec![0]),
20542 &mut cx,
20543 );
20544}
20545
20546#[gpui::test]
20547async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
20548 let (buffer_id, mut cx) = setup_indent_guides_editor(
20549 &"
20550 fn main() {
20551 let a = 1;
20552
20553 let b = 2;
20554 }"
20555 .unindent(),
20556 cx,
20557 )
20558 .await;
20559
20560 cx.update_editor(|editor, window, cx| {
20561 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20562 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20563 });
20564 });
20565
20566 assert_indent_guides(
20567 0..5,
20568 vec![indent_guide(buffer_id, 1, 3, 0)],
20569 Some(vec![0]),
20570 &mut cx,
20571 );
20572}
20573
20574#[gpui::test]
20575async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
20576 let (buffer_id, mut cx) = setup_indent_guides_editor(
20577 &"
20578 def m:
20579 a = 1
20580 pass"
20581 .unindent(),
20582 cx,
20583 )
20584 .await;
20585
20586 cx.update_editor(|editor, window, cx| {
20587 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20588 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20589 });
20590 });
20591
20592 assert_indent_guides(
20593 0..3,
20594 vec![indent_guide(buffer_id, 1, 2, 0)],
20595 Some(vec![0]),
20596 &mut cx,
20597 );
20598}
20599
20600#[gpui::test]
20601async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
20602 init_test(cx, |_| {});
20603 let mut cx = EditorTestContext::new(cx).await;
20604 let text = indoc! {
20605 "
20606 impl A {
20607 fn b() {
20608 0;
20609 3;
20610 5;
20611 6;
20612 7;
20613 }
20614 }
20615 "
20616 };
20617 let base_text = indoc! {
20618 "
20619 impl A {
20620 fn b() {
20621 0;
20622 1;
20623 2;
20624 3;
20625 4;
20626 }
20627 fn c() {
20628 5;
20629 6;
20630 7;
20631 }
20632 }
20633 "
20634 };
20635
20636 cx.update_editor(|editor, window, cx| {
20637 editor.set_text(text, window, cx);
20638
20639 editor.buffer().update(cx, |multibuffer, cx| {
20640 let buffer = multibuffer.as_singleton().unwrap();
20641 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
20642
20643 multibuffer.set_all_diff_hunks_expanded(cx);
20644 multibuffer.add_diff(diff, cx);
20645
20646 buffer.read(cx).remote_id()
20647 })
20648 });
20649 cx.run_until_parked();
20650
20651 cx.assert_state_with_diff(
20652 indoc! { "
20653 impl A {
20654 fn b() {
20655 0;
20656 - 1;
20657 - 2;
20658 3;
20659 - 4;
20660 - }
20661 - fn c() {
20662 5;
20663 6;
20664 7;
20665 }
20666 }
20667 ˇ"
20668 }
20669 .to_string(),
20670 );
20671
20672 let mut actual_guides = cx.update_editor(|editor, window, cx| {
20673 editor
20674 .snapshot(window, cx)
20675 .buffer_snapshot
20676 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
20677 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
20678 .collect::<Vec<_>>()
20679 });
20680 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
20681 assert_eq!(
20682 actual_guides,
20683 vec![
20684 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
20685 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
20686 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
20687 ]
20688 );
20689}
20690
20691#[gpui::test]
20692async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20693 init_test(cx, |_| {});
20694 let mut cx = EditorTestContext::new(cx).await;
20695
20696 let diff_base = r#"
20697 a
20698 b
20699 c
20700 "#
20701 .unindent();
20702
20703 cx.set_state(
20704 &r#"
20705 ˇA
20706 b
20707 C
20708 "#
20709 .unindent(),
20710 );
20711 cx.set_head_text(&diff_base);
20712 cx.update_editor(|editor, window, cx| {
20713 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20714 });
20715 executor.run_until_parked();
20716
20717 let both_hunks_expanded = r#"
20718 - a
20719 + ˇA
20720 b
20721 - c
20722 + C
20723 "#
20724 .unindent();
20725
20726 cx.assert_state_with_diff(both_hunks_expanded.clone());
20727
20728 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20729 let snapshot = editor.snapshot(window, cx);
20730 let hunks = editor
20731 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20732 .collect::<Vec<_>>();
20733 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20734 let buffer_id = hunks[0].buffer_id;
20735 hunks
20736 .into_iter()
20737 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20738 .collect::<Vec<_>>()
20739 });
20740 assert_eq!(hunk_ranges.len(), 2);
20741
20742 cx.update_editor(|editor, _, cx| {
20743 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20744 });
20745 executor.run_until_parked();
20746
20747 let second_hunk_expanded = r#"
20748 ˇA
20749 b
20750 - c
20751 + C
20752 "#
20753 .unindent();
20754
20755 cx.assert_state_with_diff(second_hunk_expanded);
20756
20757 cx.update_editor(|editor, _, cx| {
20758 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20759 });
20760 executor.run_until_parked();
20761
20762 cx.assert_state_with_diff(both_hunks_expanded.clone());
20763
20764 cx.update_editor(|editor, _, cx| {
20765 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20766 });
20767 executor.run_until_parked();
20768
20769 let first_hunk_expanded = r#"
20770 - a
20771 + ˇA
20772 b
20773 C
20774 "#
20775 .unindent();
20776
20777 cx.assert_state_with_diff(first_hunk_expanded);
20778
20779 cx.update_editor(|editor, _, cx| {
20780 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20781 });
20782 executor.run_until_parked();
20783
20784 cx.assert_state_with_diff(both_hunks_expanded);
20785
20786 cx.set_state(
20787 &r#"
20788 ˇA
20789 b
20790 "#
20791 .unindent(),
20792 );
20793 cx.run_until_parked();
20794
20795 // TODO this cursor position seems bad
20796 cx.assert_state_with_diff(
20797 r#"
20798 - ˇa
20799 + A
20800 b
20801 "#
20802 .unindent(),
20803 );
20804
20805 cx.update_editor(|editor, window, cx| {
20806 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20807 });
20808
20809 cx.assert_state_with_diff(
20810 r#"
20811 - ˇa
20812 + A
20813 b
20814 - c
20815 "#
20816 .unindent(),
20817 );
20818
20819 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20820 let snapshot = editor.snapshot(window, cx);
20821 let hunks = editor
20822 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20823 .collect::<Vec<_>>();
20824 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20825 let buffer_id = hunks[0].buffer_id;
20826 hunks
20827 .into_iter()
20828 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20829 .collect::<Vec<_>>()
20830 });
20831 assert_eq!(hunk_ranges.len(), 2);
20832
20833 cx.update_editor(|editor, _, cx| {
20834 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
20835 });
20836 executor.run_until_parked();
20837
20838 cx.assert_state_with_diff(
20839 r#"
20840 - ˇa
20841 + A
20842 b
20843 "#
20844 .unindent(),
20845 );
20846}
20847
20848#[gpui::test]
20849async fn test_toggle_deletion_hunk_at_start_of_file(
20850 executor: BackgroundExecutor,
20851 cx: &mut TestAppContext,
20852) {
20853 init_test(cx, |_| {});
20854 let mut cx = EditorTestContext::new(cx).await;
20855
20856 let diff_base = r#"
20857 a
20858 b
20859 c
20860 "#
20861 .unindent();
20862
20863 cx.set_state(
20864 &r#"
20865 ˇb
20866 c
20867 "#
20868 .unindent(),
20869 );
20870 cx.set_head_text(&diff_base);
20871 cx.update_editor(|editor, window, cx| {
20872 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20873 });
20874 executor.run_until_parked();
20875
20876 let hunk_expanded = r#"
20877 - a
20878 ˇb
20879 c
20880 "#
20881 .unindent();
20882
20883 cx.assert_state_with_diff(hunk_expanded.clone());
20884
20885 let hunk_ranges = cx.update_editor(|editor, window, cx| {
20886 let snapshot = editor.snapshot(window, cx);
20887 let hunks = editor
20888 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
20889 .collect::<Vec<_>>();
20890 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
20891 let buffer_id = hunks[0].buffer_id;
20892 hunks
20893 .into_iter()
20894 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
20895 .collect::<Vec<_>>()
20896 });
20897 assert_eq!(hunk_ranges.len(), 1);
20898
20899 cx.update_editor(|editor, _, cx| {
20900 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20901 });
20902 executor.run_until_parked();
20903
20904 let hunk_collapsed = r#"
20905 ˇb
20906 c
20907 "#
20908 .unindent();
20909
20910 cx.assert_state_with_diff(hunk_collapsed);
20911
20912 cx.update_editor(|editor, _, cx| {
20913 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
20914 });
20915 executor.run_until_parked();
20916
20917 cx.assert_state_with_diff(hunk_expanded);
20918}
20919
20920#[gpui::test]
20921async fn test_display_diff_hunks(cx: &mut TestAppContext) {
20922 init_test(cx, |_| {});
20923
20924 let fs = FakeFs::new(cx.executor());
20925 fs.insert_tree(
20926 path!("/test"),
20927 json!({
20928 ".git": {},
20929 "file-1": "ONE\n",
20930 "file-2": "TWO\n",
20931 "file-3": "THREE\n",
20932 }),
20933 )
20934 .await;
20935
20936 fs.set_head_for_repo(
20937 path!("/test/.git").as_ref(),
20938 &[
20939 ("file-1".into(), "one\n".into()),
20940 ("file-2".into(), "two\n".into()),
20941 ("file-3".into(), "three\n".into()),
20942 ],
20943 "deadbeef",
20944 );
20945
20946 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
20947 let mut buffers = vec![];
20948 for i in 1..=3 {
20949 let buffer = project
20950 .update(cx, |project, cx| {
20951 let path = format!(path!("/test/file-{}"), i);
20952 project.open_local_buffer(path, cx)
20953 })
20954 .await
20955 .unwrap();
20956 buffers.push(buffer);
20957 }
20958
20959 let multibuffer = cx.new(|cx| {
20960 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
20961 multibuffer.set_all_diff_hunks_expanded(cx);
20962 for buffer in &buffers {
20963 let snapshot = buffer.read(cx).snapshot();
20964 multibuffer.set_excerpts_for_path(
20965 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
20966 buffer.clone(),
20967 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
20968 2,
20969 cx,
20970 );
20971 }
20972 multibuffer
20973 });
20974
20975 let editor = cx.add_window(|window, cx| {
20976 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
20977 });
20978 cx.run_until_parked();
20979
20980 let snapshot = editor
20981 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
20982 .unwrap();
20983 let hunks = snapshot
20984 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
20985 .map(|hunk| match hunk {
20986 DisplayDiffHunk::Unfolded {
20987 display_row_range, ..
20988 } => display_row_range,
20989 DisplayDiffHunk::Folded { .. } => unreachable!(),
20990 })
20991 .collect::<Vec<_>>();
20992 assert_eq!(
20993 hunks,
20994 [
20995 DisplayRow(2)..DisplayRow(4),
20996 DisplayRow(7)..DisplayRow(9),
20997 DisplayRow(12)..DisplayRow(14),
20998 ]
20999 );
21000}
21001
21002#[gpui::test]
21003async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21004 init_test(cx, |_| {});
21005
21006 let mut cx = EditorTestContext::new(cx).await;
21007 cx.set_head_text(indoc! { "
21008 one
21009 two
21010 three
21011 four
21012 five
21013 "
21014 });
21015 cx.set_index_text(indoc! { "
21016 one
21017 two
21018 three
21019 four
21020 five
21021 "
21022 });
21023 cx.set_state(indoc! {"
21024 one
21025 TWO
21026 ˇTHREE
21027 FOUR
21028 five
21029 "});
21030 cx.run_until_parked();
21031 cx.update_editor(|editor, window, cx| {
21032 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21033 });
21034 cx.run_until_parked();
21035 cx.assert_index_text(Some(indoc! {"
21036 one
21037 TWO
21038 THREE
21039 FOUR
21040 five
21041 "}));
21042 cx.set_state(indoc! { "
21043 one
21044 TWO
21045 ˇTHREE-HUNDRED
21046 FOUR
21047 five
21048 "});
21049 cx.run_until_parked();
21050 cx.update_editor(|editor, window, cx| {
21051 let snapshot = editor.snapshot(window, cx);
21052 let hunks = editor
21053 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
21054 .collect::<Vec<_>>();
21055 assert_eq!(hunks.len(), 1);
21056 assert_eq!(
21057 hunks[0].status(),
21058 DiffHunkStatus {
21059 kind: DiffHunkStatusKind::Modified,
21060 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21061 }
21062 );
21063
21064 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21065 });
21066 cx.run_until_parked();
21067 cx.assert_index_text(Some(indoc! {"
21068 one
21069 TWO
21070 THREE-HUNDRED
21071 FOUR
21072 five
21073 "}));
21074}
21075
21076#[gpui::test]
21077fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21078 init_test(cx, |_| {});
21079
21080 let editor = cx.add_window(|window, cx| {
21081 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21082 build_editor(buffer, window, cx)
21083 });
21084
21085 let render_args = Arc::new(Mutex::new(None));
21086 let snapshot = editor
21087 .update(cx, |editor, window, cx| {
21088 let snapshot = editor.buffer().read(cx).snapshot(cx);
21089 let range =
21090 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21091
21092 struct RenderArgs {
21093 row: MultiBufferRow,
21094 folded: bool,
21095 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21096 }
21097
21098 let crease = Crease::inline(
21099 range,
21100 FoldPlaceholder::test(),
21101 {
21102 let toggle_callback = render_args.clone();
21103 move |row, folded, callback, _window, _cx| {
21104 *toggle_callback.lock() = Some(RenderArgs {
21105 row,
21106 folded,
21107 callback,
21108 });
21109 div()
21110 }
21111 },
21112 |_row, _folded, _window, _cx| div(),
21113 );
21114
21115 editor.insert_creases(Some(crease), cx);
21116 let snapshot = editor.snapshot(window, cx);
21117 let _div =
21118 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21119 snapshot
21120 })
21121 .unwrap();
21122
21123 let render_args = render_args.lock().take().unwrap();
21124 assert_eq!(render_args.row, MultiBufferRow(1));
21125 assert!(!render_args.folded);
21126 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21127
21128 cx.update_window(*editor, |_, window, cx| {
21129 (render_args.callback)(true, window, cx)
21130 })
21131 .unwrap();
21132 let snapshot = editor
21133 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21134 .unwrap();
21135 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21136
21137 cx.update_window(*editor, |_, window, cx| {
21138 (render_args.callback)(false, window, cx)
21139 })
21140 .unwrap();
21141 let snapshot = editor
21142 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21143 .unwrap();
21144 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21145}
21146
21147#[gpui::test]
21148async fn test_input_text(cx: &mut TestAppContext) {
21149 init_test(cx, |_| {});
21150 let mut cx = EditorTestContext::new(cx).await;
21151
21152 cx.set_state(
21153 &r#"ˇone
21154 two
21155
21156 three
21157 fourˇ
21158 five
21159
21160 siˇx"#
21161 .unindent(),
21162 );
21163
21164 cx.dispatch_action(HandleInput(String::new()));
21165 cx.assert_editor_state(
21166 &r#"ˇone
21167 two
21168
21169 three
21170 fourˇ
21171 five
21172
21173 siˇx"#
21174 .unindent(),
21175 );
21176
21177 cx.dispatch_action(HandleInput("AAAA".to_string()));
21178 cx.assert_editor_state(
21179 &r#"AAAAˇone
21180 two
21181
21182 three
21183 fourAAAAˇ
21184 five
21185
21186 siAAAAˇx"#
21187 .unindent(),
21188 );
21189}
21190
21191#[gpui::test]
21192async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21193 init_test(cx, |_| {});
21194
21195 let mut cx = EditorTestContext::new(cx).await;
21196 cx.set_state(
21197 r#"let foo = 1;
21198let foo = 2;
21199let foo = 3;
21200let fooˇ = 4;
21201let foo = 5;
21202let foo = 6;
21203let foo = 7;
21204let foo = 8;
21205let foo = 9;
21206let foo = 10;
21207let foo = 11;
21208let foo = 12;
21209let foo = 13;
21210let foo = 14;
21211let foo = 15;"#,
21212 );
21213
21214 cx.update_editor(|e, window, cx| {
21215 assert_eq!(
21216 e.next_scroll_position,
21217 NextScrollCursorCenterTopBottom::Center,
21218 "Default next scroll direction is center",
21219 );
21220
21221 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21222 assert_eq!(
21223 e.next_scroll_position,
21224 NextScrollCursorCenterTopBottom::Top,
21225 "After center, next scroll direction should be top",
21226 );
21227
21228 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21229 assert_eq!(
21230 e.next_scroll_position,
21231 NextScrollCursorCenterTopBottom::Bottom,
21232 "After top, next scroll direction should be bottom",
21233 );
21234
21235 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21236 assert_eq!(
21237 e.next_scroll_position,
21238 NextScrollCursorCenterTopBottom::Center,
21239 "After bottom, scrolling should start over",
21240 );
21241
21242 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21243 assert_eq!(
21244 e.next_scroll_position,
21245 NextScrollCursorCenterTopBottom::Top,
21246 "Scrolling continues if retriggered fast enough"
21247 );
21248 });
21249
21250 cx.executor()
21251 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21252 cx.executor().run_until_parked();
21253 cx.update_editor(|e, _, _| {
21254 assert_eq!(
21255 e.next_scroll_position,
21256 NextScrollCursorCenterTopBottom::Center,
21257 "If scrolling is not triggered fast enough, it should reset"
21258 );
21259 });
21260}
21261
21262#[gpui::test]
21263async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21264 init_test(cx, |_| {});
21265 let mut cx = EditorLspTestContext::new_rust(
21266 lsp::ServerCapabilities {
21267 definition_provider: Some(lsp::OneOf::Left(true)),
21268 references_provider: Some(lsp::OneOf::Left(true)),
21269 ..lsp::ServerCapabilities::default()
21270 },
21271 cx,
21272 )
21273 .await;
21274
21275 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21276 let go_to_definition = cx
21277 .lsp
21278 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21279 move |params, _| async move {
21280 if empty_go_to_definition {
21281 Ok(None)
21282 } else {
21283 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21284 uri: params.text_document_position_params.text_document.uri,
21285 range: lsp::Range::new(
21286 lsp::Position::new(4, 3),
21287 lsp::Position::new(4, 6),
21288 ),
21289 })))
21290 }
21291 },
21292 );
21293 let references = cx
21294 .lsp
21295 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21296 Ok(Some(vec![lsp::Location {
21297 uri: params.text_document_position.text_document.uri,
21298 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21299 }]))
21300 });
21301 (go_to_definition, references)
21302 };
21303
21304 cx.set_state(
21305 &r#"fn one() {
21306 let mut a = ˇtwo();
21307 }
21308
21309 fn two() {}"#
21310 .unindent(),
21311 );
21312 set_up_lsp_handlers(false, &mut cx);
21313 let navigated = cx
21314 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21315 .await
21316 .expect("Failed to navigate to definition");
21317 assert_eq!(
21318 navigated,
21319 Navigated::Yes,
21320 "Should have navigated to definition from the GetDefinition response"
21321 );
21322 cx.assert_editor_state(
21323 &r#"fn one() {
21324 let mut a = two();
21325 }
21326
21327 fn «twoˇ»() {}"#
21328 .unindent(),
21329 );
21330
21331 let editors = cx.update_workspace(|workspace, _, cx| {
21332 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21333 });
21334 cx.update_editor(|_, _, test_editor_cx| {
21335 assert_eq!(
21336 editors.len(),
21337 1,
21338 "Initially, only one, test, editor should be open in the workspace"
21339 );
21340 assert_eq!(
21341 test_editor_cx.entity(),
21342 editors.last().expect("Asserted len is 1").clone()
21343 );
21344 });
21345
21346 set_up_lsp_handlers(true, &mut cx);
21347 let navigated = cx
21348 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21349 .await
21350 .expect("Failed to navigate to lookup references");
21351 assert_eq!(
21352 navigated,
21353 Navigated::Yes,
21354 "Should have navigated to references as a fallback after empty GoToDefinition response"
21355 );
21356 // We should not change the selections in the existing file,
21357 // if opening another milti buffer with the references
21358 cx.assert_editor_state(
21359 &r#"fn one() {
21360 let mut a = two();
21361 }
21362
21363 fn «twoˇ»() {}"#
21364 .unindent(),
21365 );
21366 let editors = cx.update_workspace(|workspace, _, cx| {
21367 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21368 });
21369 cx.update_editor(|_, _, test_editor_cx| {
21370 assert_eq!(
21371 editors.len(),
21372 2,
21373 "After falling back to references search, we open a new editor with the results"
21374 );
21375 let references_fallback_text = editors
21376 .into_iter()
21377 .find(|new_editor| *new_editor != test_editor_cx.entity())
21378 .expect("Should have one non-test editor now")
21379 .read(test_editor_cx)
21380 .text(test_editor_cx);
21381 assert_eq!(
21382 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21383 "Should use the range from the references response and not the GoToDefinition one"
21384 );
21385 });
21386}
21387
21388#[gpui::test]
21389async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21390 init_test(cx, |_| {});
21391 cx.update(|cx| {
21392 let mut editor_settings = EditorSettings::get_global(cx).clone();
21393 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21394 EditorSettings::override_global(editor_settings, cx);
21395 });
21396 let mut cx = EditorLspTestContext::new_rust(
21397 lsp::ServerCapabilities {
21398 definition_provider: Some(lsp::OneOf::Left(true)),
21399 references_provider: Some(lsp::OneOf::Left(true)),
21400 ..lsp::ServerCapabilities::default()
21401 },
21402 cx,
21403 )
21404 .await;
21405 let original_state = r#"fn one() {
21406 let mut a = ˇtwo();
21407 }
21408
21409 fn two() {}"#
21410 .unindent();
21411 cx.set_state(&original_state);
21412
21413 let mut go_to_definition = cx
21414 .lsp
21415 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21416 move |_, _| async move { Ok(None) },
21417 );
21418 let _references = cx
21419 .lsp
21420 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21421 panic!("Should not call for references with no go to definition fallback")
21422 });
21423
21424 let navigated = cx
21425 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21426 .await
21427 .expect("Failed to navigate to lookup references");
21428 go_to_definition
21429 .next()
21430 .await
21431 .expect("Should have called the go_to_definition handler");
21432
21433 assert_eq!(
21434 navigated,
21435 Navigated::No,
21436 "Should have navigated to references as a fallback after empty GoToDefinition response"
21437 );
21438 cx.assert_editor_state(&original_state);
21439 let editors = cx.update_workspace(|workspace, _, cx| {
21440 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21441 });
21442 cx.update_editor(|_, _, _| {
21443 assert_eq!(
21444 editors.len(),
21445 1,
21446 "After unsuccessful fallback, no other editor should have been opened"
21447 );
21448 });
21449}
21450
21451#[gpui::test]
21452async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21453 init_test(cx, |_| {});
21454 let mut cx = EditorLspTestContext::new_rust(
21455 lsp::ServerCapabilities {
21456 references_provider: Some(lsp::OneOf::Left(true)),
21457 ..lsp::ServerCapabilities::default()
21458 },
21459 cx,
21460 )
21461 .await;
21462
21463 cx.set_state(
21464 &r#"
21465 fn one() {
21466 let mut a = two();
21467 }
21468
21469 fn ˇtwo() {}"#
21470 .unindent(),
21471 );
21472 cx.lsp
21473 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21474 Ok(Some(vec![
21475 lsp::Location {
21476 uri: params.text_document_position.text_document.uri.clone(),
21477 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21478 },
21479 lsp::Location {
21480 uri: params.text_document_position.text_document.uri,
21481 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21482 },
21483 ]))
21484 });
21485 let navigated = cx
21486 .update_editor(|editor, window, cx| {
21487 editor.find_all_references(&FindAllReferences, window, cx)
21488 })
21489 .unwrap()
21490 .await
21491 .expect("Failed to navigate to references");
21492 assert_eq!(
21493 navigated,
21494 Navigated::Yes,
21495 "Should have navigated to references from the FindAllReferences response"
21496 );
21497 cx.assert_editor_state(
21498 &r#"fn one() {
21499 let mut a = two();
21500 }
21501
21502 fn ˇtwo() {}"#
21503 .unindent(),
21504 );
21505
21506 let editors = cx.update_workspace(|workspace, _, cx| {
21507 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21508 });
21509 cx.update_editor(|_, _, _| {
21510 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21511 });
21512
21513 cx.set_state(
21514 &r#"fn one() {
21515 let mut a = ˇtwo();
21516 }
21517
21518 fn two() {}"#
21519 .unindent(),
21520 );
21521 let navigated = cx
21522 .update_editor(|editor, window, cx| {
21523 editor.find_all_references(&FindAllReferences, window, cx)
21524 })
21525 .unwrap()
21526 .await
21527 .expect("Failed to navigate to references");
21528 assert_eq!(
21529 navigated,
21530 Navigated::Yes,
21531 "Should have navigated to references from the FindAllReferences response"
21532 );
21533 cx.assert_editor_state(
21534 &r#"fn one() {
21535 let mut a = ˇtwo();
21536 }
21537
21538 fn two() {}"#
21539 .unindent(),
21540 );
21541 let editors = cx.update_workspace(|workspace, _, cx| {
21542 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21543 });
21544 cx.update_editor(|_, _, _| {
21545 assert_eq!(
21546 editors.len(),
21547 2,
21548 "should have re-used the previous multibuffer"
21549 );
21550 });
21551
21552 cx.set_state(
21553 &r#"fn one() {
21554 let mut a = ˇtwo();
21555 }
21556 fn three() {}
21557 fn two() {}"#
21558 .unindent(),
21559 );
21560 cx.lsp
21561 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21562 Ok(Some(vec![
21563 lsp::Location {
21564 uri: params.text_document_position.text_document.uri.clone(),
21565 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21566 },
21567 lsp::Location {
21568 uri: params.text_document_position.text_document.uri,
21569 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
21570 },
21571 ]))
21572 });
21573 let navigated = cx
21574 .update_editor(|editor, window, cx| {
21575 editor.find_all_references(&FindAllReferences, window, cx)
21576 })
21577 .unwrap()
21578 .await
21579 .expect("Failed to navigate to references");
21580 assert_eq!(
21581 navigated,
21582 Navigated::Yes,
21583 "Should have navigated to references from the FindAllReferences response"
21584 );
21585 cx.assert_editor_state(
21586 &r#"fn one() {
21587 let mut a = ˇtwo();
21588 }
21589 fn three() {}
21590 fn two() {}"#
21591 .unindent(),
21592 );
21593 let editors = cx.update_workspace(|workspace, _, cx| {
21594 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21595 });
21596 cx.update_editor(|_, _, _| {
21597 assert_eq!(
21598 editors.len(),
21599 3,
21600 "should have used a new multibuffer as offsets changed"
21601 );
21602 });
21603}
21604#[gpui::test]
21605async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
21606 init_test(cx, |_| {});
21607
21608 let language = Arc::new(Language::new(
21609 LanguageConfig::default(),
21610 Some(tree_sitter_rust::LANGUAGE.into()),
21611 ));
21612
21613 let text = r#"
21614 #[cfg(test)]
21615 mod tests() {
21616 #[test]
21617 fn runnable_1() {
21618 let a = 1;
21619 }
21620
21621 #[test]
21622 fn runnable_2() {
21623 let a = 1;
21624 let b = 2;
21625 }
21626 }
21627 "#
21628 .unindent();
21629
21630 let fs = FakeFs::new(cx.executor());
21631 fs.insert_file("/file.rs", Default::default()).await;
21632
21633 let project = Project::test(fs, ["/a".as_ref()], cx).await;
21634 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21635 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21636 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
21637 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
21638
21639 let editor = cx.new_window_entity(|window, cx| {
21640 Editor::new(
21641 EditorMode::full(),
21642 multi_buffer,
21643 Some(project.clone()),
21644 window,
21645 cx,
21646 )
21647 });
21648
21649 editor.update_in(cx, |editor, window, cx| {
21650 let snapshot = editor.buffer().read(cx).snapshot(cx);
21651 editor.tasks.insert(
21652 (buffer.read(cx).remote_id(), 3),
21653 RunnableTasks {
21654 templates: vec![],
21655 offset: snapshot.anchor_before(43),
21656 column: 0,
21657 extra_variables: HashMap::default(),
21658 context_range: BufferOffset(43)..BufferOffset(85),
21659 },
21660 );
21661 editor.tasks.insert(
21662 (buffer.read(cx).remote_id(), 8),
21663 RunnableTasks {
21664 templates: vec![],
21665 offset: snapshot.anchor_before(86),
21666 column: 0,
21667 extra_variables: HashMap::default(),
21668 context_range: BufferOffset(86)..BufferOffset(191),
21669 },
21670 );
21671
21672 // Test finding task when cursor is inside function body
21673 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21674 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
21675 });
21676 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21677 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
21678
21679 // Test finding task when cursor is on function name
21680 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21681 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
21682 });
21683 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
21684 assert_eq!(row, 8, "Should find task when cursor is on function name");
21685 });
21686}
21687
21688#[gpui::test]
21689async fn test_folding_buffers(cx: &mut TestAppContext) {
21690 init_test(cx, |_| {});
21691
21692 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
21693 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
21694 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
21695
21696 let fs = FakeFs::new(cx.executor());
21697 fs.insert_tree(
21698 path!("/a"),
21699 json!({
21700 "first.rs": sample_text_1,
21701 "second.rs": sample_text_2,
21702 "third.rs": sample_text_3,
21703 }),
21704 )
21705 .await;
21706 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21707 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21708 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21709 let worktree = project.update(cx, |project, cx| {
21710 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21711 assert_eq!(worktrees.len(), 1);
21712 worktrees.pop().unwrap()
21713 });
21714 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21715
21716 let buffer_1 = project
21717 .update(cx, |project, cx| {
21718 project.open_buffer((worktree_id, "first.rs"), cx)
21719 })
21720 .await
21721 .unwrap();
21722 let buffer_2 = project
21723 .update(cx, |project, cx| {
21724 project.open_buffer((worktree_id, "second.rs"), cx)
21725 })
21726 .await
21727 .unwrap();
21728 let buffer_3 = project
21729 .update(cx, |project, cx| {
21730 project.open_buffer((worktree_id, "third.rs"), cx)
21731 })
21732 .await
21733 .unwrap();
21734
21735 let multi_buffer = cx.new(|cx| {
21736 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21737 multi_buffer.push_excerpts(
21738 buffer_1.clone(),
21739 [
21740 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21741 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21742 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21743 ],
21744 cx,
21745 );
21746 multi_buffer.push_excerpts(
21747 buffer_2.clone(),
21748 [
21749 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21750 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21751 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21752 ],
21753 cx,
21754 );
21755 multi_buffer.push_excerpts(
21756 buffer_3.clone(),
21757 [
21758 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
21759 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
21760 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
21761 ],
21762 cx,
21763 );
21764 multi_buffer
21765 });
21766 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21767 Editor::new(
21768 EditorMode::full(),
21769 multi_buffer.clone(),
21770 Some(project.clone()),
21771 window,
21772 cx,
21773 )
21774 });
21775
21776 assert_eq!(
21777 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21778 "\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",
21779 );
21780
21781 multi_buffer_editor.update(cx, |editor, cx| {
21782 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21783 });
21784 assert_eq!(
21785 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21786 "\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",
21787 "After folding the first buffer, its text should not be displayed"
21788 );
21789
21790 multi_buffer_editor.update(cx, |editor, cx| {
21791 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21792 });
21793 assert_eq!(
21794 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21795 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
21796 "After folding the second buffer, its text should not be displayed"
21797 );
21798
21799 multi_buffer_editor.update(cx, |editor, cx| {
21800 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21801 });
21802 assert_eq!(
21803 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21804 "\n\n\n\n\n",
21805 "After folding the third buffer, its text should not be displayed"
21806 );
21807
21808 // Emulate selection inside the fold logic, that should work
21809 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21810 editor
21811 .snapshot(window, cx)
21812 .next_line_boundary(Point::new(0, 4));
21813 });
21814
21815 multi_buffer_editor.update(cx, |editor, cx| {
21816 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21817 });
21818 assert_eq!(
21819 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21820 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21821 "After unfolding the second buffer, its text should be displayed"
21822 );
21823
21824 // Typing inside of buffer 1 causes that buffer to be unfolded.
21825 multi_buffer_editor.update_in(cx, |editor, window, cx| {
21826 assert_eq!(
21827 multi_buffer
21828 .read(cx)
21829 .snapshot(cx)
21830 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
21831 .collect::<String>(),
21832 "bbbb"
21833 );
21834 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
21835 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
21836 });
21837 editor.handle_input("B", window, cx);
21838 });
21839
21840 assert_eq!(
21841 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21842 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
21843 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
21844 );
21845
21846 multi_buffer_editor.update(cx, |editor, cx| {
21847 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21848 });
21849 assert_eq!(
21850 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21851 "\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",
21852 "After unfolding the all buffers, all original text should be displayed"
21853 );
21854}
21855
21856#[gpui::test]
21857async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
21858 init_test(cx, |_| {});
21859
21860 let sample_text_1 = "1111\n2222\n3333".to_string();
21861 let sample_text_2 = "4444\n5555\n6666".to_string();
21862 let sample_text_3 = "7777\n8888\n9999".to_string();
21863
21864 let fs = FakeFs::new(cx.executor());
21865 fs.insert_tree(
21866 path!("/a"),
21867 json!({
21868 "first.rs": sample_text_1,
21869 "second.rs": sample_text_2,
21870 "third.rs": sample_text_3,
21871 }),
21872 )
21873 .await;
21874 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
21875 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
21876 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
21877 let worktree = project.update(cx, |project, cx| {
21878 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
21879 assert_eq!(worktrees.len(), 1);
21880 worktrees.pop().unwrap()
21881 });
21882 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
21883
21884 let buffer_1 = project
21885 .update(cx, |project, cx| {
21886 project.open_buffer((worktree_id, "first.rs"), cx)
21887 })
21888 .await
21889 .unwrap();
21890 let buffer_2 = project
21891 .update(cx, |project, cx| {
21892 project.open_buffer((worktree_id, "second.rs"), cx)
21893 })
21894 .await
21895 .unwrap();
21896 let buffer_3 = project
21897 .update(cx, |project, cx| {
21898 project.open_buffer((worktree_id, "third.rs"), cx)
21899 })
21900 .await
21901 .unwrap();
21902
21903 let multi_buffer = cx.new(|cx| {
21904 let mut multi_buffer = MultiBuffer::new(ReadWrite);
21905 multi_buffer.push_excerpts(
21906 buffer_1.clone(),
21907 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21908 cx,
21909 );
21910 multi_buffer.push_excerpts(
21911 buffer_2.clone(),
21912 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21913 cx,
21914 );
21915 multi_buffer.push_excerpts(
21916 buffer_3.clone(),
21917 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
21918 cx,
21919 );
21920 multi_buffer
21921 });
21922
21923 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
21924 Editor::new(
21925 EditorMode::full(),
21926 multi_buffer,
21927 Some(project.clone()),
21928 window,
21929 cx,
21930 )
21931 });
21932
21933 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
21934 assert_eq!(
21935 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21936 full_text,
21937 );
21938
21939 multi_buffer_editor.update(cx, |editor, cx| {
21940 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
21941 });
21942 assert_eq!(
21943 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21944 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
21945 "After folding the first buffer, its text should not be displayed"
21946 );
21947
21948 multi_buffer_editor.update(cx, |editor, cx| {
21949 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
21950 });
21951
21952 assert_eq!(
21953 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21954 "\n\n\n\n\n\n7777\n8888\n9999",
21955 "After folding the second buffer, its text should not be displayed"
21956 );
21957
21958 multi_buffer_editor.update(cx, |editor, cx| {
21959 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
21960 });
21961 assert_eq!(
21962 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21963 "\n\n\n\n\n",
21964 "After folding the third buffer, its text should not be displayed"
21965 );
21966
21967 multi_buffer_editor.update(cx, |editor, cx| {
21968 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
21969 });
21970 assert_eq!(
21971 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21972 "\n\n\n\n4444\n5555\n6666\n\n",
21973 "After unfolding the second buffer, its text should be displayed"
21974 );
21975
21976 multi_buffer_editor.update(cx, |editor, cx| {
21977 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
21978 });
21979 assert_eq!(
21980 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21981 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
21982 "After unfolding the first buffer, its text should be displayed"
21983 );
21984
21985 multi_buffer_editor.update(cx, |editor, cx| {
21986 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
21987 });
21988 assert_eq!(
21989 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
21990 full_text,
21991 "After unfolding all buffers, all original text should be displayed"
21992 );
21993}
21994
21995#[gpui::test]
21996async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
21997 init_test(cx, |_| {});
21998
21999 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22000
22001 let fs = FakeFs::new(cx.executor());
22002 fs.insert_tree(
22003 path!("/a"),
22004 json!({
22005 "main.rs": sample_text,
22006 }),
22007 )
22008 .await;
22009 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22010 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22011 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22012 let worktree = project.update(cx, |project, cx| {
22013 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22014 assert_eq!(worktrees.len(), 1);
22015 worktrees.pop().unwrap()
22016 });
22017 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22018
22019 let buffer_1 = project
22020 .update(cx, |project, cx| {
22021 project.open_buffer((worktree_id, "main.rs"), cx)
22022 })
22023 .await
22024 .unwrap();
22025
22026 let multi_buffer = cx.new(|cx| {
22027 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22028 multi_buffer.push_excerpts(
22029 buffer_1.clone(),
22030 [ExcerptRange::new(
22031 Point::new(0, 0)
22032 ..Point::new(
22033 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22034 0,
22035 ),
22036 )],
22037 cx,
22038 );
22039 multi_buffer
22040 });
22041 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22042 Editor::new(
22043 EditorMode::full(),
22044 multi_buffer,
22045 Some(project.clone()),
22046 window,
22047 cx,
22048 )
22049 });
22050
22051 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22052 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22053 enum TestHighlight {}
22054 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22055 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22056 editor.highlight_text::<TestHighlight>(
22057 vec![highlight_range.clone()],
22058 HighlightStyle::color(Hsla::green()),
22059 cx,
22060 );
22061 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22062 s.select_ranges(Some(highlight_range))
22063 });
22064 });
22065
22066 let full_text = format!("\n\n{sample_text}");
22067 assert_eq!(
22068 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22069 full_text,
22070 );
22071}
22072
22073#[gpui::test]
22074async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22075 init_test(cx, |_| {});
22076 cx.update(|cx| {
22077 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22078 "keymaps/default-linux.json",
22079 cx,
22080 )
22081 .unwrap();
22082 cx.bind_keys(default_key_bindings);
22083 });
22084
22085 let (editor, cx) = cx.add_window_view(|window, cx| {
22086 let multi_buffer = MultiBuffer::build_multi(
22087 [
22088 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22089 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22090 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22091 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22092 ],
22093 cx,
22094 );
22095 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22096
22097 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22098 // fold all but the second buffer, so that we test navigating between two
22099 // adjacent folded buffers, as well as folded buffers at the start and
22100 // end the multibuffer
22101 editor.fold_buffer(buffer_ids[0], cx);
22102 editor.fold_buffer(buffer_ids[2], cx);
22103 editor.fold_buffer(buffer_ids[3], cx);
22104
22105 editor
22106 });
22107 cx.simulate_resize(size(px(1000.), px(1000.)));
22108
22109 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22110 cx.assert_excerpts_with_selections(indoc! {"
22111 [EXCERPT]
22112 ˇ[FOLDED]
22113 [EXCERPT]
22114 a1
22115 b1
22116 [EXCERPT]
22117 [FOLDED]
22118 [EXCERPT]
22119 [FOLDED]
22120 "
22121 });
22122 cx.simulate_keystroke("down");
22123 cx.assert_excerpts_with_selections(indoc! {"
22124 [EXCERPT]
22125 [FOLDED]
22126 [EXCERPT]
22127 ˇa1
22128 b1
22129 [EXCERPT]
22130 [FOLDED]
22131 [EXCERPT]
22132 [FOLDED]
22133 "
22134 });
22135 cx.simulate_keystroke("down");
22136 cx.assert_excerpts_with_selections(indoc! {"
22137 [EXCERPT]
22138 [FOLDED]
22139 [EXCERPT]
22140 a1
22141 ˇb1
22142 [EXCERPT]
22143 [FOLDED]
22144 [EXCERPT]
22145 [FOLDED]
22146 "
22147 });
22148 cx.simulate_keystroke("down");
22149 cx.assert_excerpts_with_selections(indoc! {"
22150 [EXCERPT]
22151 [FOLDED]
22152 [EXCERPT]
22153 a1
22154 b1
22155 ˇ[EXCERPT]
22156 [FOLDED]
22157 [EXCERPT]
22158 [FOLDED]
22159 "
22160 });
22161 cx.simulate_keystroke("down");
22162 cx.assert_excerpts_with_selections(indoc! {"
22163 [EXCERPT]
22164 [FOLDED]
22165 [EXCERPT]
22166 a1
22167 b1
22168 [EXCERPT]
22169 ˇ[FOLDED]
22170 [EXCERPT]
22171 [FOLDED]
22172 "
22173 });
22174 for _ in 0..5 {
22175 cx.simulate_keystroke("down");
22176 cx.assert_excerpts_with_selections(indoc! {"
22177 [EXCERPT]
22178 [FOLDED]
22179 [EXCERPT]
22180 a1
22181 b1
22182 [EXCERPT]
22183 [FOLDED]
22184 [EXCERPT]
22185 ˇ[FOLDED]
22186 "
22187 });
22188 }
22189
22190 cx.simulate_keystroke("up");
22191 cx.assert_excerpts_with_selections(indoc! {"
22192 [EXCERPT]
22193 [FOLDED]
22194 [EXCERPT]
22195 a1
22196 b1
22197 [EXCERPT]
22198 ˇ[FOLDED]
22199 [EXCERPT]
22200 [FOLDED]
22201 "
22202 });
22203 cx.simulate_keystroke("up");
22204 cx.assert_excerpts_with_selections(indoc! {"
22205 [EXCERPT]
22206 [FOLDED]
22207 [EXCERPT]
22208 a1
22209 b1
22210 ˇ[EXCERPT]
22211 [FOLDED]
22212 [EXCERPT]
22213 [FOLDED]
22214 "
22215 });
22216 cx.simulate_keystroke("up");
22217 cx.assert_excerpts_with_selections(indoc! {"
22218 [EXCERPT]
22219 [FOLDED]
22220 [EXCERPT]
22221 a1
22222 ˇb1
22223 [EXCERPT]
22224 [FOLDED]
22225 [EXCERPT]
22226 [FOLDED]
22227 "
22228 });
22229 cx.simulate_keystroke("up");
22230 cx.assert_excerpts_with_selections(indoc! {"
22231 [EXCERPT]
22232 [FOLDED]
22233 [EXCERPT]
22234 ˇa1
22235 b1
22236 [EXCERPT]
22237 [FOLDED]
22238 [EXCERPT]
22239 [FOLDED]
22240 "
22241 });
22242 for _ in 0..5 {
22243 cx.simulate_keystroke("up");
22244 cx.assert_excerpts_with_selections(indoc! {"
22245 [EXCERPT]
22246 ˇ[FOLDED]
22247 [EXCERPT]
22248 a1
22249 b1
22250 [EXCERPT]
22251 [FOLDED]
22252 [EXCERPT]
22253 [FOLDED]
22254 "
22255 });
22256 }
22257}
22258
22259#[gpui::test]
22260async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22261 init_test(cx, |_| {});
22262
22263 // Simple insertion
22264 assert_highlighted_edits(
22265 "Hello, world!",
22266 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22267 true,
22268 cx,
22269 |highlighted_edits, cx| {
22270 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22271 assert_eq!(highlighted_edits.highlights.len(), 1);
22272 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22273 assert_eq!(
22274 highlighted_edits.highlights[0].1.background_color,
22275 Some(cx.theme().status().created_background)
22276 );
22277 },
22278 )
22279 .await;
22280
22281 // Replacement
22282 assert_highlighted_edits(
22283 "This is a test.",
22284 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22285 false,
22286 cx,
22287 |highlighted_edits, cx| {
22288 assert_eq!(highlighted_edits.text, "That is a test.");
22289 assert_eq!(highlighted_edits.highlights.len(), 1);
22290 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22291 assert_eq!(
22292 highlighted_edits.highlights[0].1.background_color,
22293 Some(cx.theme().status().created_background)
22294 );
22295 },
22296 )
22297 .await;
22298
22299 // Multiple edits
22300 assert_highlighted_edits(
22301 "Hello, world!",
22302 vec![
22303 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22304 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22305 ],
22306 false,
22307 cx,
22308 |highlighted_edits, cx| {
22309 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22310 assert_eq!(highlighted_edits.highlights.len(), 2);
22311 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22312 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22313 assert_eq!(
22314 highlighted_edits.highlights[0].1.background_color,
22315 Some(cx.theme().status().created_background)
22316 );
22317 assert_eq!(
22318 highlighted_edits.highlights[1].1.background_color,
22319 Some(cx.theme().status().created_background)
22320 );
22321 },
22322 )
22323 .await;
22324
22325 // Multiple lines with edits
22326 assert_highlighted_edits(
22327 "First line\nSecond line\nThird line\nFourth line",
22328 vec![
22329 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22330 (
22331 Point::new(2, 0)..Point::new(2, 10),
22332 "New third line".to_string(),
22333 ),
22334 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22335 ],
22336 false,
22337 cx,
22338 |highlighted_edits, cx| {
22339 assert_eq!(
22340 highlighted_edits.text,
22341 "Second modified\nNew third line\nFourth updated line"
22342 );
22343 assert_eq!(highlighted_edits.highlights.len(), 3);
22344 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22345 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22346 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22347 for highlight in &highlighted_edits.highlights {
22348 assert_eq!(
22349 highlight.1.background_color,
22350 Some(cx.theme().status().created_background)
22351 );
22352 }
22353 },
22354 )
22355 .await;
22356}
22357
22358#[gpui::test]
22359async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22360 init_test(cx, |_| {});
22361
22362 // Deletion
22363 assert_highlighted_edits(
22364 "Hello, world!",
22365 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22366 true,
22367 cx,
22368 |highlighted_edits, cx| {
22369 assert_eq!(highlighted_edits.text, "Hello, world!");
22370 assert_eq!(highlighted_edits.highlights.len(), 1);
22371 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22372 assert_eq!(
22373 highlighted_edits.highlights[0].1.background_color,
22374 Some(cx.theme().status().deleted_background)
22375 );
22376 },
22377 )
22378 .await;
22379
22380 // Insertion
22381 assert_highlighted_edits(
22382 "Hello, world!",
22383 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22384 true,
22385 cx,
22386 |highlighted_edits, cx| {
22387 assert_eq!(highlighted_edits.highlights.len(), 1);
22388 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22389 assert_eq!(
22390 highlighted_edits.highlights[0].1.background_color,
22391 Some(cx.theme().status().created_background)
22392 );
22393 },
22394 )
22395 .await;
22396}
22397
22398async fn assert_highlighted_edits(
22399 text: &str,
22400 edits: Vec<(Range<Point>, String)>,
22401 include_deletions: bool,
22402 cx: &mut TestAppContext,
22403 assertion_fn: impl Fn(HighlightedText, &App),
22404) {
22405 let window = cx.add_window(|window, cx| {
22406 let buffer = MultiBuffer::build_simple(text, cx);
22407 Editor::new(EditorMode::full(), buffer, None, window, cx)
22408 });
22409 let cx = &mut VisualTestContext::from_window(*window, cx);
22410
22411 let (buffer, snapshot) = window
22412 .update(cx, |editor, _window, cx| {
22413 (
22414 editor.buffer().clone(),
22415 editor.buffer().read(cx).snapshot(cx),
22416 )
22417 })
22418 .unwrap();
22419
22420 let edits = edits
22421 .into_iter()
22422 .map(|(range, edit)| {
22423 (
22424 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22425 edit,
22426 )
22427 })
22428 .collect::<Vec<_>>();
22429
22430 let text_anchor_edits = edits
22431 .clone()
22432 .into_iter()
22433 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
22434 .collect::<Vec<_>>();
22435
22436 let edit_preview = window
22437 .update(cx, |_, _window, cx| {
22438 buffer
22439 .read(cx)
22440 .as_singleton()
22441 .unwrap()
22442 .read(cx)
22443 .preview_edits(text_anchor_edits.into(), cx)
22444 })
22445 .unwrap()
22446 .await;
22447
22448 cx.update(|_window, cx| {
22449 let highlighted_edits = edit_prediction_edit_text(
22450 snapshot.as_singleton().unwrap().2,
22451 &edits,
22452 &edit_preview,
22453 include_deletions,
22454 cx,
22455 );
22456 assertion_fn(highlighted_edits, cx)
22457 });
22458}
22459
22460#[track_caller]
22461fn assert_breakpoint(
22462 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22463 path: &Arc<Path>,
22464 expected: Vec<(u32, Breakpoint)>,
22465) {
22466 if expected.is_empty() {
22467 assert!(!breakpoints.contains_key(path), "{}", path.display());
22468 } else {
22469 let mut breakpoint = breakpoints
22470 .get(path)
22471 .unwrap()
22472 .iter()
22473 .map(|breakpoint| {
22474 (
22475 breakpoint.row,
22476 Breakpoint {
22477 message: breakpoint.message.clone(),
22478 state: breakpoint.state,
22479 condition: breakpoint.condition.clone(),
22480 hit_condition: breakpoint.hit_condition.clone(),
22481 },
22482 )
22483 })
22484 .collect::<Vec<_>>();
22485
22486 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22487
22488 assert_eq!(expected, breakpoint);
22489 }
22490}
22491
22492fn add_log_breakpoint_at_cursor(
22493 editor: &mut Editor,
22494 log_message: &str,
22495 window: &mut Window,
22496 cx: &mut Context<Editor>,
22497) {
22498 let (anchor, bp) = editor
22499 .breakpoints_at_cursors(window, cx)
22500 .first()
22501 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22502 .unwrap_or_else(|| {
22503 let cursor_position: Point = editor.selections.newest(cx).head();
22504
22505 let breakpoint_position = editor
22506 .snapshot(window, cx)
22507 .display_snapshot
22508 .buffer_snapshot
22509 .anchor_before(Point::new(cursor_position.row, 0));
22510
22511 (breakpoint_position, Breakpoint::new_log(log_message))
22512 });
22513
22514 editor.edit_breakpoint_at_anchor(
22515 anchor,
22516 bp,
22517 BreakpointEditAction::EditLogMessage(log_message.into()),
22518 cx,
22519 );
22520}
22521
22522#[gpui::test]
22523async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22524 init_test(cx, |_| {});
22525
22526 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22527 let fs = FakeFs::new(cx.executor());
22528 fs.insert_tree(
22529 path!("/a"),
22530 json!({
22531 "main.rs": sample_text,
22532 }),
22533 )
22534 .await;
22535 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22536 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22537 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22538
22539 let fs = FakeFs::new(cx.executor());
22540 fs.insert_tree(
22541 path!("/a"),
22542 json!({
22543 "main.rs": sample_text,
22544 }),
22545 )
22546 .await;
22547 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22548 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22549 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22550 let worktree_id = workspace
22551 .update(cx, |workspace, _window, cx| {
22552 workspace.project().update(cx, |project, cx| {
22553 project.worktrees(cx).next().unwrap().read(cx).id()
22554 })
22555 })
22556 .unwrap();
22557
22558 let buffer = project
22559 .update(cx, |project, cx| {
22560 project.open_buffer((worktree_id, "main.rs"), cx)
22561 })
22562 .await
22563 .unwrap();
22564
22565 let (editor, cx) = cx.add_window_view(|window, cx| {
22566 Editor::new(
22567 EditorMode::full(),
22568 MultiBuffer::build_from_buffer(buffer, cx),
22569 Some(project.clone()),
22570 window,
22571 cx,
22572 )
22573 });
22574
22575 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22576 let abs_path = project.read_with(cx, |project, cx| {
22577 project
22578 .absolute_path(&project_path, cx)
22579 .map(Arc::from)
22580 .unwrap()
22581 });
22582
22583 // assert we can add breakpoint on the first line
22584 editor.update_in(cx, |editor, window, cx| {
22585 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22586 editor.move_to_end(&MoveToEnd, window, cx);
22587 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22588 });
22589
22590 let breakpoints = editor.update(cx, |editor, cx| {
22591 editor
22592 .breakpoint_store()
22593 .as_ref()
22594 .unwrap()
22595 .read(cx)
22596 .all_source_breakpoints(cx)
22597 });
22598
22599 assert_eq!(1, breakpoints.len());
22600 assert_breakpoint(
22601 &breakpoints,
22602 &abs_path,
22603 vec![
22604 (0, Breakpoint::new_standard()),
22605 (3, Breakpoint::new_standard()),
22606 ],
22607 );
22608
22609 editor.update_in(cx, |editor, window, cx| {
22610 editor.move_to_beginning(&MoveToBeginning, window, cx);
22611 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22612 });
22613
22614 let breakpoints = editor.update(cx, |editor, cx| {
22615 editor
22616 .breakpoint_store()
22617 .as_ref()
22618 .unwrap()
22619 .read(cx)
22620 .all_source_breakpoints(cx)
22621 });
22622
22623 assert_eq!(1, breakpoints.len());
22624 assert_breakpoint(
22625 &breakpoints,
22626 &abs_path,
22627 vec![(3, Breakpoint::new_standard())],
22628 );
22629
22630 editor.update_in(cx, |editor, window, cx| {
22631 editor.move_to_end(&MoveToEnd, window, cx);
22632 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22633 });
22634
22635 let breakpoints = editor.update(cx, |editor, cx| {
22636 editor
22637 .breakpoint_store()
22638 .as_ref()
22639 .unwrap()
22640 .read(cx)
22641 .all_source_breakpoints(cx)
22642 });
22643
22644 assert_eq!(0, breakpoints.len());
22645 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22646}
22647
22648#[gpui::test]
22649async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
22650 init_test(cx, |_| {});
22651
22652 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22653
22654 let fs = FakeFs::new(cx.executor());
22655 fs.insert_tree(
22656 path!("/a"),
22657 json!({
22658 "main.rs": sample_text,
22659 }),
22660 )
22661 .await;
22662 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22663 let (workspace, cx) =
22664 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
22665
22666 let worktree_id = workspace.update(cx, |workspace, cx| {
22667 workspace.project().update(cx, |project, cx| {
22668 project.worktrees(cx).next().unwrap().read(cx).id()
22669 })
22670 });
22671
22672 let buffer = project
22673 .update(cx, |project, cx| {
22674 project.open_buffer((worktree_id, "main.rs"), cx)
22675 })
22676 .await
22677 .unwrap();
22678
22679 let (editor, cx) = cx.add_window_view(|window, cx| {
22680 Editor::new(
22681 EditorMode::full(),
22682 MultiBuffer::build_from_buffer(buffer, cx),
22683 Some(project.clone()),
22684 window,
22685 cx,
22686 )
22687 });
22688
22689 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22690 let abs_path = project.read_with(cx, |project, cx| {
22691 project
22692 .absolute_path(&project_path, cx)
22693 .map(Arc::from)
22694 .unwrap()
22695 });
22696
22697 editor.update_in(cx, |editor, window, cx| {
22698 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22699 });
22700
22701 let breakpoints = editor.update(cx, |editor, cx| {
22702 editor
22703 .breakpoint_store()
22704 .as_ref()
22705 .unwrap()
22706 .read(cx)
22707 .all_source_breakpoints(cx)
22708 });
22709
22710 assert_breakpoint(
22711 &breakpoints,
22712 &abs_path,
22713 vec![(0, Breakpoint::new_log("hello world"))],
22714 );
22715
22716 // Removing a log message from a log breakpoint should remove it
22717 editor.update_in(cx, |editor, window, cx| {
22718 add_log_breakpoint_at_cursor(editor, "", window, cx);
22719 });
22720
22721 let breakpoints = editor.update(cx, |editor, cx| {
22722 editor
22723 .breakpoint_store()
22724 .as_ref()
22725 .unwrap()
22726 .read(cx)
22727 .all_source_breakpoints(cx)
22728 });
22729
22730 assert_breakpoint(&breakpoints, &abs_path, vec![]);
22731
22732 editor.update_in(cx, |editor, window, cx| {
22733 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22734 editor.move_to_end(&MoveToEnd, window, cx);
22735 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22736 // Not adding a log message to a standard breakpoint shouldn't remove it
22737 add_log_breakpoint_at_cursor(editor, "", window, cx);
22738 });
22739
22740 let breakpoints = editor.update(cx, |editor, cx| {
22741 editor
22742 .breakpoint_store()
22743 .as_ref()
22744 .unwrap()
22745 .read(cx)
22746 .all_source_breakpoints(cx)
22747 });
22748
22749 assert_breakpoint(
22750 &breakpoints,
22751 &abs_path,
22752 vec![
22753 (0, Breakpoint::new_standard()),
22754 (3, Breakpoint::new_standard()),
22755 ],
22756 );
22757
22758 editor.update_in(cx, |editor, window, cx| {
22759 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
22760 });
22761
22762 let breakpoints = editor.update(cx, |editor, cx| {
22763 editor
22764 .breakpoint_store()
22765 .as_ref()
22766 .unwrap()
22767 .read(cx)
22768 .all_source_breakpoints(cx)
22769 });
22770
22771 assert_breakpoint(
22772 &breakpoints,
22773 &abs_path,
22774 vec![
22775 (0, Breakpoint::new_standard()),
22776 (3, Breakpoint::new_log("hello world")),
22777 ],
22778 );
22779
22780 editor.update_in(cx, |editor, window, cx| {
22781 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
22782 });
22783
22784 let breakpoints = editor.update(cx, |editor, cx| {
22785 editor
22786 .breakpoint_store()
22787 .as_ref()
22788 .unwrap()
22789 .read(cx)
22790 .all_source_breakpoints(cx)
22791 });
22792
22793 assert_breakpoint(
22794 &breakpoints,
22795 &abs_path,
22796 vec![
22797 (0, Breakpoint::new_standard()),
22798 (3, Breakpoint::new_log("hello Earth!!")),
22799 ],
22800 );
22801}
22802
22803/// This also tests that Editor::breakpoint_at_cursor_head is working properly
22804/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
22805/// or when breakpoints were placed out of order. This tests for a regression too
22806#[gpui::test]
22807async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
22808 init_test(cx, |_| {});
22809
22810 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22811 let fs = FakeFs::new(cx.executor());
22812 fs.insert_tree(
22813 path!("/a"),
22814 json!({
22815 "main.rs": sample_text,
22816 }),
22817 )
22818 .await;
22819 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22820 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22821 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22822
22823 let fs = FakeFs::new(cx.executor());
22824 fs.insert_tree(
22825 path!("/a"),
22826 json!({
22827 "main.rs": sample_text,
22828 }),
22829 )
22830 .await;
22831 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22832 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22833 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22834 let worktree_id = workspace
22835 .update(cx, |workspace, _window, cx| {
22836 workspace.project().update(cx, |project, cx| {
22837 project.worktrees(cx).next().unwrap().read(cx).id()
22838 })
22839 })
22840 .unwrap();
22841
22842 let buffer = project
22843 .update(cx, |project, cx| {
22844 project.open_buffer((worktree_id, "main.rs"), cx)
22845 })
22846 .await
22847 .unwrap();
22848
22849 let (editor, cx) = cx.add_window_view(|window, cx| {
22850 Editor::new(
22851 EditorMode::full(),
22852 MultiBuffer::build_from_buffer(buffer, cx),
22853 Some(project.clone()),
22854 window,
22855 cx,
22856 )
22857 });
22858
22859 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
22860 let abs_path = project.read_with(cx, |project, cx| {
22861 project
22862 .absolute_path(&project_path, cx)
22863 .map(Arc::from)
22864 .unwrap()
22865 });
22866
22867 // assert we can add breakpoint on the first line
22868 editor.update_in(cx, |editor, window, cx| {
22869 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22870 editor.move_to_end(&MoveToEnd, window, cx);
22871 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22872 editor.move_up(&MoveUp, window, cx);
22873 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
22874 });
22875
22876 let breakpoints = editor.update(cx, |editor, cx| {
22877 editor
22878 .breakpoint_store()
22879 .as_ref()
22880 .unwrap()
22881 .read(cx)
22882 .all_source_breakpoints(cx)
22883 });
22884
22885 assert_eq!(1, breakpoints.len());
22886 assert_breakpoint(
22887 &breakpoints,
22888 &abs_path,
22889 vec![
22890 (0, Breakpoint::new_standard()),
22891 (2, Breakpoint::new_standard()),
22892 (3, Breakpoint::new_standard()),
22893 ],
22894 );
22895
22896 editor.update_in(cx, |editor, window, cx| {
22897 editor.move_to_beginning(&MoveToBeginning, window, cx);
22898 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22899 editor.move_to_end(&MoveToEnd, window, cx);
22900 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22901 // Disabling a breakpoint that doesn't exist should do nothing
22902 editor.move_up(&MoveUp, window, cx);
22903 editor.move_up(&MoveUp, window, cx);
22904 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22905 });
22906
22907 let breakpoints = editor.update(cx, |editor, cx| {
22908 editor
22909 .breakpoint_store()
22910 .as_ref()
22911 .unwrap()
22912 .read(cx)
22913 .all_source_breakpoints(cx)
22914 });
22915
22916 let disable_breakpoint = {
22917 let mut bp = Breakpoint::new_standard();
22918 bp.state = BreakpointState::Disabled;
22919 bp
22920 };
22921
22922 assert_eq!(1, breakpoints.len());
22923 assert_breakpoint(
22924 &breakpoints,
22925 &abs_path,
22926 vec![
22927 (0, disable_breakpoint.clone()),
22928 (2, Breakpoint::new_standard()),
22929 (3, disable_breakpoint.clone()),
22930 ],
22931 );
22932
22933 editor.update_in(cx, |editor, window, cx| {
22934 editor.move_to_beginning(&MoveToBeginning, window, cx);
22935 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22936 editor.move_to_end(&MoveToEnd, window, cx);
22937 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
22938 editor.move_up(&MoveUp, window, cx);
22939 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
22940 });
22941
22942 let breakpoints = editor.update(cx, |editor, cx| {
22943 editor
22944 .breakpoint_store()
22945 .as_ref()
22946 .unwrap()
22947 .read(cx)
22948 .all_source_breakpoints(cx)
22949 });
22950
22951 assert_eq!(1, breakpoints.len());
22952 assert_breakpoint(
22953 &breakpoints,
22954 &abs_path,
22955 vec![
22956 (0, Breakpoint::new_standard()),
22957 (2, disable_breakpoint),
22958 (3, Breakpoint::new_standard()),
22959 ],
22960 );
22961}
22962
22963#[gpui::test]
22964async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
22965 init_test(cx, |_| {});
22966 let capabilities = lsp::ServerCapabilities {
22967 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
22968 prepare_provider: Some(true),
22969 work_done_progress_options: Default::default(),
22970 })),
22971 ..Default::default()
22972 };
22973 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
22974
22975 cx.set_state(indoc! {"
22976 struct Fˇoo {}
22977 "});
22978
22979 cx.update_editor(|editor, _, cx| {
22980 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
22981 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
22982 editor.highlight_background::<DocumentHighlightRead>(
22983 &[highlight_range],
22984 |theme| theme.colors().editor_document_highlight_read_background,
22985 cx,
22986 );
22987 });
22988
22989 let mut prepare_rename_handler = cx
22990 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
22991 move |_, _, _| async move {
22992 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
22993 start: lsp::Position {
22994 line: 0,
22995 character: 7,
22996 },
22997 end: lsp::Position {
22998 line: 0,
22999 character: 10,
23000 },
23001 })))
23002 },
23003 );
23004 let prepare_rename_task = cx
23005 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23006 .expect("Prepare rename was not started");
23007 prepare_rename_handler.next().await.unwrap();
23008 prepare_rename_task.await.expect("Prepare rename failed");
23009
23010 let mut rename_handler =
23011 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23012 let edit = lsp::TextEdit {
23013 range: lsp::Range {
23014 start: lsp::Position {
23015 line: 0,
23016 character: 7,
23017 },
23018 end: lsp::Position {
23019 line: 0,
23020 character: 10,
23021 },
23022 },
23023 new_text: "FooRenamed".to_string(),
23024 };
23025 Ok(Some(lsp::WorkspaceEdit::new(
23026 // Specify the same edit twice
23027 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23028 )))
23029 });
23030 let rename_task = cx
23031 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23032 .expect("Confirm rename was not started");
23033 rename_handler.next().await.unwrap();
23034 rename_task.await.expect("Confirm rename failed");
23035 cx.run_until_parked();
23036
23037 // Despite two edits, only one is actually applied as those are identical
23038 cx.assert_editor_state(indoc! {"
23039 struct FooRenamedˇ {}
23040 "});
23041}
23042
23043#[gpui::test]
23044async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23045 init_test(cx, |_| {});
23046 // These capabilities indicate that the server does not support prepare rename.
23047 let capabilities = lsp::ServerCapabilities {
23048 rename_provider: Some(lsp::OneOf::Left(true)),
23049 ..Default::default()
23050 };
23051 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23052
23053 cx.set_state(indoc! {"
23054 struct Fˇoo {}
23055 "});
23056
23057 cx.update_editor(|editor, _window, cx| {
23058 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23059 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23060 editor.highlight_background::<DocumentHighlightRead>(
23061 &[highlight_range],
23062 |theme| theme.colors().editor_document_highlight_read_background,
23063 cx,
23064 );
23065 });
23066
23067 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23068 .expect("Prepare rename was not started")
23069 .await
23070 .expect("Prepare rename failed");
23071
23072 let mut rename_handler =
23073 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23074 let edit = lsp::TextEdit {
23075 range: lsp::Range {
23076 start: lsp::Position {
23077 line: 0,
23078 character: 7,
23079 },
23080 end: lsp::Position {
23081 line: 0,
23082 character: 10,
23083 },
23084 },
23085 new_text: "FooRenamed".to_string(),
23086 };
23087 Ok(Some(lsp::WorkspaceEdit::new(
23088 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23089 )))
23090 });
23091 let rename_task = cx
23092 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23093 .expect("Confirm rename was not started");
23094 rename_handler.next().await.unwrap();
23095 rename_task.await.expect("Confirm rename failed");
23096 cx.run_until_parked();
23097
23098 // Correct range is renamed, as `surrounding_word` is used to find it.
23099 cx.assert_editor_state(indoc! {"
23100 struct FooRenamedˇ {}
23101 "});
23102}
23103
23104#[gpui::test]
23105async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23106 init_test(cx, |_| {});
23107 let mut cx = EditorTestContext::new(cx).await;
23108
23109 let language = Arc::new(
23110 Language::new(
23111 LanguageConfig::default(),
23112 Some(tree_sitter_html::LANGUAGE.into()),
23113 )
23114 .with_brackets_query(
23115 r#"
23116 ("<" @open "/>" @close)
23117 ("</" @open ">" @close)
23118 ("<" @open ">" @close)
23119 ("\"" @open "\"" @close)
23120 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23121 "#,
23122 )
23123 .unwrap(),
23124 );
23125 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23126
23127 cx.set_state(indoc! {"
23128 <span>ˇ</span>
23129 "});
23130 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23131 cx.assert_editor_state(indoc! {"
23132 <span>
23133 ˇ
23134 </span>
23135 "});
23136
23137 cx.set_state(indoc! {"
23138 <span><span></span>ˇ</span>
23139 "});
23140 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23141 cx.assert_editor_state(indoc! {"
23142 <span><span></span>
23143 ˇ</span>
23144 "});
23145
23146 cx.set_state(indoc! {"
23147 <span>ˇ
23148 </span>
23149 "});
23150 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23151 cx.assert_editor_state(indoc! {"
23152 <span>
23153 ˇ
23154 </span>
23155 "});
23156}
23157
23158#[gpui::test(iterations = 10)]
23159async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23160 init_test(cx, |_| {});
23161
23162 let fs = FakeFs::new(cx.executor());
23163 fs.insert_tree(
23164 path!("/dir"),
23165 json!({
23166 "a.ts": "a",
23167 }),
23168 )
23169 .await;
23170
23171 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23172 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23173 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23174
23175 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23176 language_registry.add(Arc::new(Language::new(
23177 LanguageConfig {
23178 name: "TypeScript".into(),
23179 matcher: LanguageMatcher {
23180 path_suffixes: vec!["ts".to_string()],
23181 ..Default::default()
23182 },
23183 ..Default::default()
23184 },
23185 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23186 )));
23187 let mut fake_language_servers = language_registry.register_fake_lsp(
23188 "TypeScript",
23189 FakeLspAdapter {
23190 capabilities: lsp::ServerCapabilities {
23191 code_lens_provider: Some(lsp::CodeLensOptions {
23192 resolve_provider: Some(true),
23193 }),
23194 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23195 commands: vec!["_the/command".to_string()],
23196 ..lsp::ExecuteCommandOptions::default()
23197 }),
23198 ..lsp::ServerCapabilities::default()
23199 },
23200 ..FakeLspAdapter::default()
23201 },
23202 );
23203
23204 let editor = workspace
23205 .update(cx, |workspace, window, cx| {
23206 workspace.open_abs_path(
23207 PathBuf::from(path!("/dir/a.ts")),
23208 OpenOptions::default(),
23209 window,
23210 cx,
23211 )
23212 })
23213 .unwrap()
23214 .await
23215 .unwrap()
23216 .downcast::<Editor>()
23217 .unwrap();
23218 cx.executor().run_until_parked();
23219
23220 let fake_server = fake_language_servers.next().await.unwrap();
23221
23222 let buffer = editor.update(cx, |editor, cx| {
23223 editor
23224 .buffer()
23225 .read(cx)
23226 .as_singleton()
23227 .expect("have opened a single file by path")
23228 });
23229
23230 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23231 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23232 drop(buffer_snapshot);
23233 let actions = cx
23234 .update_window(*workspace, |_, window, cx| {
23235 project.code_actions(&buffer, anchor..anchor, window, cx)
23236 })
23237 .unwrap();
23238
23239 fake_server
23240 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23241 Ok(Some(vec![
23242 lsp::CodeLens {
23243 range: lsp::Range::default(),
23244 command: Some(lsp::Command {
23245 title: "Code lens command".to_owned(),
23246 command: "_the/command".to_owned(),
23247 arguments: None,
23248 }),
23249 data: None,
23250 },
23251 lsp::CodeLens {
23252 range: lsp::Range::default(),
23253 command: Some(lsp::Command {
23254 title: "Command not in capabilities".to_owned(),
23255 command: "not in capabilities".to_owned(),
23256 arguments: None,
23257 }),
23258 data: None,
23259 },
23260 lsp::CodeLens {
23261 range: lsp::Range {
23262 start: lsp::Position {
23263 line: 1,
23264 character: 1,
23265 },
23266 end: lsp::Position {
23267 line: 1,
23268 character: 1,
23269 },
23270 },
23271 command: Some(lsp::Command {
23272 title: "Command not in range".to_owned(),
23273 command: "_the/command".to_owned(),
23274 arguments: None,
23275 }),
23276 data: None,
23277 },
23278 ]))
23279 })
23280 .next()
23281 .await;
23282
23283 let actions = actions.await.unwrap();
23284 assert_eq!(
23285 actions.len(),
23286 1,
23287 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23288 );
23289 let action = actions[0].clone();
23290 let apply = project.update(cx, |project, cx| {
23291 project.apply_code_action(buffer.clone(), action, true, cx)
23292 });
23293
23294 // Resolving the code action does not populate its edits. In absence of
23295 // edits, we must execute the given command.
23296 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23297 |mut lens, _| async move {
23298 let lens_command = lens.command.as_mut().expect("should have a command");
23299 assert_eq!(lens_command.title, "Code lens command");
23300 lens_command.arguments = Some(vec![json!("the-argument")]);
23301 Ok(lens)
23302 },
23303 );
23304
23305 // While executing the command, the language server sends the editor
23306 // a `workspaceEdit` request.
23307 fake_server
23308 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23309 let fake = fake_server.clone();
23310 move |params, _| {
23311 assert_eq!(params.command, "_the/command");
23312 let fake = fake.clone();
23313 async move {
23314 fake.server
23315 .request::<lsp::request::ApplyWorkspaceEdit>(
23316 lsp::ApplyWorkspaceEditParams {
23317 label: None,
23318 edit: lsp::WorkspaceEdit {
23319 changes: Some(
23320 [(
23321 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23322 vec![lsp::TextEdit {
23323 range: lsp::Range::new(
23324 lsp::Position::new(0, 0),
23325 lsp::Position::new(0, 0),
23326 ),
23327 new_text: "X".into(),
23328 }],
23329 )]
23330 .into_iter()
23331 .collect(),
23332 ),
23333 ..lsp::WorkspaceEdit::default()
23334 },
23335 },
23336 )
23337 .await
23338 .into_response()
23339 .unwrap();
23340 Ok(Some(json!(null)))
23341 }
23342 }
23343 })
23344 .next()
23345 .await;
23346
23347 // Applying the code lens command returns a project transaction containing the edits
23348 // sent by the language server in its `workspaceEdit` request.
23349 let transaction = apply.await.unwrap();
23350 assert!(transaction.0.contains_key(&buffer));
23351 buffer.update(cx, |buffer, cx| {
23352 assert_eq!(buffer.text(), "Xa");
23353 buffer.undo(cx);
23354 assert_eq!(buffer.text(), "a");
23355 });
23356
23357 let actions_after_edits = cx
23358 .update_window(*workspace, |_, window, cx| {
23359 project.code_actions(&buffer, anchor..anchor, window, cx)
23360 })
23361 .unwrap()
23362 .await
23363 .unwrap();
23364 assert_eq!(
23365 actions, actions_after_edits,
23366 "For the same selection, same code lens actions should be returned"
23367 );
23368
23369 let _responses =
23370 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23371 panic!("No more code lens requests are expected");
23372 });
23373 editor.update_in(cx, |editor, window, cx| {
23374 editor.select_all(&SelectAll, window, cx);
23375 });
23376 cx.executor().run_until_parked();
23377 let new_actions = cx
23378 .update_window(*workspace, |_, window, cx| {
23379 project.code_actions(&buffer, anchor..anchor, window, cx)
23380 })
23381 .unwrap()
23382 .await
23383 .unwrap();
23384 assert_eq!(
23385 actions, new_actions,
23386 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23387 );
23388}
23389
23390#[gpui::test]
23391async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23392 init_test(cx, |_| {});
23393
23394 let fs = FakeFs::new(cx.executor());
23395 let main_text = r#"fn main() {
23396println!("1");
23397println!("2");
23398println!("3");
23399println!("4");
23400println!("5");
23401}"#;
23402 let lib_text = "mod foo {}";
23403 fs.insert_tree(
23404 path!("/a"),
23405 json!({
23406 "lib.rs": lib_text,
23407 "main.rs": main_text,
23408 }),
23409 )
23410 .await;
23411
23412 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23413 let (workspace, cx) =
23414 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23415 let worktree_id = workspace.update(cx, |workspace, cx| {
23416 workspace.project().update(cx, |project, cx| {
23417 project.worktrees(cx).next().unwrap().read(cx).id()
23418 })
23419 });
23420
23421 let expected_ranges = vec![
23422 Point::new(0, 0)..Point::new(0, 0),
23423 Point::new(1, 0)..Point::new(1, 1),
23424 Point::new(2, 0)..Point::new(2, 2),
23425 Point::new(3, 0)..Point::new(3, 3),
23426 ];
23427
23428 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23429 let editor_1 = workspace
23430 .update_in(cx, |workspace, window, cx| {
23431 workspace.open_path(
23432 (worktree_id, "main.rs"),
23433 Some(pane_1.downgrade()),
23434 true,
23435 window,
23436 cx,
23437 )
23438 })
23439 .unwrap()
23440 .await
23441 .downcast::<Editor>()
23442 .unwrap();
23443 pane_1.update(cx, |pane, cx| {
23444 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23445 open_editor.update(cx, |editor, cx| {
23446 assert_eq!(
23447 editor.display_text(cx),
23448 main_text,
23449 "Original main.rs text on initial open",
23450 );
23451 assert_eq!(
23452 editor
23453 .selections
23454 .all::<Point>(cx)
23455 .into_iter()
23456 .map(|s| s.range())
23457 .collect::<Vec<_>>(),
23458 vec![Point::zero()..Point::zero()],
23459 "Default selections on initial open",
23460 );
23461 })
23462 });
23463 editor_1.update_in(cx, |editor, window, cx| {
23464 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23465 s.select_ranges(expected_ranges.clone());
23466 });
23467 });
23468
23469 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23470 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23471 });
23472 let editor_2 = workspace
23473 .update_in(cx, |workspace, window, cx| {
23474 workspace.open_path(
23475 (worktree_id, "main.rs"),
23476 Some(pane_2.downgrade()),
23477 true,
23478 window,
23479 cx,
23480 )
23481 })
23482 .unwrap()
23483 .await
23484 .downcast::<Editor>()
23485 .unwrap();
23486 pane_2.update(cx, |pane, cx| {
23487 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23488 open_editor.update(cx, |editor, cx| {
23489 assert_eq!(
23490 editor.display_text(cx),
23491 main_text,
23492 "Original main.rs text on initial open in another panel",
23493 );
23494 assert_eq!(
23495 editor
23496 .selections
23497 .all::<Point>(cx)
23498 .into_iter()
23499 .map(|s| s.range())
23500 .collect::<Vec<_>>(),
23501 vec![Point::zero()..Point::zero()],
23502 "Default selections on initial open in another panel",
23503 );
23504 })
23505 });
23506
23507 editor_2.update_in(cx, |editor, window, cx| {
23508 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23509 });
23510
23511 let _other_editor_1 = workspace
23512 .update_in(cx, |workspace, window, cx| {
23513 workspace.open_path(
23514 (worktree_id, "lib.rs"),
23515 Some(pane_1.downgrade()),
23516 true,
23517 window,
23518 cx,
23519 )
23520 })
23521 .unwrap()
23522 .await
23523 .downcast::<Editor>()
23524 .unwrap();
23525 pane_1
23526 .update_in(cx, |pane, window, cx| {
23527 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23528 })
23529 .await
23530 .unwrap();
23531 drop(editor_1);
23532 pane_1.update(cx, |pane, cx| {
23533 pane.active_item()
23534 .unwrap()
23535 .downcast::<Editor>()
23536 .unwrap()
23537 .update(cx, |editor, cx| {
23538 assert_eq!(
23539 editor.display_text(cx),
23540 lib_text,
23541 "Other file should be open and active",
23542 );
23543 });
23544 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23545 });
23546
23547 let _other_editor_2 = workspace
23548 .update_in(cx, |workspace, window, cx| {
23549 workspace.open_path(
23550 (worktree_id, "lib.rs"),
23551 Some(pane_2.downgrade()),
23552 true,
23553 window,
23554 cx,
23555 )
23556 })
23557 .unwrap()
23558 .await
23559 .downcast::<Editor>()
23560 .unwrap();
23561 pane_2
23562 .update_in(cx, |pane, window, cx| {
23563 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23564 })
23565 .await
23566 .unwrap();
23567 drop(editor_2);
23568 pane_2.update(cx, |pane, cx| {
23569 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23570 open_editor.update(cx, |editor, cx| {
23571 assert_eq!(
23572 editor.display_text(cx),
23573 lib_text,
23574 "Other file should be open and active in another panel too",
23575 );
23576 });
23577 assert_eq!(
23578 pane.items().count(),
23579 1,
23580 "No other editors should be open in another pane",
23581 );
23582 });
23583
23584 let _editor_1_reopened = workspace
23585 .update_in(cx, |workspace, window, cx| {
23586 workspace.open_path(
23587 (worktree_id, "main.rs"),
23588 Some(pane_1.downgrade()),
23589 true,
23590 window,
23591 cx,
23592 )
23593 })
23594 .unwrap()
23595 .await
23596 .downcast::<Editor>()
23597 .unwrap();
23598 let _editor_2_reopened = workspace
23599 .update_in(cx, |workspace, window, cx| {
23600 workspace.open_path(
23601 (worktree_id, "main.rs"),
23602 Some(pane_2.downgrade()),
23603 true,
23604 window,
23605 cx,
23606 )
23607 })
23608 .unwrap()
23609 .await
23610 .downcast::<Editor>()
23611 .unwrap();
23612 pane_1.update(cx, |pane, cx| {
23613 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23614 open_editor.update(cx, |editor, cx| {
23615 assert_eq!(
23616 editor.display_text(cx),
23617 main_text,
23618 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
23619 );
23620 assert_eq!(
23621 editor
23622 .selections
23623 .all::<Point>(cx)
23624 .into_iter()
23625 .map(|s| s.range())
23626 .collect::<Vec<_>>(),
23627 expected_ranges,
23628 "Previous editor in the 1st panel had selections and should get them restored on reopen",
23629 );
23630 })
23631 });
23632 pane_2.update(cx, |pane, cx| {
23633 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23634 open_editor.update(cx, |editor, cx| {
23635 assert_eq!(
23636 editor.display_text(cx),
23637 r#"fn main() {
23638⋯rintln!("1");
23639⋯intln!("2");
23640⋯ntln!("3");
23641println!("4");
23642println!("5");
23643}"#,
23644 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
23645 );
23646 assert_eq!(
23647 editor
23648 .selections
23649 .all::<Point>(cx)
23650 .into_iter()
23651 .map(|s| s.range())
23652 .collect::<Vec<_>>(),
23653 vec![Point::zero()..Point::zero()],
23654 "Previous editor in the 2nd pane had no selections changed hence should restore none",
23655 );
23656 })
23657 });
23658}
23659
23660#[gpui::test]
23661async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
23662 init_test(cx, |_| {});
23663
23664 let fs = FakeFs::new(cx.executor());
23665 let main_text = r#"fn main() {
23666println!("1");
23667println!("2");
23668println!("3");
23669println!("4");
23670println!("5");
23671}"#;
23672 let lib_text = "mod foo {}";
23673 fs.insert_tree(
23674 path!("/a"),
23675 json!({
23676 "lib.rs": lib_text,
23677 "main.rs": main_text,
23678 }),
23679 )
23680 .await;
23681
23682 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23683 let (workspace, cx) =
23684 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23685 let worktree_id = workspace.update(cx, |workspace, cx| {
23686 workspace.project().update(cx, |project, cx| {
23687 project.worktrees(cx).next().unwrap().read(cx).id()
23688 })
23689 });
23690
23691 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23692 let editor = workspace
23693 .update_in(cx, |workspace, window, cx| {
23694 workspace.open_path(
23695 (worktree_id, "main.rs"),
23696 Some(pane.downgrade()),
23697 true,
23698 window,
23699 cx,
23700 )
23701 })
23702 .unwrap()
23703 .await
23704 .downcast::<Editor>()
23705 .unwrap();
23706 pane.update(cx, |pane, cx| {
23707 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23708 open_editor.update(cx, |editor, cx| {
23709 assert_eq!(
23710 editor.display_text(cx),
23711 main_text,
23712 "Original main.rs text on initial open",
23713 );
23714 })
23715 });
23716 editor.update_in(cx, |editor, window, cx| {
23717 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
23718 });
23719
23720 cx.update_global(|store: &mut SettingsStore, cx| {
23721 store.update_user_settings(cx, |s| {
23722 s.workspace.restore_on_file_reopen = Some(false);
23723 });
23724 });
23725 editor.update_in(cx, |editor, window, cx| {
23726 editor.fold_ranges(
23727 vec![
23728 Point::new(1, 0)..Point::new(1, 1),
23729 Point::new(2, 0)..Point::new(2, 2),
23730 Point::new(3, 0)..Point::new(3, 3),
23731 ],
23732 false,
23733 window,
23734 cx,
23735 );
23736 });
23737 pane.update_in(cx, |pane, window, cx| {
23738 pane.close_all_items(&CloseAllItems::default(), window, cx)
23739 })
23740 .await
23741 .unwrap();
23742 pane.update(cx, |pane, _| {
23743 assert!(pane.active_item().is_none());
23744 });
23745 cx.update_global(|store: &mut SettingsStore, cx| {
23746 store.update_user_settings(cx, |s| {
23747 s.workspace.restore_on_file_reopen = Some(true);
23748 });
23749 });
23750
23751 let _editor_reopened = workspace
23752 .update_in(cx, |workspace, window, cx| {
23753 workspace.open_path(
23754 (worktree_id, "main.rs"),
23755 Some(pane.downgrade()),
23756 true,
23757 window,
23758 cx,
23759 )
23760 })
23761 .unwrap()
23762 .await
23763 .downcast::<Editor>()
23764 .unwrap();
23765 pane.update(cx, |pane, cx| {
23766 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23767 open_editor.update(cx, |editor, cx| {
23768 assert_eq!(
23769 editor.display_text(cx),
23770 main_text,
23771 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
23772 );
23773 })
23774 });
23775}
23776
23777#[gpui::test]
23778async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
23779 struct EmptyModalView {
23780 focus_handle: gpui::FocusHandle,
23781 }
23782 impl EventEmitter<DismissEvent> for EmptyModalView {}
23783 impl Render for EmptyModalView {
23784 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
23785 div()
23786 }
23787 }
23788 impl Focusable for EmptyModalView {
23789 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
23790 self.focus_handle.clone()
23791 }
23792 }
23793 impl workspace::ModalView for EmptyModalView {}
23794 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
23795 EmptyModalView {
23796 focus_handle: cx.focus_handle(),
23797 }
23798 }
23799
23800 init_test(cx, |_| {});
23801
23802 let fs = FakeFs::new(cx.executor());
23803 let project = Project::test(fs, [], cx).await;
23804 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23805 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
23806 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23807 let editor = cx.new_window_entity(|window, cx| {
23808 Editor::new(
23809 EditorMode::full(),
23810 buffer,
23811 Some(project.clone()),
23812 window,
23813 cx,
23814 )
23815 });
23816 workspace
23817 .update(cx, |workspace, window, cx| {
23818 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
23819 })
23820 .unwrap();
23821 editor.update_in(cx, |editor, window, cx| {
23822 editor.open_context_menu(&OpenContextMenu, window, cx);
23823 assert!(editor.mouse_context_menu.is_some());
23824 });
23825 workspace
23826 .update(cx, |workspace, window, cx| {
23827 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
23828 })
23829 .unwrap();
23830 cx.read(|cx| {
23831 assert!(editor.read(cx).mouse_context_menu.is_none());
23832 });
23833}
23834
23835fn set_linked_edit_ranges(
23836 opening: (Point, Point),
23837 closing: (Point, Point),
23838 editor: &mut Editor,
23839 cx: &mut Context<Editor>,
23840) {
23841 let Some((buffer, _)) = editor
23842 .buffer
23843 .read(cx)
23844 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
23845 else {
23846 panic!("Failed to get buffer for selection position");
23847 };
23848 let buffer = buffer.read(cx);
23849 let buffer_id = buffer.remote_id();
23850 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
23851 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
23852 let mut linked_ranges = HashMap::default();
23853 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
23854 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
23855}
23856
23857#[gpui::test]
23858async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
23859 init_test(cx, |_| {});
23860
23861 let fs = FakeFs::new(cx.executor());
23862 fs.insert_file(path!("/file.html"), Default::default())
23863 .await;
23864
23865 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
23866
23867 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23868 let html_language = Arc::new(Language::new(
23869 LanguageConfig {
23870 name: "HTML".into(),
23871 matcher: LanguageMatcher {
23872 path_suffixes: vec!["html".to_string()],
23873 ..LanguageMatcher::default()
23874 },
23875 brackets: BracketPairConfig {
23876 pairs: vec![BracketPair {
23877 start: "<".into(),
23878 end: ">".into(),
23879 close: true,
23880 ..Default::default()
23881 }],
23882 ..Default::default()
23883 },
23884 ..Default::default()
23885 },
23886 Some(tree_sitter_html::LANGUAGE.into()),
23887 ));
23888 language_registry.add(html_language);
23889 let mut fake_servers = language_registry.register_fake_lsp(
23890 "HTML",
23891 FakeLspAdapter {
23892 capabilities: lsp::ServerCapabilities {
23893 completion_provider: Some(lsp::CompletionOptions {
23894 resolve_provider: Some(true),
23895 ..Default::default()
23896 }),
23897 ..Default::default()
23898 },
23899 ..Default::default()
23900 },
23901 );
23902
23903 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23904 let cx = &mut VisualTestContext::from_window(*workspace, cx);
23905
23906 let worktree_id = workspace
23907 .update(cx, |workspace, _window, cx| {
23908 workspace.project().update(cx, |project, cx| {
23909 project.worktrees(cx).next().unwrap().read(cx).id()
23910 })
23911 })
23912 .unwrap();
23913 project
23914 .update(cx, |project, cx| {
23915 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
23916 })
23917 .await
23918 .unwrap();
23919 let editor = workspace
23920 .update(cx, |workspace, window, cx| {
23921 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
23922 })
23923 .unwrap()
23924 .await
23925 .unwrap()
23926 .downcast::<Editor>()
23927 .unwrap();
23928
23929 let fake_server = fake_servers.next().await.unwrap();
23930 editor.update_in(cx, |editor, window, cx| {
23931 editor.set_text("<ad></ad>", window, cx);
23932 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
23933 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
23934 });
23935 set_linked_edit_ranges(
23936 (Point::new(0, 1), Point::new(0, 3)),
23937 (Point::new(0, 6), Point::new(0, 8)),
23938 editor,
23939 cx,
23940 );
23941 });
23942 let mut completion_handle =
23943 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
23944 Ok(Some(lsp::CompletionResponse::Array(vec![
23945 lsp::CompletionItem {
23946 label: "head".to_string(),
23947 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
23948 lsp::InsertReplaceEdit {
23949 new_text: "head".to_string(),
23950 insert: lsp::Range::new(
23951 lsp::Position::new(0, 1),
23952 lsp::Position::new(0, 3),
23953 ),
23954 replace: lsp::Range::new(
23955 lsp::Position::new(0, 1),
23956 lsp::Position::new(0, 3),
23957 ),
23958 },
23959 )),
23960 ..Default::default()
23961 },
23962 ])))
23963 });
23964 editor.update_in(cx, |editor, window, cx| {
23965 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
23966 });
23967 cx.run_until_parked();
23968 completion_handle.next().await.unwrap();
23969 editor.update(cx, |editor, _| {
23970 assert!(
23971 editor.context_menu_visible(),
23972 "Completion menu should be visible"
23973 );
23974 });
23975 editor.update_in(cx, |editor, window, cx| {
23976 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
23977 });
23978 cx.executor().run_until_parked();
23979 editor.update(cx, |editor, cx| {
23980 assert_eq!(editor.text(cx), "<head></head>");
23981 });
23982}
23983
23984#[gpui::test]
23985async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
23986 init_test(cx, |_| {});
23987
23988 let mut cx = EditorTestContext::new(cx).await;
23989 let language = Arc::new(Language::new(
23990 LanguageConfig {
23991 name: "TSX".into(),
23992 matcher: LanguageMatcher {
23993 path_suffixes: vec!["tsx".to_string()],
23994 ..LanguageMatcher::default()
23995 },
23996 brackets: BracketPairConfig {
23997 pairs: vec![BracketPair {
23998 start: "<".into(),
23999 end: ">".into(),
24000 close: true,
24001 ..Default::default()
24002 }],
24003 ..Default::default()
24004 },
24005 linked_edit_characters: HashSet::from_iter(['.']),
24006 ..Default::default()
24007 },
24008 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24009 ));
24010 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24011
24012 // Test typing > does not extend linked pair
24013 cx.set_state("<divˇ<div></div>");
24014 cx.update_editor(|editor, _, cx| {
24015 set_linked_edit_ranges(
24016 (Point::new(0, 1), Point::new(0, 4)),
24017 (Point::new(0, 11), Point::new(0, 14)),
24018 editor,
24019 cx,
24020 );
24021 });
24022 cx.update_editor(|editor, window, cx| {
24023 editor.handle_input(">", window, cx);
24024 });
24025 cx.assert_editor_state("<div>ˇ<div></div>");
24026
24027 // Test typing . do extend linked pair
24028 cx.set_state("<Animatedˇ></Animated>");
24029 cx.update_editor(|editor, _, cx| {
24030 set_linked_edit_ranges(
24031 (Point::new(0, 1), Point::new(0, 9)),
24032 (Point::new(0, 12), Point::new(0, 20)),
24033 editor,
24034 cx,
24035 );
24036 });
24037 cx.update_editor(|editor, window, cx| {
24038 editor.handle_input(".", window, cx);
24039 });
24040 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24041 cx.update_editor(|editor, _, cx| {
24042 set_linked_edit_ranges(
24043 (Point::new(0, 1), Point::new(0, 10)),
24044 (Point::new(0, 13), Point::new(0, 21)),
24045 editor,
24046 cx,
24047 );
24048 });
24049 cx.update_editor(|editor, window, cx| {
24050 editor.handle_input("V", window, cx);
24051 });
24052 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24053}
24054
24055#[gpui::test]
24056async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24057 init_test(cx, |_| {});
24058
24059 let fs = FakeFs::new(cx.executor());
24060 fs.insert_tree(
24061 path!("/root"),
24062 json!({
24063 "a": {
24064 "main.rs": "fn main() {}",
24065 },
24066 "foo": {
24067 "bar": {
24068 "external_file.rs": "pub mod external {}",
24069 }
24070 }
24071 }),
24072 )
24073 .await;
24074
24075 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24076 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24077 language_registry.add(rust_lang());
24078 let _fake_servers = language_registry.register_fake_lsp(
24079 "Rust",
24080 FakeLspAdapter {
24081 ..FakeLspAdapter::default()
24082 },
24083 );
24084 let (workspace, cx) =
24085 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24086 let worktree_id = workspace.update(cx, |workspace, cx| {
24087 workspace.project().update(cx, |project, cx| {
24088 project.worktrees(cx).next().unwrap().read(cx).id()
24089 })
24090 });
24091
24092 let assert_language_servers_count =
24093 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24094 project.update(cx, |project, cx| {
24095 let current = project
24096 .lsp_store()
24097 .read(cx)
24098 .as_local()
24099 .unwrap()
24100 .language_servers
24101 .len();
24102 assert_eq!(expected, current, "{context}");
24103 });
24104 };
24105
24106 assert_language_servers_count(
24107 0,
24108 "No servers should be running before any file is open",
24109 cx,
24110 );
24111 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24112 let main_editor = workspace
24113 .update_in(cx, |workspace, window, cx| {
24114 workspace.open_path(
24115 (worktree_id, "main.rs"),
24116 Some(pane.downgrade()),
24117 true,
24118 window,
24119 cx,
24120 )
24121 })
24122 .unwrap()
24123 .await
24124 .downcast::<Editor>()
24125 .unwrap();
24126 pane.update(cx, |pane, cx| {
24127 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24128 open_editor.update(cx, |editor, cx| {
24129 assert_eq!(
24130 editor.display_text(cx),
24131 "fn main() {}",
24132 "Original main.rs text on initial open",
24133 );
24134 });
24135 assert_eq!(open_editor, main_editor);
24136 });
24137 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24138
24139 let external_editor = workspace
24140 .update_in(cx, |workspace, window, cx| {
24141 workspace.open_abs_path(
24142 PathBuf::from("/root/foo/bar/external_file.rs"),
24143 OpenOptions::default(),
24144 window,
24145 cx,
24146 )
24147 })
24148 .await
24149 .expect("opening external file")
24150 .downcast::<Editor>()
24151 .expect("downcasted external file's open element to editor");
24152 pane.update(cx, |pane, cx| {
24153 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24154 open_editor.update(cx, |editor, cx| {
24155 assert_eq!(
24156 editor.display_text(cx),
24157 "pub mod external {}",
24158 "External file is open now",
24159 );
24160 });
24161 assert_eq!(open_editor, external_editor);
24162 });
24163 assert_language_servers_count(
24164 1,
24165 "Second, external, *.rs file should join the existing server",
24166 cx,
24167 );
24168
24169 pane.update_in(cx, |pane, window, cx| {
24170 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24171 })
24172 .await
24173 .unwrap();
24174 pane.update_in(cx, |pane, window, cx| {
24175 pane.navigate_backward(&Default::default(), window, cx);
24176 });
24177 cx.run_until_parked();
24178 pane.update(cx, |pane, cx| {
24179 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24180 open_editor.update(cx, |editor, cx| {
24181 assert_eq!(
24182 editor.display_text(cx),
24183 "pub mod external {}",
24184 "External file is open now",
24185 );
24186 });
24187 });
24188 assert_language_servers_count(
24189 1,
24190 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24191 cx,
24192 );
24193
24194 cx.update(|_, cx| {
24195 workspace::reload(cx);
24196 });
24197 assert_language_servers_count(
24198 1,
24199 "After reloading the worktree with local and external files opened, only one project should be started",
24200 cx,
24201 );
24202}
24203
24204#[gpui::test]
24205async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24206 init_test(cx, |_| {});
24207
24208 let mut cx = EditorTestContext::new(cx).await;
24209 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24210 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24211
24212 // test cursor move to start of each line on tab
24213 // for `if`, `elif`, `else`, `while`, `with` and `for`
24214 cx.set_state(indoc! {"
24215 def main():
24216 ˇ for item in items:
24217 ˇ while item.active:
24218 ˇ if item.value > 10:
24219 ˇ continue
24220 ˇ elif item.value < 0:
24221 ˇ break
24222 ˇ else:
24223 ˇ with item.context() as ctx:
24224 ˇ yield count
24225 ˇ else:
24226 ˇ log('while else')
24227 ˇ else:
24228 ˇ log('for else')
24229 "});
24230 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24231 cx.assert_editor_state(indoc! {"
24232 def main():
24233 ˇfor item in items:
24234 ˇwhile item.active:
24235 ˇif item.value > 10:
24236 ˇcontinue
24237 ˇelif item.value < 0:
24238 ˇbreak
24239 ˇelse:
24240 ˇwith item.context() as ctx:
24241 ˇyield count
24242 ˇelse:
24243 ˇlog('while else')
24244 ˇelse:
24245 ˇlog('for else')
24246 "});
24247 // test relative indent is preserved when tab
24248 // for `if`, `elif`, `else`, `while`, `with` and `for`
24249 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24250 cx.assert_editor_state(indoc! {"
24251 def main():
24252 ˇfor item in items:
24253 ˇwhile item.active:
24254 ˇif item.value > 10:
24255 ˇcontinue
24256 ˇelif item.value < 0:
24257 ˇbreak
24258 ˇelse:
24259 ˇwith item.context() as ctx:
24260 ˇyield count
24261 ˇelse:
24262 ˇlog('while else')
24263 ˇelse:
24264 ˇlog('for else')
24265 "});
24266
24267 // test cursor move to start of each line on tab
24268 // for `try`, `except`, `else`, `finally`, `match` and `def`
24269 cx.set_state(indoc! {"
24270 def main():
24271 ˇ try:
24272 ˇ fetch()
24273 ˇ except ValueError:
24274 ˇ handle_error()
24275 ˇ else:
24276 ˇ match value:
24277 ˇ case _:
24278 ˇ finally:
24279 ˇ def status():
24280 ˇ return 0
24281 "});
24282 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24283 cx.assert_editor_state(indoc! {"
24284 def main():
24285 ˇtry:
24286 ˇfetch()
24287 ˇexcept ValueError:
24288 ˇhandle_error()
24289 ˇelse:
24290 ˇmatch value:
24291 ˇcase _:
24292 ˇfinally:
24293 ˇdef status():
24294 ˇreturn 0
24295 "});
24296 // test relative indent is preserved when tab
24297 // for `try`, `except`, `else`, `finally`, `match` and `def`
24298 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24299 cx.assert_editor_state(indoc! {"
24300 def main():
24301 ˇtry:
24302 ˇfetch()
24303 ˇexcept ValueError:
24304 ˇhandle_error()
24305 ˇelse:
24306 ˇmatch value:
24307 ˇcase _:
24308 ˇfinally:
24309 ˇdef status():
24310 ˇreturn 0
24311 "});
24312}
24313
24314#[gpui::test]
24315async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24316 init_test(cx, |_| {});
24317
24318 let mut cx = EditorTestContext::new(cx).await;
24319 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24320 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24321
24322 // test `else` auto outdents when typed inside `if` block
24323 cx.set_state(indoc! {"
24324 def main():
24325 if i == 2:
24326 return
24327 ˇ
24328 "});
24329 cx.update_editor(|editor, window, cx| {
24330 editor.handle_input("else:", window, cx);
24331 });
24332 cx.assert_editor_state(indoc! {"
24333 def main():
24334 if i == 2:
24335 return
24336 else:ˇ
24337 "});
24338
24339 // test `except` auto outdents when typed inside `try` block
24340 cx.set_state(indoc! {"
24341 def main():
24342 try:
24343 i = 2
24344 ˇ
24345 "});
24346 cx.update_editor(|editor, window, cx| {
24347 editor.handle_input("except:", window, cx);
24348 });
24349 cx.assert_editor_state(indoc! {"
24350 def main():
24351 try:
24352 i = 2
24353 except:ˇ
24354 "});
24355
24356 // test `else` auto outdents when typed inside `except` block
24357 cx.set_state(indoc! {"
24358 def main():
24359 try:
24360 i = 2
24361 except:
24362 j = 2
24363 ˇ
24364 "});
24365 cx.update_editor(|editor, window, cx| {
24366 editor.handle_input("else:", window, cx);
24367 });
24368 cx.assert_editor_state(indoc! {"
24369 def main():
24370 try:
24371 i = 2
24372 except:
24373 j = 2
24374 else:ˇ
24375 "});
24376
24377 // test `finally` auto outdents when typed inside `else` block
24378 cx.set_state(indoc! {"
24379 def main():
24380 try:
24381 i = 2
24382 except:
24383 j = 2
24384 else:
24385 k = 2
24386 ˇ
24387 "});
24388 cx.update_editor(|editor, window, cx| {
24389 editor.handle_input("finally:", window, cx);
24390 });
24391 cx.assert_editor_state(indoc! {"
24392 def main():
24393 try:
24394 i = 2
24395 except:
24396 j = 2
24397 else:
24398 k = 2
24399 finally:ˇ
24400 "});
24401
24402 // test `else` does not outdents when typed inside `except` block right after for block
24403 cx.set_state(indoc! {"
24404 def main():
24405 try:
24406 i = 2
24407 except:
24408 for i in range(n):
24409 pass
24410 ˇ
24411 "});
24412 cx.update_editor(|editor, window, cx| {
24413 editor.handle_input("else:", window, cx);
24414 });
24415 cx.assert_editor_state(indoc! {"
24416 def main():
24417 try:
24418 i = 2
24419 except:
24420 for i in range(n):
24421 pass
24422 else:ˇ
24423 "});
24424
24425 // test `finally` auto outdents when typed inside `else` block right after for block
24426 cx.set_state(indoc! {"
24427 def main():
24428 try:
24429 i = 2
24430 except:
24431 j = 2
24432 else:
24433 for i in range(n):
24434 pass
24435 ˇ
24436 "});
24437 cx.update_editor(|editor, window, cx| {
24438 editor.handle_input("finally:", window, cx);
24439 });
24440 cx.assert_editor_state(indoc! {"
24441 def main():
24442 try:
24443 i = 2
24444 except:
24445 j = 2
24446 else:
24447 for i in range(n):
24448 pass
24449 finally:ˇ
24450 "});
24451
24452 // test `except` outdents to inner "try" block
24453 cx.set_state(indoc! {"
24454 def main():
24455 try:
24456 i = 2
24457 if i == 2:
24458 try:
24459 i = 3
24460 ˇ
24461 "});
24462 cx.update_editor(|editor, window, cx| {
24463 editor.handle_input("except:", window, cx);
24464 });
24465 cx.assert_editor_state(indoc! {"
24466 def main():
24467 try:
24468 i = 2
24469 if i == 2:
24470 try:
24471 i = 3
24472 except:ˇ
24473 "});
24474
24475 // test `except` outdents to outer "try" block
24476 cx.set_state(indoc! {"
24477 def main():
24478 try:
24479 i = 2
24480 if i == 2:
24481 try:
24482 i = 3
24483 ˇ
24484 "});
24485 cx.update_editor(|editor, window, cx| {
24486 editor.handle_input("except:", window, cx);
24487 });
24488 cx.assert_editor_state(indoc! {"
24489 def main():
24490 try:
24491 i = 2
24492 if i == 2:
24493 try:
24494 i = 3
24495 except:ˇ
24496 "});
24497
24498 // test `else` stays at correct indent when typed after `for` block
24499 cx.set_state(indoc! {"
24500 def main():
24501 for i in range(10):
24502 if i == 3:
24503 break
24504 ˇ
24505 "});
24506 cx.update_editor(|editor, window, cx| {
24507 editor.handle_input("else:", window, cx);
24508 });
24509 cx.assert_editor_state(indoc! {"
24510 def main():
24511 for i in range(10):
24512 if i == 3:
24513 break
24514 else:ˇ
24515 "});
24516
24517 // test does not outdent on typing after line with square brackets
24518 cx.set_state(indoc! {"
24519 def f() -> list[str]:
24520 ˇ
24521 "});
24522 cx.update_editor(|editor, window, cx| {
24523 editor.handle_input("a", window, cx);
24524 });
24525 cx.assert_editor_state(indoc! {"
24526 def f() -> list[str]:
24527 aˇ
24528 "});
24529
24530 // test does not outdent on typing : after case keyword
24531 cx.set_state(indoc! {"
24532 match 1:
24533 caseˇ
24534 "});
24535 cx.update_editor(|editor, window, cx| {
24536 editor.handle_input(":", window, cx);
24537 });
24538 cx.assert_editor_state(indoc! {"
24539 match 1:
24540 case:ˇ
24541 "});
24542}
24543
24544#[gpui::test]
24545async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24546 init_test(cx, |_| {});
24547 update_test_language_settings(cx, |settings| {
24548 settings.defaults.extend_comment_on_newline = Some(false);
24549 });
24550 let mut cx = EditorTestContext::new(cx).await;
24551 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24552 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24553
24554 // test correct indent after newline on comment
24555 cx.set_state(indoc! {"
24556 # COMMENT:ˇ
24557 "});
24558 cx.update_editor(|editor, window, cx| {
24559 editor.newline(&Newline, window, cx);
24560 });
24561 cx.assert_editor_state(indoc! {"
24562 # COMMENT:
24563 ˇ
24564 "});
24565
24566 // test correct indent after newline in brackets
24567 cx.set_state(indoc! {"
24568 {ˇ}
24569 "});
24570 cx.update_editor(|editor, window, cx| {
24571 editor.newline(&Newline, window, cx);
24572 });
24573 cx.run_until_parked();
24574 cx.assert_editor_state(indoc! {"
24575 {
24576 ˇ
24577 }
24578 "});
24579
24580 cx.set_state(indoc! {"
24581 (ˇ)
24582 "});
24583 cx.update_editor(|editor, window, cx| {
24584 editor.newline(&Newline, window, cx);
24585 });
24586 cx.run_until_parked();
24587 cx.assert_editor_state(indoc! {"
24588 (
24589 ˇ
24590 )
24591 "});
24592
24593 // do not indent after empty lists or dictionaries
24594 cx.set_state(indoc! {"
24595 a = []ˇ
24596 "});
24597 cx.update_editor(|editor, window, cx| {
24598 editor.newline(&Newline, window, cx);
24599 });
24600 cx.run_until_parked();
24601 cx.assert_editor_state(indoc! {"
24602 a = []
24603 ˇ
24604 "});
24605}
24606
24607#[gpui::test]
24608async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
24609 init_test(cx, |_| {});
24610
24611 let mut cx = EditorTestContext::new(cx).await;
24612 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24613 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24614
24615 // test cursor move to start of each line on tab
24616 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
24617 cx.set_state(indoc! {"
24618 function main() {
24619 ˇ for item in $items; do
24620 ˇ while [ -n \"$item\" ]; do
24621 ˇ if [ \"$value\" -gt 10 ]; then
24622 ˇ continue
24623 ˇ elif [ \"$value\" -lt 0 ]; then
24624 ˇ break
24625 ˇ else
24626 ˇ echo \"$item\"
24627 ˇ fi
24628 ˇ done
24629 ˇ done
24630 ˇ}
24631 "});
24632 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24633 cx.assert_editor_state(indoc! {"
24634 function main() {
24635 ˇfor item in $items; do
24636 ˇwhile [ -n \"$item\" ]; do
24637 ˇif [ \"$value\" -gt 10 ]; then
24638 ˇcontinue
24639 ˇelif [ \"$value\" -lt 0 ]; then
24640 ˇbreak
24641 ˇelse
24642 ˇecho \"$item\"
24643 ˇfi
24644 ˇdone
24645 ˇdone
24646 ˇ}
24647 "});
24648 // test relative indent is preserved when tab
24649 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24650 cx.assert_editor_state(indoc! {"
24651 function main() {
24652 ˇfor item in $items; do
24653 ˇwhile [ -n \"$item\" ]; do
24654 ˇif [ \"$value\" -gt 10 ]; then
24655 ˇcontinue
24656 ˇelif [ \"$value\" -lt 0 ]; then
24657 ˇbreak
24658 ˇelse
24659 ˇecho \"$item\"
24660 ˇfi
24661 ˇdone
24662 ˇdone
24663 ˇ}
24664 "});
24665
24666 // test cursor move to start of each line on tab
24667 // for `case` statement with patterns
24668 cx.set_state(indoc! {"
24669 function handle() {
24670 ˇ case \"$1\" in
24671 ˇ start)
24672 ˇ echo \"a\"
24673 ˇ ;;
24674 ˇ stop)
24675 ˇ echo \"b\"
24676 ˇ ;;
24677 ˇ *)
24678 ˇ echo \"c\"
24679 ˇ ;;
24680 ˇ esac
24681 ˇ}
24682 "});
24683 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24684 cx.assert_editor_state(indoc! {"
24685 function handle() {
24686 ˇcase \"$1\" in
24687 ˇstart)
24688 ˇecho \"a\"
24689 ˇ;;
24690 ˇstop)
24691 ˇecho \"b\"
24692 ˇ;;
24693 ˇ*)
24694 ˇecho \"c\"
24695 ˇ;;
24696 ˇesac
24697 ˇ}
24698 "});
24699}
24700
24701#[gpui::test]
24702async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
24703 init_test(cx, |_| {});
24704
24705 let mut cx = EditorTestContext::new(cx).await;
24706 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24707 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24708
24709 // test indents on comment insert
24710 cx.set_state(indoc! {"
24711 function main() {
24712 ˇ for item in $items; do
24713 ˇ while [ -n \"$item\" ]; do
24714 ˇ if [ \"$value\" -gt 10 ]; then
24715 ˇ continue
24716 ˇ elif [ \"$value\" -lt 0 ]; then
24717 ˇ break
24718 ˇ else
24719 ˇ echo \"$item\"
24720 ˇ fi
24721 ˇ done
24722 ˇ done
24723 ˇ}
24724 "});
24725 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
24726 cx.assert_editor_state(indoc! {"
24727 function main() {
24728 #ˇ for item in $items; do
24729 #ˇ while [ -n \"$item\" ]; do
24730 #ˇ if [ \"$value\" -gt 10 ]; then
24731 #ˇ continue
24732 #ˇ elif [ \"$value\" -lt 0 ]; then
24733 #ˇ break
24734 #ˇ else
24735 #ˇ echo \"$item\"
24736 #ˇ fi
24737 #ˇ done
24738 #ˇ done
24739 #ˇ}
24740 "});
24741}
24742
24743#[gpui::test]
24744async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
24745 init_test(cx, |_| {});
24746
24747 let mut cx = EditorTestContext::new(cx).await;
24748 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24749 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24750
24751 // test `else` auto outdents when typed inside `if` block
24752 cx.set_state(indoc! {"
24753 if [ \"$1\" = \"test\" ]; then
24754 echo \"foo bar\"
24755 ˇ
24756 "});
24757 cx.update_editor(|editor, window, cx| {
24758 editor.handle_input("else", window, cx);
24759 });
24760 cx.assert_editor_state(indoc! {"
24761 if [ \"$1\" = \"test\" ]; then
24762 echo \"foo bar\"
24763 elseˇ
24764 "});
24765
24766 // test `elif` auto outdents when typed inside `if` block
24767 cx.set_state(indoc! {"
24768 if [ \"$1\" = \"test\" ]; then
24769 echo \"foo bar\"
24770 ˇ
24771 "});
24772 cx.update_editor(|editor, window, cx| {
24773 editor.handle_input("elif", window, cx);
24774 });
24775 cx.assert_editor_state(indoc! {"
24776 if [ \"$1\" = \"test\" ]; then
24777 echo \"foo bar\"
24778 elifˇ
24779 "});
24780
24781 // test `fi` auto outdents when typed inside `else` block
24782 cx.set_state(indoc! {"
24783 if [ \"$1\" = \"test\" ]; then
24784 echo \"foo bar\"
24785 else
24786 echo \"bar baz\"
24787 ˇ
24788 "});
24789 cx.update_editor(|editor, window, cx| {
24790 editor.handle_input("fi", window, cx);
24791 });
24792 cx.assert_editor_state(indoc! {"
24793 if [ \"$1\" = \"test\" ]; then
24794 echo \"foo bar\"
24795 else
24796 echo \"bar baz\"
24797 fiˇ
24798 "});
24799
24800 // test `done` auto outdents when typed inside `while` block
24801 cx.set_state(indoc! {"
24802 while read line; do
24803 echo \"$line\"
24804 ˇ
24805 "});
24806 cx.update_editor(|editor, window, cx| {
24807 editor.handle_input("done", window, cx);
24808 });
24809 cx.assert_editor_state(indoc! {"
24810 while read line; do
24811 echo \"$line\"
24812 doneˇ
24813 "});
24814
24815 // test `done` auto outdents when typed inside `for` block
24816 cx.set_state(indoc! {"
24817 for file in *.txt; do
24818 cat \"$file\"
24819 ˇ
24820 "});
24821 cx.update_editor(|editor, window, cx| {
24822 editor.handle_input("done", window, cx);
24823 });
24824 cx.assert_editor_state(indoc! {"
24825 for file in *.txt; do
24826 cat \"$file\"
24827 doneˇ
24828 "});
24829
24830 // test `esac` auto outdents when typed inside `case` block
24831 cx.set_state(indoc! {"
24832 case \"$1\" in
24833 start)
24834 echo \"foo bar\"
24835 ;;
24836 stop)
24837 echo \"bar baz\"
24838 ;;
24839 ˇ
24840 "});
24841 cx.update_editor(|editor, window, cx| {
24842 editor.handle_input("esac", window, cx);
24843 });
24844 cx.assert_editor_state(indoc! {"
24845 case \"$1\" in
24846 start)
24847 echo \"foo bar\"
24848 ;;
24849 stop)
24850 echo \"bar baz\"
24851 ;;
24852 esacˇ
24853 "});
24854
24855 // test `*)` auto outdents when typed inside `case` block
24856 cx.set_state(indoc! {"
24857 case \"$1\" in
24858 start)
24859 echo \"foo bar\"
24860 ;;
24861 ˇ
24862 "});
24863 cx.update_editor(|editor, window, cx| {
24864 editor.handle_input("*)", window, cx);
24865 });
24866 cx.assert_editor_state(indoc! {"
24867 case \"$1\" in
24868 start)
24869 echo \"foo bar\"
24870 ;;
24871 *)ˇ
24872 "});
24873
24874 // test `fi` outdents to correct level with nested if blocks
24875 cx.set_state(indoc! {"
24876 if [ \"$1\" = \"test\" ]; then
24877 echo \"outer if\"
24878 if [ \"$2\" = \"debug\" ]; then
24879 echo \"inner if\"
24880 ˇ
24881 "});
24882 cx.update_editor(|editor, window, cx| {
24883 editor.handle_input("fi", window, cx);
24884 });
24885 cx.assert_editor_state(indoc! {"
24886 if [ \"$1\" = \"test\" ]; then
24887 echo \"outer if\"
24888 if [ \"$2\" = \"debug\" ]; then
24889 echo \"inner if\"
24890 fiˇ
24891 "});
24892}
24893
24894#[gpui::test]
24895async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
24896 init_test(cx, |_| {});
24897 update_test_language_settings(cx, |settings| {
24898 settings.defaults.extend_comment_on_newline = Some(false);
24899 });
24900 let mut cx = EditorTestContext::new(cx).await;
24901 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
24902 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24903
24904 // test correct indent after newline on comment
24905 cx.set_state(indoc! {"
24906 # COMMENT:ˇ
24907 "});
24908 cx.update_editor(|editor, window, cx| {
24909 editor.newline(&Newline, window, cx);
24910 });
24911 cx.assert_editor_state(indoc! {"
24912 # COMMENT:
24913 ˇ
24914 "});
24915
24916 // test correct indent after newline after `then`
24917 cx.set_state(indoc! {"
24918
24919 if [ \"$1\" = \"test\" ]; thenˇ
24920 "});
24921 cx.update_editor(|editor, window, cx| {
24922 editor.newline(&Newline, window, cx);
24923 });
24924 cx.run_until_parked();
24925 cx.assert_editor_state(indoc! {"
24926
24927 if [ \"$1\" = \"test\" ]; then
24928 ˇ
24929 "});
24930
24931 // test correct indent after newline after `else`
24932 cx.set_state(indoc! {"
24933 if [ \"$1\" = \"test\" ]; then
24934 elseˇ
24935 "});
24936 cx.update_editor(|editor, window, cx| {
24937 editor.newline(&Newline, window, cx);
24938 });
24939 cx.run_until_parked();
24940 cx.assert_editor_state(indoc! {"
24941 if [ \"$1\" = \"test\" ]; then
24942 else
24943 ˇ
24944 "});
24945
24946 // test correct indent after newline after `elif`
24947 cx.set_state(indoc! {"
24948 if [ \"$1\" = \"test\" ]; then
24949 elifˇ
24950 "});
24951 cx.update_editor(|editor, window, cx| {
24952 editor.newline(&Newline, window, cx);
24953 });
24954 cx.run_until_parked();
24955 cx.assert_editor_state(indoc! {"
24956 if [ \"$1\" = \"test\" ]; then
24957 elif
24958 ˇ
24959 "});
24960
24961 // test correct indent after newline after `do`
24962 cx.set_state(indoc! {"
24963 for file in *.txt; doˇ
24964 "});
24965 cx.update_editor(|editor, window, cx| {
24966 editor.newline(&Newline, window, cx);
24967 });
24968 cx.run_until_parked();
24969 cx.assert_editor_state(indoc! {"
24970 for file in *.txt; do
24971 ˇ
24972 "});
24973
24974 // test correct indent after newline after case pattern
24975 cx.set_state(indoc! {"
24976 case \"$1\" in
24977 start)ˇ
24978 "});
24979 cx.update_editor(|editor, window, cx| {
24980 editor.newline(&Newline, window, cx);
24981 });
24982 cx.run_until_parked();
24983 cx.assert_editor_state(indoc! {"
24984 case \"$1\" in
24985 start)
24986 ˇ
24987 "});
24988
24989 // test correct indent after newline after case pattern
24990 cx.set_state(indoc! {"
24991 case \"$1\" in
24992 start)
24993 ;;
24994 *)ˇ
24995 "});
24996 cx.update_editor(|editor, window, cx| {
24997 editor.newline(&Newline, window, cx);
24998 });
24999 cx.run_until_parked();
25000 cx.assert_editor_state(indoc! {"
25001 case \"$1\" in
25002 start)
25003 ;;
25004 *)
25005 ˇ
25006 "});
25007
25008 // test correct indent after newline after function opening brace
25009 cx.set_state(indoc! {"
25010 function test() {ˇ}
25011 "});
25012 cx.update_editor(|editor, window, cx| {
25013 editor.newline(&Newline, window, cx);
25014 });
25015 cx.run_until_parked();
25016 cx.assert_editor_state(indoc! {"
25017 function test() {
25018 ˇ
25019 }
25020 "});
25021
25022 // test no extra indent after semicolon on same line
25023 cx.set_state(indoc! {"
25024 echo \"test\";ˇ
25025 "});
25026 cx.update_editor(|editor, window, cx| {
25027 editor.newline(&Newline, window, cx);
25028 });
25029 cx.run_until_parked();
25030 cx.assert_editor_state(indoc! {"
25031 echo \"test\";
25032 ˇ
25033 "});
25034}
25035
25036fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25037 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25038 point..point
25039}
25040
25041#[track_caller]
25042fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25043 let (text, ranges) = marked_text_ranges(marked_text, true);
25044 assert_eq!(editor.text(cx), text);
25045 assert_eq!(
25046 editor.selections.ranges(cx),
25047 ranges,
25048 "Assert selections are {}",
25049 marked_text
25050 );
25051}
25052
25053pub fn handle_signature_help_request(
25054 cx: &mut EditorLspTestContext,
25055 mocked_response: lsp::SignatureHelp,
25056) -> impl Future<Output = ()> + use<> {
25057 let mut request =
25058 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25059 let mocked_response = mocked_response.clone();
25060 async move { Ok(Some(mocked_response)) }
25061 });
25062
25063 async move {
25064 request.next().await;
25065 }
25066}
25067
25068#[track_caller]
25069pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25070 cx.update_editor(|editor, _, _| {
25071 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25072 let entries = menu.entries.borrow();
25073 let entries = entries
25074 .iter()
25075 .map(|entry| entry.string.as_str())
25076 .collect::<Vec<_>>();
25077 assert_eq!(entries, expected);
25078 } else {
25079 panic!("Expected completions menu");
25080 }
25081 });
25082}
25083
25084/// Handle completion request passing a marked string specifying where the completion
25085/// should be triggered from using '|' character, what range should be replaced, and what completions
25086/// should be returned using '<' and '>' to delimit the range.
25087///
25088/// Also see `handle_completion_request_with_insert_and_replace`.
25089#[track_caller]
25090pub fn handle_completion_request(
25091 marked_string: &str,
25092 completions: Vec<&'static str>,
25093 is_incomplete: bool,
25094 counter: Arc<AtomicUsize>,
25095 cx: &mut EditorLspTestContext,
25096) -> impl Future<Output = ()> {
25097 let complete_from_marker: TextRangeMarker = '|'.into();
25098 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25099 let (_, mut marked_ranges) = marked_text_ranges_by(
25100 marked_string,
25101 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25102 );
25103
25104 let complete_from_position =
25105 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25106 let replace_range =
25107 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25108
25109 let mut request =
25110 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25111 let completions = completions.clone();
25112 counter.fetch_add(1, atomic::Ordering::Release);
25113 async move {
25114 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25115 assert_eq!(
25116 params.text_document_position.position,
25117 complete_from_position
25118 );
25119 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25120 is_incomplete,
25121 item_defaults: None,
25122 items: completions
25123 .iter()
25124 .map(|completion_text| lsp::CompletionItem {
25125 label: completion_text.to_string(),
25126 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25127 range: replace_range,
25128 new_text: completion_text.to_string(),
25129 })),
25130 ..Default::default()
25131 })
25132 .collect(),
25133 })))
25134 }
25135 });
25136
25137 async move {
25138 request.next().await;
25139 }
25140}
25141
25142/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25143/// given instead, which also contains an `insert` range.
25144///
25145/// This function uses markers to define ranges:
25146/// - `|` marks the cursor position
25147/// - `<>` marks the replace range
25148/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25149pub fn handle_completion_request_with_insert_and_replace(
25150 cx: &mut EditorLspTestContext,
25151 marked_string: &str,
25152 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25153 counter: Arc<AtomicUsize>,
25154) -> impl Future<Output = ()> {
25155 let complete_from_marker: TextRangeMarker = '|'.into();
25156 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25157 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25158
25159 let (_, mut marked_ranges) = marked_text_ranges_by(
25160 marked_string,
25161 vec![
25162 complete_from_marker.clone(),
25163 replace_range_marker.clone(),
25164 insert_range_marker.clone(),
25165 ],
25166 );
25167
25168 let complete_from_position =
25169 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25170 let replace_range =
25171 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25172
25173 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25174 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25175 _ => lsp::Range {
25176 start: replace_range.start,
25177 end: complete_from_position,
25178 },
25179 };
25180
25181 let mut request =
25182 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25183 let completions = completions.clone();
25184 counter.fetch_add(1, atomic::Ordering::Release);
25185 async move {
25186 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25187 assert_eq!(
25188 params.text_document_position.position, complete_from_position,
25189 "marker `|` position doesn't match",
25190 );
25191 Ok(Some(lsp::CompletionResponse::Array(
25192 completions
25193 .iter()
25194 .map(|(label, new_text)| lsp::CompletionItem {
25195 label: label.to_string(),
25196 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25197 lsp::InsertReplaceEdit {
25198 insert: insert_range,
25199 replace: replace_range,
25200 new_text: new_text.to_string(),
25201 },
25202 )),
25203 ..Default::default()
25204 })
25205 .collect(),
25206 )))
25207 }
25208 });
25209
25210 async move {
25211 request.next().await;
25212 }
25213}
25214
25215fn handle_resolve_completion_request(
25216 cx: &mut EditorLspTestContext,
25217 edits: Option<Vec<(&'static str, &'static str)>>,
25218) -> impl Future<Output = ()> {
25219 let edits = edits.map(|edits| {
25220 edits
25221 .iter()
25222 .map(|(marked_string, new_text)| {
25223 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25224 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25225 lsp::TextEdit::new(replace_range, new_text.to_string())
25226 })
25227 .collect::<Vec<_>>()
25228 });
25229
25230 let mut request =
25231 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25232 let edits = edits.clone();
25233 async move {
25234 Ok(lsp::CompletionItem {
25235 additional_text_edits: edits,
25236 ..Default::default()
25237 })
25238 }
25239 });
25240
25241 async move {
25242 request.next().await;
25243 }
25244}
25245
25246pub(crate) fn update_test_language_settings(
25247 cx: &mut TestAppContext,
25248 f: impl Fn(&mut AllLanguageSettingsContent),
25249) {
25250 cx.update(|cx| {
25251 SettingsStore::update_global(cx, |store, cx| {
25252 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25253 });
25254 });
25255}
25256
25257pub(crate) fn update_test_project_settings(
25258 cx: &mut TestAppContext,
25259 f: impl Fn(&mut ProjectSettingsContent),
25260) {
25261 cx.update(|cx| {
25262 SettingsStore::update_global(cx, |store, cx| {
25263 store.update_user_settings(cx, |settings| f(&mut settings.project));
25264 });
25265 });
25266}
25267
25268pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25269 cx.update(|cx| {
25270 assets::Assets.load_test_fonts(cx);
25271 let store = SettingsStore::test(cx);
25272 cx.set_global(store);
25273 theme::init(theme::LoadThemes::JustBase, cx);
25274 release_channel::init(SemanticVersion::default(), cx);
25275 client::init_settings(cx);
25276 language::init(cx);
25277 Project::init_settings(cx);
25278 workspace::init_settings(cx);
25279 crate::init(cx);
25280 });
25281 zlog::init_test();
25282 update_test_language_settings(cx, f);
25283}
25284
25285#[track_caller]
25286fn assert_hunk_revert(
25287 not_reverted_text_with_selections: &str,
25288 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25289 expected_reverted_text_with_selections: &str,
25290 base_text: &str,
25291 cx: &mut EditorLspTestContext,
25292) {
25293 cx.set_state(not_reverted_text_with_selections);
25294 cx.set_head_text(base_text);
25295 cx.executor().run_until_parked();
25296
25297 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25298 let snapshot = editor.snapshot(window, cx);
25299 let reverted_hunk_statuses = snapshot
25300 .buffer_snapshot
25301 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
25302 .map(|hunk| hunk.status().kind)
25303 .collect::<Vec<_>>();
25304
25305 editor.git_restore(&Default::default(), window, cx);
25306 reverted_hunk_statuses
25307 });
25308 cx.executor().run_until_parked();
25309 cx.assert_editor_state(expected_reverted_text_with_selections);
25310 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25311}
25312
25313#[gpui::test(iterations = 10)]
25314async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25315 init_test(cx, |_| {});
25316
25317 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25318 let counter = diagnostic_requests.clone();
25319
25320 let fs = FakeFs::new(cx.executor());
25321 fs.insert_tree(
25322 path!("/a"),
25323 json!({
25324 "first.rs": "fn main() { let a = 5; }",
25325 "second.rs": "// Test file",
25326 }),
25327 )
25328 .await;
25329
25330 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25331 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25332 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25333
25334 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25335 language_registry.add(rust_lang());
25336 let mut fake_servers = language_registry.register_fake_lsp(
25337 "Rust",
25338 FakeLspAdapter {
25339 capabilities: lsp::ServerCapabilities {
25340 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25341 lsp::DiagnosticOptions {
25342 identifier: None,
25343 inter_file_dependencies: true,
25344 workspace_diagnostics: true,
25345 work_done_progress_options: Default::default(),
25346 },
25347 )),
25348 ..Default::default()
25349 },
25350 ..Default::default()
25351 },
25352 );
25353
25354 let editor = workspace
25355 .update(cx, |workspace, window, cx| {
25356 workspace.open_abs_path(
25357 PathBuf::from(path!("/a/first.rs")),
25358 OpenOptions::default(),
25359 window,
25360 cx,
25361 )
25362 })
25363 .unwrap()
25364 .await
25365 .unwrap()
25366 .downcast::<Editor>()
25367 .unwrap();
25368 let fake_server = fake_servers.next().await.unwrap();
25369 let server_id = fake_server.server.server_id();
25370 let mut first_request = fake_server
25371 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25372 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25373 let result_id = Some(new_result_id.to_string());
25374 assert_eq!(
25375 params.text_document.uri,
25376 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25377 );
25378 async move {
25379 Ok(lsp::DocumentDiagnosticReportResult::Report(
25380 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25381 related_documents: None,
25382 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25383 items: Vec::new(),
25384 result_id,
25385 },
25386 }),
25387 ))
25388 }
25389 });
25390
25391 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25392 project.update(cx, |project, cx| {
25393 let buffer_id = editor
25394 .read(cx)
25395 .buffer()
25396 .read(cx)
25397 .as_singleton()
25398 .expect("created a singleton buffer")
25399 .read(cx)
25400 .remote_id();
25401 let buffer_result_id = project
25402 .lsp_store()
25403 .read(cx)
25404 .result_id(server_id, buffer_id, cx);
25405 assert_eq!(expected, buffer_result_id);
25406 });
25407 };
25408
25409 ensure_result_id(None, cx);
25410 cx.executor().advance_clock(Duration::from_millis(60));
25411 cx.executor().run_until_parked();
25412 assert_eq!(
25413 diagnostic_requests.load(atomic::Ordering::Acquire),
25414 1,
25415 "Opening file should trigger diagnostic request"
25416 );
25417 first_request
25418 .next()
25419 .await
25420 .expect("should have sent the first diagnostics pull request");
25421 ensure_result_id(Some("1".to_string()), cx);
25422
25423 // Editing should trigger diagnostics
25424 editor.update_in(cx, |editor, window, cx| {
25425 editor.handle_input("2", window, cx)
25426 });
25427 cx.executor().advance_clock(Duration::from_millis(60));
25428 cx.executor().run_until_parked();
25429 assert_eq!(
25430 diagnostic_requests.load(atomic::Ordering::Acquire),
25431 2,
25432 "Editing should trigger diagnostic request"
25433 );
25434 ensure_result_id(Some("2".to_string()), cx);
25435
25436 // Moving cursor should not trigger diagnostic request
25437 editor.update_in(cx, |editor, window, cx| {
25438 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25439 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25440 });
25441 });
25442 cx.executor().advance_clock(Duration::from_millis(60));
25443 cx.executor().run_until_parked();
25444 assert_eq!(
25445 diagnostic_requests.load(atomic::Ordering::Acquire),
25446 2,
25447 "Cursor movement should not trigger diagnostic request"
25448 );
25449 ensure_result_id(Some("2".to_string()), cx);
25450 // Multiple rapid edits should be debounced
25451 for _ in 0..5 {
25452 editor.update_in(cx, |editor, window, cx| {
25453 editor.handle_input("x", window, cx)
25454 });
25455 }
25456 cx.executor().advance_clock(Duration::from_millis(60));
25457 cx.executor().run_until_parked();
25458
25459 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25460 assert!(
25461 final_requests <= 4,
25462 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25463 );
25464 ensure_result_id(Some(final_requests.to_string()), cx);
25465}
25466
25467#[gpui::test]
25468async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25469 // Regression test for issue #11671
25470 // Previously, adding a cursor after moving multiple cursors would reset
25471 // the cursor count instead of adding to the existing cursors.
25472 init_test(cx, |_| {});
25473 let mut cx = EditorTestContext::new(cx).await;
25474
25475 // Create a simple buffer with cursor at start
25476 cx.set_state(indoc! {"
25477 ˇaaaa
25478 bbbb
25479 cccc
25480 dddd
25481 eeee
25482 ffff
25483 gggg
25484 hhhh"});
25485
25486 // Add 2 cursors below (so we have 3 total)
25487 cx.update_editor(|editor, window, cx| {
25488 editor.add_selection_below(&Default::default(), window, cx);
25489 editor.add_selection_below(&Default::default(), window, cx);
25490 });
25491
25492 // Verify we have 3 cursors
25493 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25494 assert_eq!(
25495 initial_count, 3,
25496 "Should have 3 cursors after adding 2 below"
25497 );
25498
25499 // Move down one line
25500 cx.update_editor(|editor, window, cx| {
25501 editor.move_down(&MoveDown, window, cx);
25502 });
25503
25504 // Add another cursor below
25505 cx.update_editor(|editor, window, cx| {
25506 editor.add_selection_below(&Default::default(), window, cx);
25507 });
25508
25509 // Should now have 4 cursors (3 original + 1 new)
25510 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25511 assert_eq!(
25512 final_count, 4,
25513 "Should have 4 cursors after moving and adding another"
25514 );
25515}
25516
25517#[gpui::test(iterations = 10)]
25518async fn test_document_colors(cx: &mut TestAppContext) {
25519 let expected_color = Rgba {
25520 r: 0.33,
25521 g: 0.33,
25522 b: 0.33,
25523 a: 0.33,
25524 };
25525
25526 init_test(cx, |_| {});
25527
25528 let fs = FakeFs::new(cx.executor());
25529 fs.insert_tree(
25530 path!("/a"),
25531 json!({
25532 "first.rs": "fn main() { let a = 5; }",
25533 }),
25534 )
25535 .await;
25536
25537 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25538 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25539 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25540
25541 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25542 language_registry.add(rust_lang());
25543 let mut fake_servers = language_registry.register_fake_lsp(
25544 "Rust",
25545 FakeLspAdapter {
25546 capabilities: lsp::ServerCapabilities {
25547 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
25548 ..lsp::ServerCapabilities::default()
25549 },
25550 name: "rust-analyzer",
25551 ..FakeLspAdapter::default()
25552 },
25553 );
25554 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
25555 "Rust",
25556 FakeLspAdapter {
25557 capabilities: lsp::ServerCapabilities {
25558 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
25559 ..lsp::ServerCapabilities::default()
25560 },
25561 name: "not-rust-analyzer",
25562 ..FakeLspAdapter::default()
25563 },
25564 );
25565
25566 let editor = workspace
25567 .update(cx, |workspace, window, cx| {
25568 workspace.open_abs_path(
25569 PathBuf::from(path!("/a/first.rs")),
25570 OpenOptions::default(),
25571 window,
25572 cx,
25573 )
25574 })
25575 .unwrap()
25576 .await
25577 .unwrap()
25578 .downcast::<Editor>()
25579 .unwrap();
25580 let fake_language_server = fake_servers.next().await.unwrap();
25581 let fake_language_server_without_capabilities =
25582 fake_servers_without_capabilities.next().await.unwrap();
25583 let requests_made = Arc::new(AtomicUsize::new(0));
25584 let closure_requests_made = Arc::clone(&requests_made);
25585 let mut color_request_handle = fake_language_server
25586 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
25587 let requests_made = Arc::clone(&closure_requests_made);
25588 async move {
25589 assert_eq!(
25590 params.text_document.uri,
25591 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25592 );
25593 requests_made.fetch_add(1, atomic::Ordering::Release);
25594 Ok(vec![
25595 lsp::ColorInformation {
25596 range: lsp::Range {
25597 start: lsp::Position {
25598 line: 0,
25599 character: 0,
25600 },
25601 end: lsp::Position {
25602 line: 0,
25603 character: 1,
25604 },
25605 },
25606 color: lsp::Color {
25607 red: 0.33,
25608 green: 0.33,
25609 blue: 0.33,
25610 alpha: 0.33,
25611 },
25612 },
25613 lsp::ColorInformation {
25614 range: lsp::Range {
25615 start: lsp::Position {
25616 line: 0,
25617 character: 0,
25618 },
25619 end: lsp::Position {
25620 line: 0,
25621 character: 1,
25622 },
25623 },
25624 color: lsp::Color {
25625 red: 0.33,
25626 green: 0.33,
25627 blue: 0.33,
25628 alpha: 0.33,
25629 },
25630 },
25631 ])
25632 }
25633 });
25634
25635 let _handle = fake_language_server_without_capabilities
25636 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
25637 panic!("Should not be called");
25638 });
25639 cx.executor().advance_clock(Duration::from_millis(100));
25640 color_request_handle.next().await.unwrap();
25641 cx.run_until_parked();
25642 assert_eq!(
25643 1,
25644 requests_made.load(atomic::Ordering::Acquire),
25645 "Should query for colors once per editor open"
25646 );
25647 editor.update_in(cx, |editor, _, cx| {
25648 assert_eq!(
25649 vec![expected_color],
25650 extract_color_inlays(editor, cx),
25651 "Should have an initial inlay"
25652 );
25653 });
25654
25655 // opening another file in a split should not influence the LSP query counter
25656 workspace
25657 .update(cx, |workspace, window, cx| {
25658 assert_eq!(
25659 workspace.panes().len(),
25660 1,
25661 "Should have one pane with one editor"
25662 );
25663 workspace.move_item_to_pane_in_direction(
25664 &MoveItemToPaneInDirection {
25665 direction: SplitDirection::Right,
25666 focus: false,
25667 clone: true,
25668 },
25669 window,
25670 cx,
25671 );
25672 })
25673 .unwrap();
25674 cx.run_until_parked();
25675 workspace
25676 .update(cx, |workspace, _, cx| {
25677 let panes = workspace.panes();
25678 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
25679 for pane in panes {
25680 let editor = pane
25681 .read(cx)
25682 .active_item()
25683 .and_then(|item| item.downcast::<Editor>())
25684 .expect("Should have opened an editor in each split");
25685 let editor_file = editor
25686 .read(cx)
25687 .buffer()
25688 .read(cx)
25689 .as_singleton()
25690 .expect("test deals with singleton buffers")
25691 .read(cx)
25692 .file()
25693 .expect("test buffese should have a file")
25694 .path();
25695 assert_eq!(
25696 editor_file.as_ref(),
25697 Path::new("first.rs"),
25698 "Both editors should be opened for the same file"
25699 )
25700 }
25701 })
25702 .unwrap();
25703
25704 cx.executor().advance_clock(Duration::from_millis(500));
25705 let save = editor.update_in(cx, |editor, window, cx| {
25706 editor.move_to_end(&MoveToEnd, window, cx);
25707 editor.handle_input("dirty", window, cx);
25708 editor.save(
25709 SaveOptions {
25710 format: true,
25711 autosave: true,
25712 },
25713 project.clone(),
25714 window,
25715 cx,
25716 )
25717 });
25718 save.await.unwrap();
25719
25720 color_request_handle.next().await.unwrap();
25721 cx.run_until_parked();
25722 assert_eq!(
25723 3,
25724 requests_made.load(atomic::Ordering::Acquire),
25725 "Should query for colors once per save and once per formatting after save"
25726 );
25727
25728 drop(editor);
25729 let close = workspace
25730 .update(cx, |workspace, window, cx| {
25731 workspace.active_pane().update(cx, |pane, cx| {
25732 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25733 })
25734 })
25735 .unwrap();
25736 close.await.unwrap();
25737 let close = workspace
25738 .update(cx, |workspace, window, cx| {
25739 workspace.active_pane().update(cx, |pane, cx| {
25740 pane.close_active_item(&CloseActiveItem::default(), window, cx)
25741 })
25742 })
25743 .unwrap();
25744 close.await.unwrap();
25745 assert_eq!(
25746 3,
25747 requests_made.load(atomic::Ordering::Acquire),
25748 "After saving and closing all editors, no extra requests should be made"
25749 );
25750 workspace
25751 .update(cx, |workspace, _, cx| {
25752 assert!(
25753 workspace.active_item(cx).is_none(),
25754 "Should close all editors"
25755 )
25756 })
25757 .unwrap();
25758
25759 workspace
25760 .update(cx, |workspace, window, cx| {
25761 workspace.active_pane().update(cx, |pane, cx| {
25762 pane.navigate_backward(&Default::default(), window, cx);
25763 })
25764 })
25765 .unwrap();
25766 cx.executor().advance_clock(Duration::from_millis(100));
25767 cx.run_until_parked();
25768 let editor = workspace
25769 .update(cx, |workspace, _, cx| {
25770 workspace
25771 .active_item(cx)
25772 .expect("Should have reopened the editor again after navigating back")
25773 .downcast::<Editor>()
25774 .expect("Should be an editor")
25775 })
25776 .unwrap();
25777 color_request_handle.next().await.unwrap();
25778 assert_eq!(
25779 3,
25780 requests_made.load(atomic::Ordering::Acquire),
25781 "Cache should be reused on buffer close and reopen"
25782 );
25783 editor.update(cx, |editor, cx| {
25784 assert_eq!(
25785 vec![expected_color],
25786 extract_color_inlays(editor, cx),
25787 "Should have an initial inlay"
25788 );
25789 });
25790}
25791
25792#[gpui::test]
25793async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
25794 init_test(cx, |_| {});
25795 let (editor, cx) = cx.add_window_view(Editor::single_line);
25796 editor.update_in(cx, |editor, window, cx| {
25797 editor.set_text("oops\n\nwow\n", window, cx)
25798 });
25799 cx.run_until_parked();
25800 editor.update(cx, |editor, cx| {
25801 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
25802 });
25803 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
25804 cx.run_until_parked();
25805 editor.update(cx, |editor, cx| {
25806 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
25807 });
25808}
25809
25810#[gpui::test]
25811async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
25812 init_test(cx, |_| {});
25813
25814 cx.update(|cx| {
25815 register_project_item::<Editor>(cx);
25816 });
25817
25818 let fs = FakeFs::new(cx.executor());
25819 fs.insert_tree("/root1", json!({})).await;
25820 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
25821 .await;
25822
25823 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
25824 let (workspace, cx) =
25825 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
25826
25827 let worktree_id = project.update(cx, |project, cx| {
25828 project.worktrees(cx).next().unwrap().read(cx).id()
25829 });
25830
25831 let handle = workspace
25832 .update_in(cx, |workspace, window, cx| {
25833 let project_path = (worktree_id, "one.pdf");
25834 workspace.open_path(project_path, None, true, window, cx)
25835 })
25836 .await
25837 .unwrap();
25838
25839 assert_eq!(
25840 handle.to_any().entity_type(),
25841 TypeId::of::<InvalidBufferView>()
25842 );
25843}
25844
25845#[gpui::test]
25846async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
25847 init_test(cx, |_| {});
25848
25849 let language = Arc::new(Language::new(
25850 LanguageConfig::default(),
25851 Some(tree_sitter_rust::LANGUAGE.into()),
25852 ));
25853
25854 // Test hierarchical sibling navigation
25855 let text = r#"
25856 fn outer() {
25857 if condition {
25858 let a = 1;
25859 }
25860 let b = 2;
25861 }
25862
25863 fn another() {
25864 let c = 3;
25865 }
25866 "#;
25867
25868 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
25869 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
25870 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
25871
25872 // Wait for parsing to complete
25873 editor
25874 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
25875 .await;
25876
25877 editor.update_in(cx, |editor, window, cx| {
25878 // Start by selecting "let a = 1;" inside the if block
25879 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25880 s.select_display_ranges([
25881 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
25882 ]);
25883 });
25884
25885 let initial_selection = editor.selections.display_ranges(cx);
25886 assert_eq!(initial_selection.len(), 1, "Should have one selection");
25887
25888 // Test select next sibling - should move up levels to find the next sibling
25889 // Since "let a = 1;" has no siblings in the if block, it should move up
25890 // to find "let b = 2;" which is a sibling of the if block
25891 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25892 let next_selection = editor.selections.display_ranges(cx);
25893
25894 // Should have a selection and it should be different from the initial
25895 assert_eq!(
25896 next_selection.len(),
25897 1,
25898 "Should have one selection after next"
25899 );
25900 assert_ne!(
25901 next_selection[0], initial_selection[0],
25902 "Next sibling selection should be different"
25903 );
25904
25905 // Test hierarchical navigation by going to the end of the current function
25906 // and trying to navigate to the next function
25907 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25908 s.select_display_ranges([
25909 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
25910 ]);
25911 });
25912
25913 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
25914 let function_next_selection = editor.selections.display_ranges(cx);
25915
25916 // Should move to the next function
25917 assert_eq!(
25918 function_next_selection.len(),
25919 1,
25920 "Should have one selection after function next"
25921 );
25922
25923 // Test select previous sibling navigation
25924 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
25925 let prev_selection = editor.selections.display_ranges(cx);
25926
25927 // Should have a selection and it should be different
25928 assert_eq!(
25929 prev_selection.len(),
25930 1,
25931 "Should have one selection after prev"
25932 );
25933 assert_ne!(
25934 prev_selection[0], function_next_selection[0],
25935 "Previous sibling selection should be different from next"
25936 );
25937 });
25938}
25939
25940#[gpui::test]
25941async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
25942 init_test(cx, |_| {});
25943
25944 let mut cx = EditorTestContext::new(cx).await;
25945 cx.set_state(
25946 "let ˇvariable = 42;
25947let another = variable + 1;
25948let result = variable * 2;",
25949 );
25950
25951 // Set up document highlights manually (simulating LSP response)
25952 cx.update_editor(|editor, _window, cx| {
25953 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
25954
25955 // Create highlights for "variable" occurrences
25956 let highlight_ranges = [
25957 Point::new(0, 4)..Point::new(0, 12), // First "variable"
25958 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
25959 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
25960 ];
25961
25962 let anchor_ranges: Vec<_> = highlight_ranges
25963 .iter()
25964 .map(|range| range.clone().to_anchors(&buffer_snapshot))
25965 .collect();
25966
25967 editor.highlight_background::<DocumentHighlightRead>(
25968 &anchor_ranges,
25969 |theme| theme.colors().editor_document_highlight_read_background,
25970 cx,
25971 );
25972 });
25973
25974 // Go to next highlight - should move to second "variable"
25975 cx.update_editor(|editor, window, cx| {
25976 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25977 });
25978 cx.assert_editor_state(
25979 "let variable = 42;
25980let another = ˇvariable + 1;
25981let result = variable * 2;",
25982 );
25983
25984 // Go to next highlight - should move to third "variable"
25985 cx.update_editor(|editor, window, cx| {
25986 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25987 });
25988 cx.assert_editor_state(
25989 "let variable = 42;
25990let another = variable + 1;
25991let result = ˇvariable * 2;",
25992 );
25993
25994 // Go to next highlight - should stay at third "variable" (no wrap-around)
25995 cx.update_editor(|editor, window, cx| {
25996 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
25997 });
25998 cx.assert_editor_state(
25999 "let variable = 42;
26000let another = variable + 1;
26001let result = ˇvariable * 2;",
26002 );
26003
26004 // Now test going backwards from third position
26005 cx.update_editor(|editor, window, cx| {
26006 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26007 });
26008 cx.assert_editor_state(
26009 "let variable = 42;
26010let another = ˇvariable + 1;
26011let result = variable * 2;",
26012 );
26013
26014 // Go to previous highlight - should move to first "variable"
26015 cx.update_editor(|editor, window, cx| {
26016 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26017 });
26018 cx.assert_editor_state(
26019 "let ˇvariable = 42;
26020let another = variable + 1;
26021let result = variable * 2;",
26022 );
26023
26024 // Go to previous highlight - should stay on first "variable"
26025 cx.update_editor(|editor, window, cx| {
26026 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26027 });
26028 cx.assert_editor_state(
26029 "let ˇvariable = 42;
26030let another = variable + 1;
26031let result = variable * 2;",
26032 );
26033}
26034
26035#[gpui::test]
26036async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26037 cx: &mut gpui::TestAppContext,
26038) {
26039 init_test(cx, |_| {});
26040
26041 let url = "https://zed.dev";
26042
26043 let markdown_language = Arc::new(Language::new(
26044 LanguageConfig {
26045 name: "Markdown".into(),
26046 ..LanguageConfig::default()
26047 },
26048 None,
26049 ));
26050
26051 let mut cx = EditorTestContext::new(cx).await;
26052 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26053 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26054
26055 cx.update_editor(|editor, window, cx| {
26056 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26057 editor.paste(&Paste, window, cx);
26058 });
26059
26060 cx.assert_editor_state(&format!(
26061 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26062 ));
26063}
26064
26065#[gpui::test]
26066async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26067 cx: &mut gpui::TestAppContext,
26068) {
26069 init_test(cx, |_| {});
26070
26071 let url = "https://zed.dev";
26072
26073 let markdown_language = Arc::new(Language::new(
26074 LanguageConfig {
26075 name: "Markdown".into(),
26076 ..LanguageConfig::default()
26077 },
26078 None,
26079 ));
26080
26081 let mut cx = EditorTestContext::new(cx).await;
26082 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26083 cx.set_state(&format!(
26084 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26085 ));
26086
26087 cx.update_editor(|editor, window, cx| {
26088 editor.copy(&Copy, window, cx);
26089 });
26090
26091 cx.set_state(&format!(
26092 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26093 ));
26094
26095 cx.update_editor(|editor, window, cx| {
26096 editor.paste(&Paste, window, cx);
26097 });
26098
26099 cx.assert_editor_state(&format!(
26100 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26101 ));
26102}
26103
26104#[gpui::test]
26105async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26106 cx: &mut gpui::TestAppContext,
26107) {
26108 init_test(cx, |_| {});
26109
26110 let url = "https://zed.dev";
26111
26112 let markdown_language = Arc::new(Language::new(
26113 LanguageConfig {
26114 name: "Markdown".into(),
26115 ..LanguageConfig::default()
26116 },
26117 None,
26118 ));
26119
26120 let mut cx = EditorTestContext::new(cx).await;
26121 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26122 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26123
26124 cx.update_editor(|editor, window, cx| {
26125 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26126 editor.paste(&Paste, window, cx);
26127 });
26128
26129 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26130}
26131
26132#[gpui::test]
26133async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26134 cx: &mut gpui::TestAppContext,
26135) {
26136 init_test(cx, |_| {});
26137
26138 let text = "Awesome";
26139
26140 let markdown_language = Arc::new(Language::new(
26141 LanguageConfig {
26142 name: "Markdown".into(),
26143 ..LanguageConfig::default()
26144 },
26145 None,
26146 ));
26147
26148 let mut cx = EditorTestContext::new(cx).await;
26149 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26150 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26151
26152 cx.update_editor(|editor, window, cx| {
26153 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26154 editor.paste(&Paste, window, cx);
26155 });
26156
26157 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26158}
26159
26160#[gpui::test]
26161async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26162 cx: &mut gpui::TestAppContext,
26163) {
26164 init_test(cx, |_| {});
26165
26166 let url = "https://zed.dev";
26167
26168 let markdown_language = Arc::new(Language::new(
26169 LanguageConfig {
26170 name: "Rust".into(),
26171 ..LanguageConfig::default()
26172 },
26173 None,
26174 ));
26175
26176 let mut cx = EditorTestContext::new(cx).await;
26177 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26178 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26179
26180 cx.update_editor(|editor, window, cx| {
26181 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26182 editor.paste(&Paste, window, cx);
26183 });
26184
26185 cx.assert_editor_state(&format!(
26186 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26187 ));
26188}
26189
26190#[gpui::test]
26191async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26192 cx: &mut TestAppContext,
26193) {
26194 init_test(cx, |_| {});
26195
26196 let url = "https://zed.dev";
26197
26198 let markdown_language = Arc::new(Language::new(
26199 LanguageConfig {
26200 name: "Markdown".into(),
26201 ..LanguageConfig::default()
26202 },
26203 None,
26204 ));
26205
26206 let (editor, cx) = cx.add_window_view(|window, cx| {
26207 let multi_buffer = MultiBuffer::build_multi(
26208 [
26209 ("this will embed -> link", vec![Point::row_range(0..1)]),
26210 ("this will replace -> link", vec![Point::row_range(0..1)]),
26211 ],
26212 cx,
26213 );
26214 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26215 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26216 s.select_ranges(vec![
26217 Point::new(0, 19)..Point::new(0, 23),
26218 Point::new(1, 21)..Point::new(1, 25),
26219 ])
26220 });
26221 let first_buffer_id = multi_buffer
26222 .read(cx)
26223 .excerpt_buffer_ids()
26224 .into_iter()
26225 .next()
26226 .unwrap();
26227 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26228 first_buffer.update(cx, |buffer, cx| {
26229 buffer.set_language(Some(markdown_language.clone()), cx);
26230 });
26231
26232 editor
26233 });
26234 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26235
26236 cx.update_editor(|editor, window, cx| {
26237 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26238 editor.paste(&Paste, window, cx);
26239 });
26240
26241 cx.assert_editor_state(&format!(
26242 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26243 ));
26244}
26245
26246#[track_caller]
26247fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26248 editor
26249 .all_inlays(cx)
26250 .into_iter()
26251 .filter_map(|inlay| inlay.get_color())
26252 .map(Rgba::from)
26253 .collect()
26254}