1use super::*;
2use crate::{
3 JoinLines,
4 code_context_menus::CodeContextMenu,
5 edit_prediction_tests::FakeEditPredictionProvider,
6 element::StickyHeader,
7 linked_editing_ranges::LinkedEditingRanges,
8 scroll::scroll_amount::ScrollAmount,
9 test::{
10 assert_text_with_selections, build_editor,
11 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
12 editor_test_context::EditorTestContext,
13 select_ranges,
14 },
15};
16use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
17use collections::HashMap;
18use futures::{StreamExt, channel::oneshot};
19use gpui::{
20 BackgroundExecutor, DismissEvent, Rgba, SemanticVersion, TestAppContext, UpdateGlobal,
21 VisualTestContext, WindowBounds, WindowOptions, div,
22};
23use indoc::indoc;
24use language::{
25 BracketPairConfig,
26 Capability::ReadWrite,
27 DiagnosticSourceKind, FakeLspAdapter, IndentGuideSettings, LanguageConfig,
28 LanguageConfigOverride, LanguageMatcher, LanguageName, Override, Point,
29 language_settings::{
30 CompletionSettingsContent, FormatterList, LanguageSettingsContent, LspInsertMode,
31 },
32 tree_sitter_python,
33};
34use language_settings::Formatter;
35use languages::rust_lang;
36use lsp::CompletionParams;
37use multi_buffer::{IndentGuide, PathKey};
38use parking_lot::Mutex;
39use pretty_assertions::{assert_eq, assert_ne};
40use project::{
41 FakeFs,
42 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
43 project_settings::LspSettings,
44};
45use serde_json::{self, json};
46use settings::{
47 AllLanguageSettingsContent, IndentGuideBackgroundColoring, IndentGuideColoring,
48 ProjectSettingsContent,
49};
50use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
51use std::{
52 iter,
53 sync::atomic::{self, AtomicUsize},
54};
55use test::build_editor_with_project;
56use text::ToPoint as _;
57use unindent::Unindent;
58use util::{
59 assert_set_eq, path,
60 rel_path::rel_path,
61 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
62 uri,
63};
64use workspace::{
65 CloseActiveItem, CloseAllItems, CloseOtherItems, MoveItemToPaneInDirection, NavigationEntry,
66 OpenOptions, ViewId,
67 invalid_item_view::InvalidItemView,
68 item::{FollowEvent, FollowableItem, Item, ItemHandle, SaveOptions},
69 register_project_item,
70};
71
72fn display_ranges(editor: &Editor, cx: &mut Context<'_, Editor>) -> Vec<Range<DisplayPoint>> {
73 editor
74 .selections
75 .display_ranges(&editor.display_snapshot(cx))
76}
77
78#[gpui::test]
79fn test_edit_events(cx: &mut TestAppContext) {
80 init_test(cx, |_| {});
81
82 let buffer = cx.new(|cx| {
83 let mut buffer = language::Buffer::local("123456", cx);
84 buffer.set_group_interval(Duration::from_secs(1));
85 buffer
86 });
87
88 let events = Rc::new(RefCell::new(Vec::new()));
89 let editor1 = cx.add_window({
90 let events = events.clone();
91 |window, cx| {
92 let entity = cx.entity();
93 cx.subscribe_in(
94 &entity,
95 window,
96 move |_, _, event: &EditorEvent, _, _| match event {
97 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
98 EditorEvent::BufferEdited => {
99 events.borrow_mut().push(("editor1", "buffer edited"))
100 }
101 _ => {}
102 },
103 )
104 .detach();
105 Editor::for_buffer(buffer.clone(), None, window, cx)
106 }
107 });
108
109 let editor2 = cx.add_window({
110 let events = events.clone();
111 |window, cx| {
112 cx.subscribe_in(
113 &cx.entity(),
114 window,
115 move |_, _, event: &EditorEvent, _, _| match event {
116 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
117 EditorEvent::BufferEdited => {
118 events.borrow_mut().push(("editor2", "buffer edited"))
119 }
120 _ => {}
121 },
122 )
123 .detach();
124 Editor::for_buffer(buffer.clone(), None, window, cx)
125 }
126 });
127
128 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
129
130 // Mutating editor 1 will emit an `Edited` event only for that editor.
131 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
132 assert_eq!(
133 mem::take(&mut *events.borrow_mut()),
134 [
135 ("editor1", "edited"),
136 ("editor1", "buffer edited"),
137 ("editor2", "buffer edited"),
138 ]
139 );
140
141 // Mutating editor 2 will emit an `Edited` event only for that editor.
142 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
143 assert_eq!(
144 mem::take(&mut *events.borrow_mut()),
145 [
146 ("editor2", "edited"),
147 ("editor1", "buffer edited"),
148 ("editor2", "buffer edited"),
149 ]
150 );
151
152 // Undoing on editor 1 will emit an `Edited` event only for that editor.
153 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
154 assert_eq!(
155 mem::take(&mut *events.borrow_mut()),
156 [
157 ("editor1", "edited"),
158 ("editor1", "buffer edited"),
159 ("editor2", "buffer edited"),
160 ]
161 );
162
163 // Redoing on editor 1 will emit an `Edited` event only for that editor.
164 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
165 assert_eq!(
166 mem::take(&mut *events.borrow_mut()),
167 [
168 ("editor1", "edited"),
169 ("editor1", "buffer edited"),
170 ("editor2", "buffer edited"),
171 ]
172 );
173
174 // Undoing on editor 2 will emit an `Edited` event only for that editor.
175 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
176 assert_eq!(
177 mem::take(&mut *events.borrow_mut()),
178 [
179 ("editor2", "edited"),
180 ("editor1", "buffer edited"),
181 ("editor2", "buffer edited"),
182 ]
183 );
184
185 // Redoing on editor 2 will emit an `Edited` event only for that editor.
186 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
187 assert_eq!(
188 mem::take(&mut *events.borrow_mut()),
189 [
190 ("editor2", "edited"),
191 ("editor1", "buffer edited"),
192 ("editor2", "buffer edited"),
193 ]
194 );
195
196 // No event is emitted when the mutation is a no-op.
197 _ = editor2.update(cx, |editor, window, cx| {
198 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
199 s.select_ranges([0..0])
200 });
201
202 editor.backspace(&Backspace, window, cx);
203 });
204 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
205}
206
207#[gpui::test]
208fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
209 init_test(cx, |_| {});
210
211 let mut now = Instant::now();
212 let group_interval = Duration::from_millis(1);
213 let buffer = cx.new(|cx| {
214 let mut buf = language::Buffer::local("123456", cx);
215 buf.set_group_interval(group_interval);
216 buf
217 });
218 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
219 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
220
221 _ = editor.update(cx, |editor, window, cx| {
222 editor.start_transaction_at(now, window, cx);
223 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
224 s.select_ranges([2..4])
225 });
226
227 editor.insert("cd", window, cx);
228 editor.end_transaction_at(now, cx);
229 assert_eq!(editor.text(cx), "12cd56");
230 assert_eq!(
231 editor.selections.ranges(&editor.display_snapshot(cx)),
232 vec![4..4]
233 );
234
235 editor.start_transaction_at(now, window, cx);
236 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
237 s.select_ranges([4..5])
238 });
239 editor.insert("e", window, cx);
240 editor.end_transaction_at(now, cx);
241 assert_eq!(editor.text(cx), "12cde6");
242 assert_eq!(
243 editor.selections.ranges(&editor.display_snapshot(cx)),
244 vec![5..5]
245 );
246
247 now += group_interval + Duration::from_millis(1);
248 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
249 s.select_ranges([2..2])
250 });
251
252 // Simulate an edit in another editor
253 buffer.update(cx, |buffer, cx| {
254 buffer.start_transaction_at(now, cx);
255 buffer.edit([(0..1, "a")], None, cx);
256 buffer.edit([(1..1, "b")], None, cx);
257 buffer.end_transaction_at(now, cx);
258 });
259
260 assert_eq!(editor.text(cx), "ab2cde6");
261 assert_eq!(
262 editor.selections.ranges(&editor.display_snapshot(cx)),
263 vec![3..3]
264 );
265
266 // Last transaction happened past the group interval in a different editor.
267 // Undo it individually and don't restore selections.
268 editor.undo(&Undo, window, cx);
269 assert_eq!(editor.text(cx), "12cde6");
270 assert_eq!(
271 editor.selections.ranges(&editor.display_snapshot(cx)),
272 vec![2..2]
273 );
274
275 // First two transactions happened within the group interval in this editor.
276 // Undo them together and restore selections.
277 editor.undo(&Undo, window, cx);
278 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
279 assert_eq!(editor.text(cx), "123456");
280 assert_eq!(
281 editor.selections.ranges(&editor.display_snapshot(cx)),
282 vec![0..0]
283 );
284
285 // Redo the first two transactions together.
286 editor.redo(&Redo, window, cx);
287 assert_eq!(editor.text(cx), "12cde6");
288 assert_eq!(
289 editor.selections.ranges(&editor.display_snapshot(cx)),
290 vec![5..5]
291 );
292
293 // Redo the last transaction on its own.
294 editor.redo(&Redo, window, cx);
295 assert_eq!(editor.text(cx), "ab2cde6");
296 assert_eq!(
297 editor.selections.ranges(&editor.display_snapshot(cx)),
298 vec![6..6]
299 );
300
301 // Test empty transactions.
302 editor.start_transaction_at(now, window, cx);
303 editor.end_transaction_at(now, cx);
304 editor.undo(&Undo, window, cx);
305 assert_eq!(editor.text(cx), "12cde6");
306 });
307}
308
309#[gpui::test]
310fn test_ime_composition(cx: &mut TestAppContext) {
311 init_test(cx, |_| {});
312
313 let buffer = cx.new(|cx| {
314 let mut buffer = language::Buffer::local("abcde", cx);
315 // Ensure automatic grouping doesn't occur.
316 buffer.set_group_interval(Duration::ZERO);
317 buffer
318 });
319
320 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
321 cx.add_window(|window, cx| {
322 let mut editor = build_editor(buffer.clone(), window, cx);
323
324 // Start a new IME composition.
325 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
326 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
327 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
328 assert_eq!(editor.text(cx), "äbcde");
329 assert_eq!(
330 editor.marked_text_ranges(cx),
331 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
332 );
333
334 // Finalize IME composition.
335 editor.replace_text_in_range(None, "ā", window, cx);
336 assert_eq!(editor.text(cx), "ābcde");
337 assert_eq!(editor.marked_text_ranges(cx), None);
338
339 // IME composition edits are grouped and are undone/redone at once.
340 editor.undo(&Default::default(), window, cx);
341 assert_eq!(editor.text(cx), "abcde");
342 assert_eq!(editor.marked_text_ranges(cx), None);
343 editor.redo(&Default::default(), window, cx);
344 assert_eq!(editor.text(cx), "ābcde");
345 assert_eq!(editor.marked_text_ranges(cx), None);
346
347 // Start a new IME composition.
348 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
349 assert_eq!(
350 editor.marked_text_ranges(cx),
351 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
352 );
353
354 // Undoing during an IME composition cancels it.
355 editor.undo(&Default::default(), window, cx);
356 assert_eq!(editor.text(cx), "ābcde");
357 assert_eq!(editor.marked_text_ranges(cx), None);
358
359 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
360 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
361 assert_eq!(editor.text(cx), "ābcdè");
362 assert_eq!(
363 editor.marked_text_ranges(cx),
364 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
365 );
366
367 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
368 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
369 assert_eq!(editor.text(cx), "ābcdę");
370 assert_eq!(editor.marked_text_ranges(cx), None);
371
372 // Start a new IME composition with multiple cursors.
373 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
374 s.select_ranges([
375 OffsetUtf16(1)..OffsetUtf16(1),
376 OffsetUtf16(3)..OffsetUtf16(3),
377 OffsetUtf16(5)..OffsetUtf16(5),
378 ])
379 });
380 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
381 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
382 assert_eq!(
383 editor.marked_text_ranges(cx),
384 Some(vec![
385 OffsetUtf16(0)..OffsetUtf16(3),
386 OffsetUtf16(4)..OffsetUtf16(7),
387 OffsetUtf16(8)..OffsetUtf16(11)
388 ])
389 );
390
391 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
392 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
393 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
394 assert_eq!(
395 editor.marked_text_ranges(cx),
396 Some(vec![
397 OffsetUtf16(1)..OffsetUtf16(2),
398 OffsetUtf16(5)..OffsetUtf16(6),
399 OffsetUtf16(9)..OffsetUtf16(10)
400 ])
401 );
402
403 // Finalize IME composition with multiple cursors.
404 editor.replace_text_in_range(Some(9..10), "2", window, cx);
405 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
406 assert_eq!(editor.marked_text_ranges(cx), None);
407
408 editor
409 });
410}
411
412#[gpui::test]
413fn test_selection_with_mouse(cx: &mut TestAppContext) {
414 init_test(cx, |_| {});
415
416 let editor = cx.add_window(|window, cx| {
417 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
418 build_editor(buffer, window, cx)
419 });
420
421 _ = editor.update(cx, |editor, window, cx| {
422 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
423 });
424 assert_eq!(
425 editor
426 .update(cx, |editor, _, cx| display_ranges(editor, cx))
427 .unwrap(),
428 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
429 );
430
431 _ = editor.update(cx, |editor, window, cx| {
432 editor.update_selection(
433 DisplayPoint::new(DisplayRow(3), 3),
434 0,
435 gpui::Point::<f32>::default(),
436 window,
437 cx,
438 );
439 });
440
441 assert_eq!(
442 editor
443 .update(cx, |editor, _, cx| display_ranges(editor, cx))
444 .unwrap(),
445 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
446 );
447
448 _ = editor.update(cx, |editor, window, cx| {
449 editor.update_selection(
450 DisplayPoint::new(DisplayRow(1), 1),
451 0,
452 gpui::Point::<f32>::default(),
453 window,
454 cx,
455 );
456 });
457
458 assert_eq!(
459 editor
460 .update(cx, |editor, _, cx| display_ranges(editor, cx))
461 .unwrap(),
462 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
463 );
464
465 _ = editor.update(cx, |editor, window, cx| {
466 editor.end_selection(window, cx);
467 editor.update_selection(
468 DisplayPoint::new(DisplayRow(3), 3),
469 0,
470 gpui::Point::<f32>::default(),
471 window,
472 cx,
473 );
474 });
475
476 assert_eq!(
477 editor
478 .update(cx, |editor, _, cx| display_ranges(editor, cx))
479 .unwrap(),
480 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
481 );
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
485 editor.update_selection(
486 DisplayPoint::new(DisplayRow(0), 0),
487 0,
488 gpui::Point::<f32>::default(),
489 window,
490 cx,
491 );
492 });
493
494 assert_eq!(
495 editor
496 .update(cx, |editor, _, cx| display_ranges(editor, cx))
497 .unwrap(),
498 [
499 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
500 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
501 ]
502 );
503
504 _ = editor.update(cx, |editor, window, cx| {
505 editor.end_selection(window, cx);
506 });
507
508 assert_eq!(
509 editor
510 .update(cx, |editor, _, cx| display_ranges(editor, cx))
511 .unwrap(),
512 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
513 );
514}
515
516#[gpui::test]
517fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
518 init_test(cx, |_| {});
519
520 let editor = cx.add_window(|window, cx| {
521 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
522 build_editor(buffer, window, cx)
523 });
524
525 _ = editor.update(cx, |editor, window, cx| {
526 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
527 });
528
529 _ = editor.update(cx, |editor, window, cx| {
530 editor.end_selection(window, cx);
531 });
532
533 _ = editor.update(cx, |editor, window, cx| {
534 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
535 });
536
537 _ = editor.update(cx, |editor, window, cx| {
538 editor.end_selection(window, cx);
539 });
540
541 assert_eq!(
542 editor
543 .update(cx, |editor, _, cx| display_ranges(editor, cx))
544 .unwrap(),
545 [
546 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
547 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
548 ]
549 );
550
551 _ = editor.update(cx, |editor, window, cx| {
552 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
553 });
554
555 _ = editor.update(cx, |editor, window, cx| {
556 editor.end_selection(window, cx);
557 });
558
559 assert_eq!(
560 editor
561 .update(cx, |editor, _, cx| display_ranges(editor, cx))
562 .unwrap(),
563 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
564 );
565}
566
567#[gpui::test]
568fn test_canceling_pending_selection(cx: &mut TestAppContext) {
569 init_test(cx, |_| {});
570
571 let editor = cx.add_window(|window, cx| {
572 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
573 build_editor(buffer, window, cx)
574 });
575
576 _ = editor.update(cx, |editor, window, cx| {
577 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
578 assert_eq!(
579 display_ranges(editor, cx),
580 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
581 );
582 });
583
584 _ = editor.update(cx, |editor, window, cx| {
585 editor.update_selection(
586 DisplayPoint::new(DisplayRow(3), 3),
587 0,
588 gpui::Point::<f32>::default(),
589 window,
590 cx,
591 );
592 assert_eq!(
593 display_ranges(editor, cx),
594 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
595 );
596 });
597
598 _ = editor.update(cx, |editor, window, cx| {
599 editor.cancel(&Cancel, window, cx);
600 editor.update_selection(
601 DisplayPoint::new(DisplayRow(1), 1),
602 0,
603 gpui::Point::<f32>::default(),
604 window,
605 cx,
606 );
607 assert_eq!(
608 display_ranges(editor, cx),
609 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
610 );
611 });
612}
613
614#[gpui::test]
615fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
616 init_test(cx, |_| {});
617
618 let editor = cx.add_window(|window, cx| {
619 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
620 build_editor(buffer, window, cx)
621 });
622
623 _ = editor.update(cx, |editor, window, cx| {
624 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
625 assert_eq!(
626 display_ranges(editor, cx),
627 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
628 );
629
630 editor.move_down(&Default::default(), window, cx);
631 assert_eq!(
632 display_ranges(editor, cx),
633 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
634 );
635
636 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
637 assert_eq!(
638 display_ranges(editor, cx),
639 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
640 );
641
642 editor.move_up(&Default::default(), window, cx);
643 assert_eq!(
644 display_ranges(editor, cx),
645 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
646 );
647 });
648}
649
650#[gpui::test]
651fn test_extending_selection(cx: &mut TestAppContext) {
652 init_test(cx, |_| {});
653
654 let editor = cx.add_window(|window, cx| {
655 let buffer = MultiBuffer::build_simple("aaa bbb ccc ddd eee", cx);
656 build_editor(buffer, window, cx)
657 });
658
659 _ = editor.update(cx, |editor, window, cx| {
660 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), false, 1, window, cx);
661 editor.end_selection(window, cx);
662 assert_eq!(
663 display_ranges(editor, cx),
664 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)]
665 );
666
667 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
668 editor.end_selection(window, cx);
669 assert_eq!(
670 display_ranges(editor, cx),
671 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 10)]
672 );
673
674 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
675 editor.end_selection(window, cx);
676 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 2, window, cx);
677 assert_eq!(
678 display_ranges(editor, cx),
679 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 11)]
680 );
681
682 editor.update_selection(
683 DisplayPoint::new(DisplayRow(0), 1),
684 0,
685 gpui::Point::<f32>::default(),
686 window,
687 cx,
688 );
689 editor.end_selection(window, cx);
690 assert_eq!(
691 display_ranges(editor, cx),
692 [DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 0)]
693 );
694
695 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 1, window, cx);
696 editor.end_selection(window, cx);
697 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 5), true, 2, window, cx);
698 editor.end_selection(window, cx);
699 assert_eq!(
700 display_ranges(editor, cx),
701 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
702 );
703
704 editor.extend_selection(DisplayPoint::new(DisplayRow(0), 10), 1, window, cx);
705 assert_eq!(
706 display_ranges(editor, cx),
707 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 11)]
708 );
709
710 editor.update_selection(
711 DisplayPoint::new(DisplayRow(0), 6),
712 0,
713 gpui::Point::<f32>::default(),
714 window,
715 cx,
716 );
717 assert_eq!(
718 display_ranges(editor, cx),
719 [DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 7)]
720 );
721
722 editor.update_selection(
723 DisplayPoint::new(DisplayRow(0), 1),
724 0,
725 gpui::Point::<f32>::default(),
726 window,
727 cx,
728 );
729 editor.end_selection(window, cx);
730 assert_eq!(
731 display_ranges(editor, cx),
732 [DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 0)]
733 );
734 });
735}
736
737#[gpui::test]
738fn test_clone(cx: &mut TestAppContext) {
739 init_test(cx, |_| {});
740
741 let (text, selection_ranges) = marked_text_ranges(
742 indoc! {"
743 one
744 two
745 threeˇ
746 four
747 fiveˇ
748 "},
749 true,
750 );
751
752 let editor = cx.add_window(|window, cx| {
753 let buffer = MultiBuffer::build_simple(&text, cx);
754 build_editor(buffer, window, cx)
755 });
756
757 _ = editor.update(cx, |editor, window, cx| {
758 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
759 s.select_ranges(selection_ranges.clone())
760 });
761 editor.fold_creases(
762 vec![
763 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
764 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
765 ],
766 true,
767 window,
768 cx,
769 );
770 });
771
772 let cloned_editor = editor
773 .update(cx, |editor, _, cx| {
774 cx.open_window(Default::default(), |window, cx| {
775 cx.new(|cx| editor.clone(window, cx))
776 })
777 })
778 .unwrap()
779 .unwrap();
780
781 let snapshot = editor
782 .update(cx, |e, window, cx| e.snapshot(window, cx))
783 .unwrap();
784 let cloned_snapshot = cloned_editor
785 .update(cx, |e, window, cx| e.snapshot(window, cx))
786 .unwrap();
787
788 assert_eq!(
789 cloned_editor
790 .update(cx, |e, _, cx| e.display_text(cx))
791 .unwrap(),
792 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
793 );
794 assert_eq!(
795 cloned_snapshot
796 .folds_in_range(0..text.len())
797 .collect::<Vec<_>>(),
798 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
799 );
800 assert_set_eq!(
801 cloned_editor
802 .update(cx, |editor, _, cx| editor
803 .selections
804 .ranges::<Point>(&editor.display_snapshot(cx)))
805 .unwrap(),
806 editor
807 .update(cx, |editor, _, cx| editor
808 .selections
809 .ranges(&editor.display_snapshot(cx)))
810 .unwrap()
811 );
812 assert_set_eq!(
813 cloned_editor
814 .update(cx, |e, _window, cx| e
815 .selections
816 .display_ranges(&e.display_snapshot(cx)))
817 .unwrap(),
818 editor
819 .update(cx, |e, _, cx| e
820 .selections
821 .display_ranges(&e.display_snapshot(cx)))
822 .unwrap()
823 );
824}
825
826#[gpui::test]
827async fn test_navigation_history(cx: &mut TestAppContext) {
828 init_test(cx, |_| {});
829
830 use workspace::item::Item;
831
832 let fs = FakeFs::new(cx.executor());
833 let project = Project::test(fs, [], cx).await;
834 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
835 let pane = workspace
836 .update(cx, |workspace, _, _| workspace.active_pane().clone())
837 .unwrap();
838
839 _ = workspace.update(cx, |_v, window, cx| {
840 cx.new(|cx| {
841 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
842 let mut editor = build_editor(buffer, window, cx);
843 let handle = cx.entity();
844 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
845
846 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
847 editor.nav_history.as_mut().unwrap().pop_backward(cx)
848 }
849
850 // Move the cursor a small distance.
851 // Nothing is added to the navigation history.
852 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
853 s.select_display_ranges([
854 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
855 ])
856 });
857 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
858 s.select_display_ranges([
859 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
860 ])
861 });
862 assert!(pop_history(&mut editor, cx).is_none());
863
864 // Move the cursor a large distance.
865 // The history can jump back to the previous position.
866 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
867 s.select_display_ranges([
868 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
869 ])
870 });
871 let nav_entry = pop_history(&mut editor, cx).unwrap();
872 editor.navigate(nav_entry.data.unwrap(), window, cx);
873 assert_eq!(nav_entry.item.id(), cx.entity_id());
874 assert_eq!(
875 editor
876 .selections
877 .display_ranges(&editor.display_snapshot(cx)),
878 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
879 );
880 assert!(pop_history(&mut editor, cx).is_none());
881
882 // Move the cursor a small distance via the mouse.
883 // Nothing is added to the navigation history.
884 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
885 editor.end_selection(window, cx);
886 assert_eq!(
887 editor
888 .selections
889 .display_ranges(&editor.display_snapshot(cx)),
890 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
891 );
892 assert!(pop_history(&mut editor, cx).is_none());
893
894 // Move the cursor a large distance via the mouse.
895 // The history can jump back to the previous position.
896 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
897 editor.end_selection(window, cx);
898 assert_eq!(
899 editor
900 .selections
901 .display_ranges(&editor.display_snapshot(cx)),
902 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
903 );
904 let nav_entry = pop_history(&mut editor, cx).unwrap();
905 editor.navigate(nav_entry.data.unwrap(), window, cx);
906 assert_eq!(nav_entry.item.id(), cx.entity_id());
907 assert_eq!(
908 editor
909 .selections
910 .display_ranges(&editor.display_snapshot(cx)),
911 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
912 );
913 assert!(pop_history(&mut editor, cx).is_none());
914
915 // Set scroll position to check later
916 editor.set_scroll_position(gpui::Point::<f64>::new(5.5, 5.5), window, cx);
917 let original_scroll_position = editor.scroll_manager.anchor();
918
919 // Jump to the end of the document and adjust scroll
920 editor.move_to_end(&MoveToEnd, window, cx);
921 editor.set_scroll_position(gpui::Point::<f64>::new(-2.5, -0.5), window, cx);
922 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
923
924 let nav_entry = pop_history(&mut editor, cx).unwrap();
925 editor.navigate(nav_entry.data.unwrap(), window, cx);
926 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
927
928 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
929 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
930 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
931 let invalid_point = Point::new(9999, 0);
932 editor.navigate(
933 Box::new(NavigationData {
934 cursor_anchor: invalid_anchor,
935 cursor_position: invalid_point,
936 scroll_anchor: ScrollAnchor {
937 anchor: invalid_anchor,
938 offset: Default::default(),
939 },
940 scroll_top_row: invalid_point.row,
941 }),
942 window,
943 cx,
944 );
945 assert_eq!(
946 editor
947 .selections
948 .display_ranges(&editor.display_snapshot(cx)),
949 &[editor.max_point(cx)..editor.max_point(cx)]
950 );
951 assert_eq!(
952 editor.scroll_position(cx),
953 gpui::Point::new(0., editor.max_point(cx).row().as_f64())
954 );
955
956 editor
957 })
958 });
959}
960
961#[gpui::test]
962fn test_cancel(cx: &mut TestAppContext) {
963 init_test(cx, |_| {});
964
965 let editor = cx.add_window(|window, cx| {
966 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
967 build_editor(buffer, window, cx)
968 });
969
970 _ = editor.update(cx, |editor, window, cx| {
971 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
972 editor.update_selection(
973 DisplayPoint::new(DisplayRow(1), 1),
974 0,
975 gpui::Point::<f32>::default(),
976 window,
977 cx,
978 );
979 editor.end_selection(window, cx);
980
981 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
982 editor.update_selection(
983 DisplayPoint::new(DisplayRow(0), 3),
984 0,
985 gpui::Point::<f32>::default(),
986 window,
987 cx,
988 );
989 editor.end_selection(window, cx);
990 assert_eq!(
991 display_ranges(editor, cx),
992 [
993 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
994 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
995 ]
996 );
997 });
998
999 _ = editor.update(cx, |editor, window, cx| {
1000 editor.cancel(&Cancel, window, cx);
1001 assert_eq!(
1002 display_ranges(editor, cx),
1003 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
1004 );
1005 });
1006
1007 _ = editor.update(cx, |editor, window, cx| {
1008 editor.cancel(&Cancel, window, cx);
1009 assert_eq!(
1010 display_ranges(editor, cx),
1011 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
1012 );
1013 });
1014}
1015
1016#[gpui::test]
1017fn test_fold_action(cx: &mut TestAppContext) {
1018 init_test(cx, |_| {});
1019
1020 let editor = cx.add_window(|window, cx| {
1021 let buffer = MultiBuffer::build_simple(
1022 &"
1023 impl Foo {
1024 // Hello!
1025
1026 fn a() {
1027 1
1028 }
1029
1030 fn b() {
1031 2
1032 }
1033
1034 fn c() {
1035 3
1036 }
1037 }
1038 "
1039 .unindent(),
1040 cx,
1041 );
1042 build_editor(buffer, window, cx)
1043 });
1044
1045 _ = editor.update(cx, |editor, window, cx| {
1046 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1047 s.select_display_ranges([
1048 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
1049 ]);
1050 });
1051 editor.fold(&Fold, window, cx);
1052 assert_eq!(
1053 editor.display_text(cx),
1054 "
1055 impl Foo {
1056 // Hello!
1057
1058 fn a() {
1059 1
1060 }
1061
1062 fn b() {⋯
1063 }
1064
1065 fn c() {⋯
1066 }
1067 }
1068 "
1069 .unindent(),
1070 );
1071
1072 editor.fold(&Fold, window, cx);
1073 assert_eq!(
1074 editor.display_text(cx),
1075 "
1076 impl Foo {⋯
1077 }
1078 "
1079 .unindent(),
1080 );
1081
1082 editor.unfold_lines(&UnfoldLines, window, cx);
1083 assert_eq!(
1084 editor.display_text(cx),
1085 "
1086 impl Foo {
1087 // Hello!
1088
1089 fn a() {
1090 1
1091 }
1092
1093 fn b() {⋯
1094 }
1095
1096 fn c() {⋯
1097 }
1098 }
1099 "
1100 .unindent(),
1101 );
1102
1103 editor.unfold_lines(&UnfoldLines, window, cx);
1104 assert_eq!(
1105 editor.display_text(cx),
1106 editor.buffer.read(cx).read(cx).text()
1107 );
1108 });
1109}
1110
1111#[gpui::test]
1112fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
1113 init_test(cx, |_| {});
1114
1115 let editor = cx.add_window(|window, cx| {
1116 let buffer = MultiBuffer::build_simple(
1117 &"
1118 class Foo:
1119 # Hello!
1120
1121 def a():
1122 print(1)
1123
1124 def b():
1125 print(2)
1126
1127 def c():
1128 print(3)
1129 "
1130 .unindent(),
1131 cx,
1132 );
1133 build_editor(buffer, window, cx)
1134 });
1135
1136 _ = editor.update(cx, |editor, window, cx| {
1137 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1138 s.select_display_ranges([
1139 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
1140 ]);
1141 });
1142 editor.fold(&Fold, window, cx);
1143 assert_eq!(
1144 editor.display_text(cx),
1145 "
1146 class Foo:
1147 # Hello!
1148
1149 def a():
1150 print(1)
1151
1152 def b():⋯
1153
1154 def c():⋯
1155 "
1156 .unindent(),
1157 );
1158
1159 editor.fold(&Fold, window, cx);
1160 assert_eq!(
1161 editor.display_text(cx),
1162 "
1163 class Foo:⋯
1164 "
1165 .unindent(),
1166 );
1167
1168 editor.unfold_lines(&UnfoldLines, window, cx);
1169 assert_eq!(
1170 editor.display_text(cx),
1171 "
1172 class Foo:
1173 # Hello!
1174
1175 def a():
1176 print(1)
1177
1178 def b():⋯
1179
1180 def c():⋯
1181 "
1182 .unindent(),
1183 );
1184
1185 editor.unfold_lines(&UnfoldLines, window, cx);
1186 assert_eq!(
1187 editor.display_text(cx),
1188 editor.buffer.read(cx).read(cx).text()
1189 );
1190 });
1191}
1192
1193#[gpui::test]
1194fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1195 init_test(cx, |_| {});
1196
1197 let editor = cx.add_window(|window, cx| {
1198 let buffer = MultiBuffer::build_simple(
1199 &"
1200 class Foo:
1201 # Hello!
1202
1203 def a():
1204 print(1)
1205
1206 def b():
1207 print(2)
1208
1209
1210 def c():
1211 print(3)
1212
1213
1214 "
1215 .unindent(),
1216 cx,
1217 );
1218 build_editor(buffer, window, cx)
1219 });
1220
1221 _ = editor.update(cx, |editor, window, cx| {
1222 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1223 s.select_display_ranges([
1224 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1225 ]);
1226 });
1227 editor.fold(&Fold, window, cx);
1228 assert_eq!(
1229 editor.display_text(cx),
1230 "
1231 class Foo:
1232 # Hello!
1233
1234 def a():
1235 print(1)
1236
1237 def b():⋯
1238
1239
1240 def c():⋯
1241
1242
1243 "
1244 .unindent(),
1245 );
1246
1247 editor.fold(&Fold, window, cx);
1248 assert_eq!(
1249 editor.display_text(cx),
1250 "
1251 class Foo:⋯
1252
1253
1254 "
1255 .unindent(),
1256 );
1257
1258 editor.unfold_lines(&UnfoldLines, window, cx);
1259 assert_eq!(
1260 editor.display_text(cx),
1261 "
1262 class Foo:
1263 # Hello!
1264
1265 def a():
1266 print(1)
1267
1268 def b():⋯
1269
1270
1271 def c():⋯
1272
1273
1274 "
1275 .unindent(),
1276 );
1277
1278 editor.unfold_lines(&UnfoldLines, window, cx);
1279 assert_eq!(
1280 editor.display_text(cx),
1281 editor.buffer.read(cx).read(cx).text()
1282 );
1283 });
1284}
1285
1286#[gpui::test]
1287fn test_fold_at_level(cx: &mut TestAppContext) {
1288 init_test(cx, |_| {});
1289
1290 let editor = cx.add_window(|window, cx| {
1291 let buffer = MultiBuffer::build_simple(
1292 &"
1293 class Foo:
1294 # Hello!
1295
1296 def a():
1297 print(1)
1298
1299 def b():
1300 print(2)
1301
1302
1303 class Bar:
1304 # World!
1305
1306 def a():
1307 print(1)
1308
1309 def b():
1310 print(2)
1311
1312
1313 "
1314 .unindent(),
1315 cx,
1316 );
1317 build_editor(buffer, window, cx)
1318 });
1319
1320 _ = editor.update(cx, |editor, window, cx| {
1321 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1322 assert_eq!(
1323 editor.display_text(cx),
1324 "
1325 class Foo:
1326 # Hello!
1327
1328 def a():⋯
1329
1330 def b():⋯
1331
1332
1333 class Bar:
1334 # World!
1335
1336 def a():⋯
1337
1338 def b():⋯
1339
1340
1341 "
1342 .unindent(),
1343 );
1344
1345 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1346 assert_eq!(
1347 editor.display_text(cx),
1348 "
1349 class Foo:⋯
1350
1351
1352 class Bar:⋯
1353
1354
1355 "
1356 .unindent(),
1357 );
1358
1359 editor.unfold_all(&UnfoldAll, window, cx);
1360 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1361 assert_eq!(
1362 editor.display_text(cx),
1363 "
1364 class Foo:
1365 # Hello!
1366
1367 def a():
1368 print(1)
1369
1370 def b():
1371 print(2)
1372
1373
1374 class Bar:
1375 # World!
1376
1377 def a():
1378 print(1)
1379
1380 def b():
1381 print(2)
1382
1383
1384 "
1385 .unindent(),
1386 );
1387
1388 assert_eq!(
1389 editor.display_text(cx),
1390 editor.buffer.read(cx).read(cx).text()
1391 );
1392 let (_, positions) = marked_text_ranges(
1393 &"
1394 class Foo:
1395 # Hello!
1396
1397 def a():
1398 print(1)
1399
1400 def b():
1401 p«riˇ»nt(2)
1402
1403
1404 class Bar:
1405 # World!
1406
1407 def a():
1408 «ˇprint(1)
1409
1410 def b():
1411 print(2)»
1412
1413
1414 "
1415 .unindent(),
1416 true,
1417 );
1418
1419 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
1420 s.select_ranges(positions)
1421 });
1422
1423 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1424 assert_eq!(
1425 editor.display_text(cx),
1426 "
1427 class Foo:
1428 # Hello!
1429
1430 def a():⋯
1431
1432 def b():
1433 print(2)
1434
1435
1436 class Bar:
1437 # World!
1438
1439 def a():
1440 print(1)
1441
1442 def b():
1443 print(2)
1444
1445
1446 "
1447 .unindent(),
1448 );
1449 });
1450}
1451
1452#[gpui::test]
1453fn test_move_cursor(cx: &mut TestAppContext) {
1454 init_test(cx, |_| {});
1455
1456 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1457 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1458
1459 buffer.update(cx, |buffer, cx| {
1460 buffer.edit(
1461 vec![
1462 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1463 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1464 ],
1465 None,
1466 cx,
1467 );
1468 });
1469 _ = editor.update(cx, |editor, window, cx| {
1470 assert_eq!(
1471 display_ranges(editor, cx),
1472 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1473 );
1474
1475 editor.move_down(&MoveDown, window, cx);
1476 assert_eq!(
1477 display_ranges(editor, cx),
1478 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1479 );
1480
1481 editor.move_right(&MoveRight, window, cx);
1482 assert_eq!(
1483 display_ranges(editor, cx),
1484 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1485 );
1486
1487 editor.move_left(&MoveLeft, window, cx);
1488 assert_eq!(
1489 display_ranges(editor, cx),
1490 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1491 );
1492
1493 editor.move_up(&MoveUp, window, cx);
1494 assert_eq!(
1495 display_ranges(editor, cx),
1496 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1497 );
1498
1499 editor.move_to_end(&MoveToEnd, window, cx);
1500 assert_eq!(
1501 display_ranges(editor, cx),
1502 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1503 );
1504
1505 editor.move_to_beginning(&MoveToBeginning, window, cx);
1506 assert_eq!(
1507 display_ranges(editor, cx),
1508 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1509 );
1510
1511 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1512 s.select_display_ranges([
1513 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1514 ]);
1515 });
1516 editor.select_to_beginning(&SelectToBeginning, window, cx);
1517 assert_eq!(
1518 display_ranges(editor, cx),
1519 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1520 );
1521
1522 editor.select_to_end(&SelectToEnd, window, cx);
1523 assert_eq!(
1524 display_ranges(editor, cx),
1525 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1526 );
1527 });
1528}
1529
1530#[gpui::test]
1531fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1532 init_test(cx, |_| {});
1533
1534 let editor = cx.add_window(|window, cx| {
1535 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1536 build_editor(buffer, window, cx)
1537 });
1538
1539 assert_eq!('🟥'.len_utf8(), 4);
1540 assert_eq!('α'.len_utf8(), 2);
1541
1542 _ = editor.update(cx, |editor, window, cx| {
1543 editor.fold_creases(
1544 vec![
1545 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1546 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1547 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1548 ],
1549 true,
1550 window,
1551 cx,
1552 );
1553 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1554
1555 editor.move_right(&MoveRight, window, cx);
1556 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1557 editor.move_right(&MoveRight, window, cx);
1558 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1559 editor.move_right(&MoveRight, window, cx);
1560 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧⋯".len())]);
1561
1562 editor.move_down(&MoveDown, window, cx);
1563 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1564 editor.move_left(&MoveLeft, window, cx);
1565 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯".len())]);
1566 editor.move_left(&MoveLeft, window, cx);
1567 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab".len())]);
1568 editor.move_left(&MoveLeft, window, cx);
1569 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "a".len())]);
1570
1571 editor.move_down(&MoveDown, window, cx);
1572 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "α".len())]);
1573 editor.move_right(&MoveRight, window, cx);
1574 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ".len())]);
1575 editor.move_right(&MoveRight, window, cx);
1576 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯".len())]);
1577 editor.move_right(&MoveRight, window, cx);
1578 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1579
1580 editor.move_up(&MoveUp, window, cx);
1581 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1582 editor.move_down(&MoveDown, window, cx);
1583 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβ⋯ε".len())]);
1584 editor.move_up(&MoveUp, window, cx);
1585 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "ab⋯e".len())]);
1586
1587 editor.move_up(&MoveUp, window, cx);
1588 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥🟧".len())]);
1589 editor.move_left(&MoveLeft, window, cx);
1590 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "🟥".len())]);
1591 editor.move_left(&MoveLeft, window, cx);
1592 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1593 });
1594}
1595
1596#[gpui::test]
1597fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1598 init_test(cx, |_| {});
1599
1600 let editor = cx.add_window(|window, cx| {
1601 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1602 build_editor(buffer, window, cx)
1603 });
1604 _ = editor.update(cx, |editor, window, cx| {
1605 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1606 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1607 });
1608
1609 // moving above start of document should move selection to start of document,
1610 // but the next move down should still be at the original goal_x
1611 editor.move_up(&MoveUp, window, cx);
1612 assert_eq!(display_ranges(editor, cx), &[empty_range(0, "".len())]);
1613
1614 editor.move_down(&MoveDown, window, cx);
1615 assert_eq!(display_ranges(editor, cx), &[empty_range(1, "abcd".len())]);
1616
1617 editor.move_down(&MoveDown, window, cx);
1618 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1619
1620 editor.move_down(&MoveDown, window, cx);
1621 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1622
1623 editor.move_down(&MoveDown, window, cx);
1624 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1625
1626 // moving past end of document should not change goal_x
1627 editor.move_down(&MoveDown, window, cx);
1628 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1629
1630 editor.move_down(&MoveDown, window, cx);
1631 assert_eq!(display_ranges(editor, cx), &[empty_range(5, "".len())]);
1632
1633 editor.move_up(&MoveUp, window, cx);
1634 assert_eq!(display_ranges(editor, cx), &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]);
1635
1636 editor.move_up(&MoveUp, window, cx);
1637 assert_eq!(display_ranges(editor, cx), &[empty_range(3, "abcd".len())]);
1638
1639 editor.move_up(&MoveUp, window, cx);
1640 assert_eq!(display_ranges(editor, cx), &[empty_range(2, "αβγ".len())]);
1641 });
1642}
1643
1644#[gpui::test]
1645fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1646 init_test(cx, |_| {});
1647 let move_to_beg = MoveToBeginningOfLine {
1648 stop_at_soft_wraps: true,
1649 stop_at_indent: true,
1650 };
1651
1652 let delete_to_beg = DeleteToBeginningOfLine {
1653 stop_at_indent: false,
1654 };
1655
1656 let move_to_end = MoveToEndOfLine {
1657 stop_at_soft_wraps: true,
1658 };
1659
1660 let editor = cx.add_window(|window, cx| {
1661 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1662 build_editor(buffer, window, cx)
1663 });
1664 _ = editor.update(cx, |editor, window, cx| {
1665 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1666 s.select_display_ranges([
1667 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1668 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1669 ]);
1670 });
1671 });
1672
1673 _ = editor.update(cx, |editor, window, cx| {
1674 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1675 assert_eq!(
1676 display_ranges(editor, cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1679 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1686 assert_eq!(
1687 display_ranges(editor, cx),
1688 &[
1689 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1690 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1691 ]
1692 );
1693 });
1694
1695 _ = editor.update(cx, |editor, window, cx| {
1696 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1697 assert_eq!(
1698 display_ranges(editor, cx),
1699 &[
1700 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1701 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1702 ]
1703 );
1704 });
1705
1706 _ = editor.update(cx, |editor, window, cx| {
1707 editor.move_to_end_of_line(&move_to_end, window, cx);
1708 assert_eq!(
1709 display_ranges(editor, cx),
1710 &[
1711 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1712 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1713 ]
1714 );
1715 });
1716
1717 // Moving to the end of line again is a no-op.
1718 _ = editor.update(cx, |editor, window, cx| {
1719 editor.move_to_end_of_line(&move_to_end, window, cx);
1720 assert_eq!(
1721 display_ranges(editor, cx),
1722 &[
1723 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1724 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1725 ]
1726 );
1727 });
1728
1729 _ = editor.update(cx, |editor, window, cx| {
1730 editor.move_left(&MoveLeft, window, cx);
1731 editor.select_to_beginning_of_line(
1732 &SelectToBeginningOfLine {
1733 stop_at_soft_wraps: true,
1734 stop_at_indent: true,
1735 },
1736 window,
1737 cx,
1738 );
1739 assert_eq!(
1740 display_ranges(editor, cx),
1741 &[
1742 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1743 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1744 ]
1745 );
1746 });
1747
1748 _ = editor.update(cx, |editor, window, cx| {
1749 editor.select_to_beginning_of_line(
1750 &SelectToBeginningOfLine {
1751 stop_at_soft_wraps: true,
1752 stop_at_indent: true,
1753 },
1754 window,
1755 cx,
1756 );
1757 assert_eq!(
1758 display_ranges(editor, cx),
1759 &[
1760 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1761 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1762 ]
1763 );
1764 });
1765
1766 _ = editor.update(cx, |editor, window, cx| {
1767 editor.select_to_beginning_of_line(
1768 &SelectToBeginningOfLine {
1769 stop_at_soft_wraps: true,
1770 stop_at_indent: true,
1771 },
1772 window,
1773 cx,
1774 );
1775 assert_eq!(
1776 display_ranges(editor, cx),
1777 &[
1778 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1779 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1780 ]
1781 );
1782 });
1783
1784 _ = editor.update(cx, |editor, window, cx| {
1785 editor.select_to_end_of_line(
1786 &SelectToEndOfLine {
1787 stop_at_soft_wraps: true,
1788 },
1789 window,
1790 cx,
1791 );
1792 assert_eq!(
1793 display_ranges(editor, cx),
1794 &[
1795 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1796 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1797 ]
1798 );
1799 });
1800
1801 _ = editor.update(cx, |editor, window, cx| {
1802 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1803 assert_eq!(editor.display_text(cx), "ab\n de");
1804 assert_eq!(
1805 display_ranges(editor, cx),
1806 &[
1807 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1808 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1809 ]
1810 );
1811 });
1812
1813 _ = editor.update(cx, |editor, window, cx| {
1814 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1815 assert_eq!(editor.display_text(cx), "\n");
1816 assert_eq!(
1817 display_ranges(editor, cx),
1818 &[
1819 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1820 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1821 ]
1822 );
1823 });
1824}
1825
1826#[gpui::test]
1827fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1828 init_test(cx, |_| {});
1829 let move_to_beg = MoveToBeginningOfLine {
1830 stop_at_soft_wraps: false,
1831 stop_at_indent: false,
1832 };
1833
1834 let move_to_end = MoveToEndOfLine {
1835 stop_at_soft_wraps: false,
1836 };
1837
1838 let editor = cx.add_window(|window, cx| {
1839 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1840 build_editor(buffer, window, cx)
1841 });
1842
1843 _ = editor.update(cx, |editor, window, cx| {
1844 editor.set_wrap_width(Some(140.0.into()), cx);
1845
1846 // We expect the following lines after wrapping
1847 // ```
1848 // thequickbrownfox
1849 // jumpedoverthelazydo
1850 // gs
1851 // ```
1852 // The final `gs` was soft-wrapped onto a new line.
1853 assert_eq!(
1854 "thequickbrownfox\njumpedoverthelaz\nydogs",
1855 editor.display_text(cx),
1856 );
1857
1858 // First, let's assert behavior on the first line, that was not soft-wrapped.
1859 // Start the cursor at the `k` on the first line
1860 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1861 s.select_display_ranges([
1862 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1863 ]);
1864 });
1865
1866 // Moving to the beginning of the line should put us at the beginning of the line.
1867 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1868 assert_eq!(
1869 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1870 display_ranges(editor, cx)
1871 );
1872
1873 // Moving to the end of the line should put us at the end of the line.
1874 editor.move_to_end_of_line(&move_to_end, window, cx);
1875 assert_eq!(
1876 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1877 display_ranges(editor, cx)
1878 );
1879
1880 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1881 // Start the cursor at the last line (`y` that was wrapped to a new line)
1882 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1883 s.select_display_ranges([
1884 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1885 ]);
1886 });
1887
1888 // Moving to the beginning of the line should put us at the start of the second line of
1889 // display text, i.e., the `j`.
1890 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1891 assert_eq!(
1892 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1893 display_ranges(editor, cx)
1894 );
1895
1896 // Moving to the beginning of the line again should be a no-op.
1897 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1898 assert_eq!(
1899 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1900 display_ranges(editor, cx)
1901 );
1902
1903 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1904 // next display line.
1905 editor.move_to_end_of_line(&move_to_end, window, cx);
1906 assert_eq!(
1907 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1908 display_ranges(editor, cx)
1909 );
1910
1911 // Moving to the end of the line again should be a no-op.
1912 editor.move_to_end_of_line(&move_to_end, window, cx);
1913 assert_eq!(
1914 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1915 display_ranges(editor, cx)
1916 );
1917 });
1918}
1919
1920#[gpui::test]
1921fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1922 init_test(cx, |_| {});
1923
1924 let move_to_beg = MoveToBeginningOfLine {
1925 stop_at_soft_wraps: true,
1926 stop_at_indent: true,
1927 };
1928
1929 let select_to_beg = SelectToBeginningOfLine {
1930 stop_at_soft_wraps: true,
1931 stop_at_indent: true,
1932 };
1933
1934 let delete_to_beg = DeleteToBeginningOfLine {
1935 stop_at_indent: true,
1936 };
1937
1938 let move_to_end = MoveToEndOfLine {
1939 stop_at_soft_wraps: false,
1940 };
1941
1942 let editor = cx.add_window(|window, cx| {
1943 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1944 build_editor(buffer, window, cx)
1945 });
1946
1947 _ = editor.update(cx, |editor, window, cx| {
1948 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
1949 s.select_display_ranges([
1950 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1951 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1952 ]);
1953 });
1954
1955 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1956 // and the second cursor at the first non-whitespace character in the line.
1957 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1958 assert_eq!(
1959 display_ranges(editor, cx),
1960 &[
1961 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1962 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1963 ]
1964 );
1965
1966 // Moving to the beginning of the line again should be a no-op for the first cursor,
1967 // and should move the second cursor to the beginning of the line.
1968 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1969 assert_eq!(
1970 display_ranges(editor, cx),
1971 &[
1972 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1973 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1974 ]
1975 );
1976
1977 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1978 // and should move the second cursor back to the first non-whitespace character in the line.
1979 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1980 assert_eq!(
1981 display_ranges(editor, cx),
1982 &[
1983 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1984 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1985 ]
1986 );
1987
1988 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1989 // and to the first non-whitespace character in the line for the second cursor.
1990 editor.move_to_end_of_line(&move_to_end, window, cx);
1991 editor.move_left(&MoveLeft, window, cx);
1992 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1993 assert_eq!(
1994 display_ranges(editor, cx),
1995 &[
1996 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1997 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1998 ]
1999 );
2000
2001 // Selecting to the beginning of the line again should be a no-op for the first cursor,
2002 // and should select to the beginning of the line for the second cursor.
2003 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
2004 assert_eq!(
2005 display_ranges(editor, cx),
2006 &[
2007 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
2008 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
2009 ]
2010 );
2011
2012 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
2013 // and should delete to the first non-whitespace character in the line for the second cursor.
2014 editor.move_to_end_of_line(&move_to_end, window, cx);
2015 editor.move_left(&MoveLeft, window, cx);
2016 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
2017 assert_eq!(editor.text(cx), "c\n f");
2018 });
2019}
2020
2021#[gpui::test]
2022fn test_beginning_of_line_with_cursor_between_line_start_and_indent(cx: &mut TestAppContext) {
2023 init_test(cx, |_| {});
2024
2025 let move_to_beg = MoveToBeginningOfLine {
2026 stop_at_soft_wraps: true,
2027 stop_at_indent: true,
2028 };
2029
2030 let editor = cx.add_window(|window, cx| {
2031 let buffer = MultiBuffer::build_simple(" hello\nworld", cx);
2032 build_editor(buffer, window, cx)
2033 });
2034
2035 _ = editor.update(cx, |editor, window, cx| {
2036 // test cursor between line_start and indent_start
2037 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2038 s.select_display_ranges([
2039 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3)
2040 ]);
2041 });
2042
2043 // cursor should move to line_start
2044 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2045 assert_eq!(
2046 display_ranges(editor, cx),
2047 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2048 );
2049
2050 // cursor should move to indent_start
2051 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2052 assert_eq!(
2053 display_ranges(editor, cx),
2054 &[DisplayPoint::new(DisplayRow(0), 4)..DisplayPoint::new(DisplayRow(0), 4)]
2055 );
2056
2057 // cursor should move to back to line_start
2058 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
2059 assert_eq!(
2060 display_ranges(editor, cx),
2061 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
2062 );
2063 });
2064}
2065
2066#[gpui::test]
2067fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
2068 init_test(cx, |_| {});
2069
2070 let editor = cx.add_window(|window, cx| {
2071 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
2072 build_editor(buffer, window, cx)
2073 });
2074 _ = editor.update(cx, |editor, window, cx| {
2075 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2076 s.select_display_ranges([
2077 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
2078 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
2079 ])
2080 });
2081 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2082 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
2083
2084 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2085 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
2086
2087 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2088 assert_selection_ranges("use ˇstd::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2089
2090 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2091 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2092
2093 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2094 assert_selection_ranges("ˇuse std::str::{foo, ˇbar}\n\n {baz.qux()}", editor, cx);
2095
2096 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2097 assert_selection_ranges("useˇ std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
2098
2099 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2100 assert_selection_ranges("use stdˇ::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
2101
2102 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2103 assert_selection_ranges("use std::ˇstr::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
2104
2105 editor.move_right(&MoveRight, window, cx);
2106 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2107 assert_selection_ranges(
2108 "use std::«ˇs»tr::{foo, bar}\n«ˇ\n» {baz.qux()}",
2109 editor,
2110 cx,
2111 );
2112
2113 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
2114 assert_selection_ranges(
2115 "use std«ˇ::s»tr::{foo, bar«ˇ}\n\n» {baz.qux()}",
2116 editor,
2117 cx,
2118 );
2119
2120 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
2121 assert_selection_ranges(
2122 "use std::«ˇs»tr::{foo, bar}«ˇ\n\n» {baz.qux()}",
2123 editor,
2124 cx,
2125 );
2126 });
2127}
2128
2129#[gpui::test]
2130fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
2131 init_test(cx, |_| {});
2132
2133 let editor = cx.add_window(|window, cx| {
2134 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
2135 build_editor(buffer, window, cx)
2136 });
2137
2138 _ = editor.update(cx, |editor, window, cx| {
2139 editor.set_wrap_width(Some(140.0.into()), cx);
2140 assert_eq!(
2141 editor.display_text(cx),
2142 "use one::{\n two::three::\n four::five\n};"
2143 );
2144
2145 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2146 s.select_display_ranges([
2147 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
2148 ]);
2149 });
2150
2151 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2152 assert_eq!(
2153 display_ranges(editor, cx),
2154 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
2155 );
2156
2157 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2158 assert_eq!(
2159 display_ranges(editor, cx),
2160 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2161 );
2162
2163 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2164 assert_eq!(
2165 display_ranges(editor, cx),
2166 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2167 );
2168
2169 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
2170 assert_eq!(
2171 display_ranges(editor, cx),
2172 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2173 );
2174
2175 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2176 assert_eq!(
2177 display_ranges(editor, cx),
2178 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2179 );
2180
2181 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2182 assert_eq!(
2183 display_ranges(editor, cx),
2184 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2185 );
2186 });
2187}
2188
2189#[gpui::test]
2190async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2191 init_test(cx, |_| {});
2192 let mut cx = EditorTestContext::new(cx).await;
2193
2194 let line_height = cx.editor(|editor, window, _| {
2195 editor
2196 .style()
2197 .unwrap()
2198 .text
2199 .line_height_in_pixels(window.rem_size())
2200 });
2201 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2202
2203 cx.set_state(
2204 &r#"ˇone
2205 two
2206
2207 three
2208 fourˇ
2209 five
2210
2211 six"#
2212 .unindent(),
2213 );
2214
2215 cx.update_editor(|editor, window, cx| {
2216 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2217 });
2218 cx.assert_editor_state(
2219 &r#"one
2220 two
2221 ˇ
2222 three
2223 four
2224 five
2225 ˇ
2226 six"#
2227 .unindent(),
2228 );
2229
2230 cx.update_editor(|editor, window, cx| {
2231 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2232 });
2233 cx.assert_editor_state(
2234 &r#"one
2235 two
2236
2237 three
2238 four
2239 five
2240 ˇ
2241 sixˇ"#
2242 .unindent(),
2243 );
2244
2245 cx.update_editor(|editor, window, cx| {
2246 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2247 });
2248 cx.assert_editor_state(
2249 &r#"one
2250 two
2251
2252 three
2253 four
2254 five
2255
2256 sixˇ"#
2257 .unindent(),
2258 );
2259
2260 cx.update_editor(|editor, window, cx| {
2261 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2262 });
2263 cx.assert_editor_state(
2264 &r#"one
2265 two
2266
2267 three
2268 four
2269 five
2270 ˇ
2271 six"#
2272 .unindent(),
2273 );
2274
2275 cx.update_editor(|editor, window, cx| {
2276 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2277 });
2278 cx.assert_editor_state(
2279 &r#"one
2280 two
2281 ˇ
2282 three
2283 four
2284 five
2285
2286 six"#
2287 .unindent(),
2288 );
2289
2290 cx.update_editor(|editor, window, cx| {
2291 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2292 });
2293 cx.assert_editor_state(
2294 &r#"ˇone
2295 two
2296
2297 three
2298 four
2299 five
2300
2301 six"#
2302 .unindent(),
2303 );
2304}
2305
2306#[gpui::test]
2307async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2308 init_test(cx, |_| {});
2309 let mut cx = EditorTestContext::new(cx).await;
2310 let line_height = cx.editor(|editor, window, _| {
2311 editor
2312 .style()
2313 .unwrap()
2314 .text
2315 .line_height_in_pixels(window.rem_size())
2316 });
2317 let window = cx.window;
2318 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2319
2320 cx.set_state(
2321 r#"ˇone
2322 two
2323 three
2324 four
2325 five
2326 six
2327 seven
2328 eight
2329 nine
2330 ten
2331 "#,
2332 );
2333
2334 cx.update_editor(|editor, window, cx| {
2335 assert_eq!(
2336 editor.snapshot(window, cx).scroll_position(),
2337 gpui::Point::new(0., 0.)
2338 );
2339 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2340 assert_eq!(
2341 editor.snapshot(window, cx).scroll_position(),
2342 gpui::Point::new(0., 3.)
2343 );
2344 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2345 assert_eq!(
2346 editor.snapshot(window, cx).scroll_position(),
2347 gpui::Point::new(0., 6.)
2348 );
2349 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2350 assert_eq!(
2351 editor.snapshot(window, cx).scroll_position(),
2352 gpui::Point::new(0., 3.)
2353 );
2354
2355 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2356 assert_eq!(
2357 editor.snapshot(window, cx).scroll_position(),
2358 gpui::Point::new(0., 1.)
2359 );
2360 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2361 assert_eq!(
2362 editor.snapshot(window, cx).scroll_position(),
2363 gpui::Point::new(0., 3.)
2364 );
2365 });
2366}
2367
2368#[gpui::test]
2369async fn test_autoscroll(cx: &mut TestAppContext) {
2370 init_test(cx, |_| {});
2371 let mut cx = EditorTestContext::new(cx).await;
2372
2373 let line_height = cx.update_editor(|editor, window, cx| {
2374 editor.set_vertical_scroll_margin(2, cx);
2375 editor
2376 .style()
2377 .unwrap()
2378 .text
2379 .line_height_in_pixels(window.rem_size())
2380 });
2381 let window = cx.window;
2382 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2383
2384 cx.set_state(
2385 r#"ˇone
2386 two
2387 three
2388 four
2389 five
2390 six
2391 seven
2392 eight
2393 nine
2394 ten
2395 "#,
2396 );
2397 cx.update_editor(|editor, window, cx| {
2398 assert_eq!(
2399 editor.snapshot(window, cx).scroll_position(),
2400 gpui::Point::new(0., 0.0)
2401 );
2402 });
2403
2404 // Add a cursor below the visible area. Since both cursors cannot fit
2405 // on screen, the editor autoscrolls to reveal the newest cursor, and
2406 // allows the vertical scroll margin below that cursor.
2407 cx.update_editor(|editor, window, cx| {
2408 editor.change_selections(Default::default(), window, cx, |selections| {
2409 selections.select_ranges([
2410 Point::new(0, 0)..Point::new(0, 0),
2411 Point::new(6, 0)..Point::new(6, 0),
2412 ]);
2413 })
2414 });
2415 cx.update_editor(|editor, window, cx| {
2416 assert_eq!(
2417 editor.snapshot(window, cx).scroll_position(),
2418 gpui::Point::new(0., 3.0)
2419 );
2420 });
2421
2422 // Move down. The editor cursor scrolls down to track the newest cursor.
2423 cx.update_editor(|editor, window, cx| {
2424 editor.move_down(&Default::default(), window, cx);
2425 });
2426 cx.update_editor(|editor, window, cx| {
2427 assert_eq!(
2428 editor.snapshot(window, cx).scroll_position(),
2429 gpui::Point::new(0., 4.0)
2430 );
2431 });
2432
2433 // Add a cursor above the visible area. Since both cursors fit on screen,
2434 // the editor scrolls to show both.
2435 cx.update_editor(|editor, window, cx| {
2436 editor.change_selections(Default::default(), window, cx, |selections| {
2437 selections.select_ranges([
2438 Point::new(1, 0)..Point::new(1, 0),
2439 Point::new(6, 0)..Point::new(6, 0),
2440 ]);
2441 })
2442 });
2443 cx.update_editor(|editor, window, cx| {
2444 assert_eq!(
2445 editor.snapshot(window, cx).scroll_position(),
2446 gpui::Point::new(0., 1.0)
2447 );
2448 });
2449}
2450
2451#[gpui::test]
2452async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2453 init_test(cx, |_| {});
2454 let mut cx = EditorTestContext::new(cx).await;
2455
2456 let line_height = cx.editor(|editor, window, _cx| {
2457 editor
2458 .style()
2459 .unwrap()
2460 .text
2461 .line_height_in_pixels(window.rem_size())
2462 });
2463 let window = cx.window;
2464 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2465 cx.set_state(
2466 &r#"
2467 ˇone
2468 two
2469 threeˇ
2470 four
2471 five
2472 six
2473 seven
2474 eight
2475 nine
2476 ten
2477 "#
2478 .unindent(),
2479 );
2480
2481 cx.update_editor(|editor, window, cx| {
2482 editor.move_page_down(&MovePageDown::default(), window, cx)
2483 });
2484 cx.assert_editor_state(
2485 &r#"
2486 one
2487 two
2488 three
2489 ˇfour
2490 five
2491 sixˇ
2492 seven
2493 eight
2494 nine
2495 ten
2496 "#
2497 .unindent(),
2498 );
2499
2500 cx.update_editor(|editor, window, cx| {
2501 editor.move_page_down(&MovePageDown::default(), window, cx)
2502 });
2503 cx.assert_editor_state(
2504 &r#"
2505 one
2506 two
2507 three
2508 four
2509 five
2510 six
2511 ˇseven
2512 eight
2513 nineˇ
2514 ten
2515 "#
2516 .unindent(),
2517 );
2518
2519 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2520 cx.assert_editor_state(
2521 &r#"
2522 one
2523 two
2524 three
2525 ˇfour
2526 five
2527 sixˇ
2528 seven
2529 eight
2530 nine
2531 ten
2532 "#
2533 .unindent(),
2534 );
2535
2536 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2537 cx.assert_editor_state(
2538 &r#"
2539 ˇone
2540 two
2541 threeˇ
2542 four
2543 five
2544 six
2545 seven
2546 eight
2547 nine
2548 ten
2549 "#
2550 .unindent(),
2551 );
2552
2553 // Test select collapsing
2554 cx.update_editor(|editor, window, cx| {
2555 editor.move_page_down(&MovePageDown::default(), window, cx);
2556 editor.move_page_down(&MovePageDown::default(), window, cx);
2557 editor.move_page_down(&MovePageDown::default(), window, cx);
2558 });
2559 cx.assert_editor_state(
2560 &r#"
2561 one
2562 two
2563 three
2564 four
2565 five
2566 six
2567 seven
2568 eight
2569 nine
2570 ˇten
2571 ˇ"#
2572 .unindent(),
2573 );
2574}
2575
2576#[gpui::test]
2577async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2578 init_test(cx, |_| {});
2579 let mut cx = EditorTestContext::new(cx).await;
2580 cx.set_state("one «two threeˇ» four");
2581 cx.update_editor(|editor, window, cx| {
2582 editor.delete_to_beginning_of_line(
2583 &DeleteToBeginningOfLine {
2584 stop_at_indent: false,
2585 },
2586 window,
2587 cx,
2588 );
2589 assert_eq!(editor.text(cx), " four");
2590 });
2591}
2592
2593#[gpui::test]
2594async fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2595 init_test(cx, |_| {});
2596
2597 let mut cx = EditorTestContext::new(cx).await;
2598
2599 // For an empty selection, the preceding word fragment is deleted.
2600 // For non-empty selections, only selected characters are deleted.
2601 cx.set_state("onˇe two t«hreˇ»e four");
2602 cx.update_editor(|editor, window, cx| {
2603 editor.delete_to_previous_word_start(
2604 &DeleteToPreviousWordStart {
2605 ignore_newlines: false,
2606 ignore_brackets: false,
2607 },
2608 window,
2609 cx,
2610 );
2611 });
2612 cx.assert_editor_state("ˇe two tˇe four");
2613
2614 cx.set_state("e tˇwo te «fˇ»our");
2615 cx.update_editor(|editor, window, cx| {
2616 editor.delete_to_next_word_end(
2617 &DeleteToNextWordEnd {
2618 ignore_newlines: false,
2619 ignore_brackets: false,
2620 },
2621 window,
2622 cx,
2623 );
2624 });
2625 cx.assert_editor_state("e tˇ te ˇour");
2626}
2627
2628#[gpui::test]
2629async fn test_delete_whitespaces(cx: &mut TestAppContext) {
2630 init_test(cx, |_| {});
2631
2632 let mut cx = EditorTestContext::new(cx).await;
2633
2634 cx.set_state("here is some text ˇwith a space");
2635 cx.update_editor(|editor, window, cx| {
2636 editor.delete_to_previous_word_start(
2637 &DeleteToPreviousWordStart {
2638 ignore_newlines: false,
2639 ignore_brackets: true,
2640 },
2641 window,
2642 cx,
2643 );
2644 });
2645 // Continuous whitespace sequences are removed entirely, words behind them are not affected by the deletion action.
2646 cx.assert_editor_state("here is some textˇwith a space");
2647
2648 cx.set_state("here is some text ˇwith a space");
2649 cx.update_editor(|editor, window, cx| {
2650 editor.delete_to_previous_word_start(
2651 &DeleteToPreviousWordStart {
2652 ignore_newlines: false,
2653 ignore_brackets: false,
2654 },
2655 window,
2656 cx,
2657 );
2658 });
2659 cx.assert_editor_state("here is some textˇwith a space");
2660
2661 cx.set_state("here is some textˇ with a space");
2662 cx.update_editor(|editor, window, cx| {
2663 editor.delete_to_next_word_end(
2664 &DeleteToNextWordEnd {
2665 ignore_newlines: false,
2666 ignore_brackets: true,
2667 },
2668 window,
2669 cx,
2670 );
2671 });
2672 // Same happens in the other direction.
2673 cx.assert_editor_state("here is some textˇwith a space");
2674
2675 cx.set_state("here is some textˇ with a space");
2676 cx.update_editor(|editor, window, cx| {
2677 editor.delete_to_next_word_end(
2678 &DeleteToNextWordEnd {
2679 ignore_newlines: false,
2680 ignore_brackets: false,
2681 },
2682 window,
2683 cx,
2684 );
2685 });
2686 cx.assert_editor_state("here is some textˇwith a space");
2687
2688 cx.set_state("here is some textˇ with a space");
2689 cx.update_editor(|editor, window, cx| {
2690 editor.delete_to_next_word_end(
2691 &DeleteToNextWordEnd {
2692 ignore_newlines: true,
2693 ignore_brackets: false,
2694 },
2695 window,
2696 cx,
2697 );
2698 });
2699 cx.assert_editor_state("here is some textˇwith a space");
2700 cx.update_editor(|editor, window, cx| {
2701 editor.delete_to_previous_word_start(
2702 &DeleteToPreviousWordStart {
2703 ignore_newlines: true,
2704 ignore_brackets: false,
2705 },
2706 window,
2707 cx,
2708 );
2709 });
2710 cx.assert_editor_state("here is some ˇwith a space");
2711 cx.update_editor(|editor, window, cx| {
2712 editor.delete_to_previous_word_start(
2713 &DeleteToPreviousWordStart {
2714 ignore_newlines: true,
2715 ignore_brackets: false,
2716 },
2717 window,
2718 cx,
2719 );
2720 });
2721 // Single whitespaces are removed with the word behind them.
2722 cx.assert_editor_state("here is ˇwith a space");
2723 cx.update_editor(|editor, window, cx| {
2724 editor.delete_to_previous_word_start(
2725 &DeleteToPreviousWordStart {
2726 ignore_newlines: true,
2727 ignore_brackets: false,
2728 },
2729 window,
2730 cx,
2731 );
2732 });
2733 cx.assert_editor_state("here ˇwith a space");
2734 cx.update_editor(|editor, window, cx| {
2735 editor.delete_to_previous_word_start(
2736 &DeleteToPreviousWordStart {
2737 ignore_newlines: true,
2738 ignore_brackets: false,
2739 },
2740 window,
2741 cx,
2742 );
2743 });
2744 cx.assert_editor_state("ˇwith a space");
2745 cx.update_editor(|editor, window, cx| {
2746 editor.delete_to_previous_word_start(
2747 &DeleteToPreviousWordStart {
2748 ignore_newlines: true,
2749 ignore_brackets: false,
2750 },
2751 window,
2752 cx,
2753 );
2754 });
2755 cx.assert_editor_state("ˇwith a space");
2756 cx.update_editor(|editor, window, cx| {
2757 editor.delete_to_next_word_end(
2758 &DeleteToNextWordEnd {
2759 ignore_newlines: true,
2760 ignore_brackets: false,
2761 },
2762 window,
2763 cx,
2764 );
2765 });
2766 // Same happens in the other direction.
2767 cx.assert_editor_state("ˇ a space");
2768 cx.update_editor(|editor, window, cx| {
2769 editor.delete_to_next_word_end(
2770 &DeleteToNextWordEnd {
2771 ignore_newlines: true,
2772 ignore_brackets: false,
2773 },
2774 window,
2775 cx,
2776 );
2777 });
2778 cx.assert_editor_state("ˇ space");
2779 cx.update_editor(|editor, window, cx| {
2780 editor.delete_to_next_word_end(
2781 &DeleteToNextWordEnd {
2782 ignore_newlines: true,
2783 ignore_brackets: false,
2784 },
2785 window,
2786 cx,
2787 );
2788 });
2789 cx.assert_editor_state("ˇ");
2790 cx.update_editor(|editor, window, cx| {
2791 editor.delete_to_next_word_end(
2792 &DeleteToNextWordEnd {
2793 ignore_newlines: true,
2794 ignore_brackets: false,
2795 },
2796 window,
2797 cx,
2798 );
2799 });
2800 cx.assert_editor_state("ˇ");
2801 cx.update_editor(|editor, window, cx| {
2802 editor.delete_to_previous_word_start(
2803 &DeleteToPreviousWordStart {
2804 ignore_newlines: true,
2805 ignore_brackets: false,
2806 },
2807 window,
2808 cx,
2809 );
2810 });
2811 cx.assert_editor_state("ˇ");
2812}
2813
2814#[gpui::test]
2815async fn test_delete_to_bracket(cx: &mut TestAppContext) {
2816 init_test(cx, |_| {});
2817
2818 let language = Arc::new(
2819 Language::new(
2820 LanguageConfig {
2821 brackets: BracketPairConfig {
2822 pairs: vec![
2823 BracketPair {
2824 start: "\"".to_string(),
2825 end: "\"".to_string(),
2826 close: true,
2827 surround: true,
2828 newline: false,
2829 },
2830 BracketPair {
2831 start: "(".to_string(),
2832 end: ")".to_string(),
2833 close: true,
2834 surround: true,
2835 newline: true,
2836 },
2837 ],
2838 ..BracketPairConfig::default()
2839 },
2840 ..LanguageConfig::default()
2841 },
2842 Some(tree_sitter_rust::LANGUAGE.into()),
2843 )
2844 .with_brackets_query(
2845 r#"
2846 ("(" @open ")" @close)
2847 ("\"" @open "\"" @close)
2848 "#,
2849 )
2850 .unwrap(),
2851 );
2852
2853 let mut cx = EditorTestContext::new(cx).await;
2854 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2855
2856 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2857 cx.update_editor(|editor, window, cx| {
2858 editor.delete_to_previous_word_start(
2859 &DeleteToPreviousWordStart {
2860 ignore_newlines: true,
2861 ignore_brackets: false,
2862 },
2863 window,
2864 cx,
2865 );
2866 });
2867 // Deletion stops before brackets if asked to not ignore them.
2868 cx.assert_editor_state(r#"macro!("ˇCOMMENT");"#);
2869 cx.update_editor(|editor, window, cx| {
2870 editor.delete_to_previous_word_start(
2871 &DeleteToPreviousWordStart {
2872 ignore_newlines: true,
2873 ignore_brackets: false,
2874 },
2875 window,
2876 cx,
2877 );
2878 });
2879 // Deletion has to remove a single bracket and then stop again.
2880 cx.assert_editor_state(r#"macro!(ˇCOMMENT");"#);
2881
2882 cx.update_editor(|editor, window, cx| {
2883 editor.delete_to_previous_word_start(
2884 &DeleteToPreviousWordStart {
2885 ignore_newlines: true,
2886 ignore_brackets: false,
2887 },
2888 window,
2889 cx,
2890 );
2891 });
2892 cx.assert_editor_state(r#"macro!ˇCOMMENT");"#);
2893
2894 cx.update_editor(|editor, window, cx| {
2895 editor.delete_to_previous_word_start(
2896 &DeleteToPreviousWordStart {
2897 ignore_newlines: true,
2898 ignore_brackets: false,
2899 },
2900 window,
2901 cx,
2902 );
2903 });
2904 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2905
2906 cx.update_editor(|editor, window, cx| {
2907 editor.delete_to_previous_word_start(
2908 &DeleteToPreviousWordStart {
2909 ignore_newlines: true,
2910 ignore_brackets: false,
2911 },
2912 window,
2913 cx,
2914 );
2915 });
2916 cx.assert_editor_state(r#"ˇCOMMENT");"#);
2917
2918 cx.update_editor(|editor, window, cx| {
2919 editor.delete_to_next_word_end(
2920 &DeleteToNextWordEnd {
2921 ignore_newlines: true,
2922 ignore_brackets: false,
2923 },
2924 window,
2925 cx,
2926 );
2927 });
2928 // Brackets on the right are not paired anymore, hence deletion does not stop at them
2929 cx.assert_editor_state(r#"ˇ");"#);
2930
2931 cx.update_editor(|editor, window, cx| {
2932 editor.delete_to_next_word_end(
2933 &DeleteToNextWordEnd {
2934 ignore_newlines: true,
2935 ignore_brackets: false,
2936 },
2937 window,
2938 cx,
2939 );
2940 });
2941 cx.assert_editor_state(r#"ˇ"#);
2942
2943 cx.update_editor(|editor, window, cx| {
2944 editor.delete_to_next_word_end(
2945 &DeleteToNextWordEnd {
2946 ignore_newlines: true,
2947 ignore_brackets: false,
2948 },
2949 window,
2950 cx,
2951 );
2952 });
2953 cx.assert_editor_state(r#"ˇ"#);
2954
2955 cx.set_state(r#"macro!("// ˇCOMMENT");"#);
2956 cx.update_editor(|editor, window, cx| {
2957 editor.delete_to_previous_word_start(
2958 &DeleteToPreviousWordStart {
2959 ignore_newlines: true,
2960 ignore_brackets: true,
2961 },
2962 window,
2963 cx,
2964 );
2965 });
2966 cx.assert_editor_state(r#"macroˇCOMMENT");"#);
2967}
2968
2969#[gpui::test]
2970fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2971 init_test(cx, |_| {});
2972
2973 let editor = cx.add_window(|window, cx| {
2974 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2975 build_editor(buffer, window, cx)
2976 });
2977 let del_to_prev_word_start = DeleteToPreviousWordStart {
2978 ignore_newlines: false,
2979 ignore_brackets: false,
2980 };
2981 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2982 ignore_newlines: true,
2983 ignore_brackets: false,
2984 };
2985
2986 _ = editor.update(cx, |editor, window, cx| {
2987 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
2988 s.select_display_ranges([
2989 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2990 ])
2991 });
2992 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2993 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2994 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2995 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2996 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2997 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2998 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2999 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
3000 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3001 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
3002 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
3003 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3004 });
3005}
3006
3007#[gpui::test]
3008fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
3009 init_test(cx, |_| {});
3010
3011 let editor = cx.add_window(|window, cx| {
3012 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
3013 build_editor(buffer, window, cx)
3014 });
3015 let del_to_next_word_end = DeleteToNextWordEnd {
3016 ignore_newlines: false,
3017 ignore_brackets: false,
3018 };
3019 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
3020 ignore_newlines: true,
3021 ignore_brackets: false,
3022 };
3023
3024 _ = editor.update(cx, |editor, window, cx| {
3025 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3026 s.select_display_ranges([
3027 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
3028 ])
3029 });
3030 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3031 assert_eq!(
3032 editor.buffer.read(cx).read(cx).text(),
3033 "one\n two\nthree\n four"
3034 );
3035 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3036 assert_eq!(
3037 editor.buffer.read(cx).read(cx).text(),
3038 "\n two\nthree\n four"
3039 );
3040 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3041 assert_eq!(
3042 editor.buffer.read(cx).read(cx).text(),
3043 "two\nthree\n four"
3044 );
3045 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
3046 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
3047 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3048 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
3049 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3050 assert_eq!(editor.buffer.read(cx).read(cx).text(), "four");
3051 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
3052 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
3053 });
3054}
3055
3056#[gpui::test]
3057fn test_newline(cx: &mut TestAppContext) {
3058 init_test(cx, |_| {});
3059
3060 let editor = cx.add_window(|window, cx| {
3061 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
3062 build_editor(buffer, window, cx)
3063 });
3064
3065 _ = editor.update(cx, |editor, window, cx| {
3066 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3067 s.select_display_ranges([
3068 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
3069 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
3070 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
3071 ])
3072 });
3073
3074 editor.newline(&Newline, window, cx);
3075 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
3076 });
3077}
3078
3079#[gpui::test]
3080async fn test_newline_yaml(cx: &mut TestAppContext) {
3081 init_test(cx, |_| {});
3082
3083 let mut cx = EditorTestContext::new(cx).await;
3084 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
3085 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
3086
3087 // Object (between 2 fields)
3088 cx.set_state(indoc! {"
3089 test:ˇ
3090 hello: bye"});
3091 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3092 cx.assert_editor_state(indoc! {"
3093 test:
3094 ˇ
3095 hello: bye"});
3096
3097 // Object (first and single line)
3098 cx.set_state(indoc! {"
3099 test:ˇ"});
3100 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3101 cx.assert_editor_state(indoc! {"
3102 test:
3103 ˇ"});
3104
3105 // Array with objects (after first element)
3106 cx.set_state(indoc! {"
3107 test:
3108 - foo: barˇ"});
3109 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3110 cx.assert_editor_state(indoc! {"
3111 test:
3112 - foo: bar
3113 ˇ"});
3114
3115 // Array with objects and comment
3116 cx.set_state(indoc! {"
3117 test:
3118 - foo: bar
3119 - bar: # testˇ"});
3120 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3121 cx.assert_editor_state(indoc! {"
3122 test:
3123 - foo: bar
3124 - bar: # test
3125 ˇ"});
3126
3127 // Array with objects (after second element)
3128 cx.set_state(indoc! {"
3129 test:
3130 - foo: bar
3131 - bar: fooˇ"});
3132 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3133 cx.assert_editor_state(indoc! {"
3134 test:
3135 - foo: bar
3136 - bar: foo
3137 ˇ"});
3138
3139 // Array with strings (after first element)
3140 cx.set_state(indoc! {"
3141 test:
3142 - fooˇ"});
3143 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3144 cx.assert_editor_state(indoc! {"
3145 test:
3146 - foo
3147 ˇ"});
3148}
3149
3150#[gpui::test]
3151fn test_newline_with_old_selections(cx: &mut TestAppContext) {
3152 init_test(cx, |_| {});
3153
3154 let editor = cx.add_window(|window, cx| {
3155 let buffer = MultiBuffer::build_simple(
3156 "
3157 a
3158 b(
3159 X
3160 )
3161 c(
3162 X
3163 )
3164 "
3165 .unindent()
3166 .as_str(),
3167 cx,
3168 );
3169 let mut editor = build_editor(buffer, window, cx);
3170 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3171 s.select_ranges([
3172 Point::new(2, 4)..Point::new(2, 5),
3173 Point::new(5, 4)..Point::new(5, 5),
3174 ])
3175 });
3176 editor
3177 });
3178
3179 _ = editor.update(cx, |editor, window, cx| {
3180 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3181 editor.buffer.update(cx, |buffer, cx| {
3182 buffer.edit(
3183 [
3184 (Point::new(1, 2)..Point::new(3, 0), ""),
3185 (Point::new(4, 2)..Point::new(6, 0), ""),
3186 ],
3187 None,
3188 cx,
3189 );
3190 assert_eq!(
3191 buffer.read(cx).text(),
3192 "
3193 a
3194 b()
3195 c()
3196 "
3197 .unindent()
3198 );
3199 });
3200 assert_eq!(
3201 editor.selections.ranges(&editor.display_snapshot(cx)),
3202 &[
3203 Point::new(1, 2)..Point::new(1, 2),
3204 Point::new(2, 2)..Point::new(2, 2),
3205 ],
3206 );
3207
3208 editor.newline(&Newline, window, cx);
3209 assert_eq!(
3210 editor.text(cx),
3211 "
3212 a
3213 b(
3214 )
3215 c(
3216 )
3217 "
3218 .unindent()
3219 );
3220
3221 // The selections are moved after the inserted newlines
3222 assert_eq!(
3223 editor.selections.ranges(&editor.display_snapshot(cx)),
3224 &[
3225 Point::new(2, 0)..Point::new(2, 0),
3226 Point::new(4, 0)..Point::new(4, 0),
3227 ],
3228 );
3229 });
3230}
3231
3232#[gpui::test]
3233async fn test_newline_above(cx: &mut TestAppContext) {
3234 init_test(cx, |settings| {
3235 settings.defaults.tab_size = NonZeroU32::new(4)
3236 });
3237
3238 let language = Arc::new(
3239 Language::new(
3240 LanguageConfig::default(),
3241 Some(tree_sitter_rust::LANGUAGE.into()),
3242 )
3243 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3244 .unwrap(),
3245 );
3246
3247 let mut cx = EditorTestContext::new(cx).await;
3248 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3249 cx.set_state(indoc! {"
3250 const a: ˇA = (
3251 (ˇ
3252 «const_functionˇ»(ˇ),
3253 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3254 )ˇ
3255 ˇ);ˇ
3256 "});
3257
3258 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
3259 cx.assert_editor_state(indoc! {"
3260 ˇ
3261 const a: A = (
3262 ˇ
3263 (
3264 ˇ
3265 ˇ
3266 const_function(),
3267 ˇ
3268 ˇ
3269 ˇ
3270 ˇ
3271 something_else,
3272 ˇ
3273 )
3274 ˇ
3275 ˇ
3276 );
3277 "});
3278}
3279
3280#[gpui::test]
3281async fn test_newline_below(cx: &mut TestAppContext) {
3282 init_test(cx, |settings| {
3283 settings.defaults.tab_size = NonZeroU32::new(4)
3284 });
3285
3286 let language = Arc::new(
3287 Language::new(
3288 LanguageConfig::default(),
3289 Some(tree_sitter_rust::LANGUAGE.into()),
3290 )
3291 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3292 .unwrap(),
3293 );
3294
3295 let mut cx = EditorTestContext::new(cx).await;
3296 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3297 cx.set_state(indoc! {"
3298 const a: ˇA = (
3299 (ˇ
3300 «const_functionˇ»(ˇ),
3301 so«mˇ»et«hˇ»ing_ˇelse,ˇ
3302 )ˇ
3303 ˇ);ˇ
3304 "});
3305
3306 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
3307 cx.assert_editor_state(indoc! {"
3308 const a: A = (
3309 ˇ
3310 (
3311 ˇ
3312 const_function(),
3313 ˇ
3314 ˇ
3315 something_else,
3316 ˇ
3317 ˇ
3318 ˇ
3319 ˇ
3320 )
3321 ˇ
3322 );
3323 ˇ
3324 ˇ
3325 "});
3326}
3327
3328#[gpui::test]
3329async fn test_newline_comments(cx: &mut TestAppContext) {
3330 init_test(cx, |settings| {
3331 settings.defaults.tab_size = NonZeroU32::new(4)
3332 });
3333
3334 let language = Arc::new(Language::new(
3335 LanguageConfig {
3336 line_comments: vec!["// ".into()],
3337 ..LanguageConfig::default()
3338 },
3339 None,
3340 ));
3341 {
3342 let mut cx = EditorTestContext::new(cx).await;
3343 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3344 cx.set_state(indoc! {"
3345 // Fooˇ
3346 "});
3347
3348 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3349 cx.assert_editor_state(indoc! {"
3350 // Foo
3351 // ˇ
3352 "});
3353 // Ensure that we add comment prefix when existing line contains space
3354 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3355 cx.assert_editor_state(
3356 indoc! {"
3357 // Foo
3358 //s
3359 // ˇ
3360 "}
3361 .replace("s", " ") // s is used as space placeholder to prevent format on save
3362 .as_str(),
3363 );
3364 // Ensure that we add comment prefix when existing line does not contain space
3365 cx.set_state(indoc! {"
3366 // Foo
3367 //ˇ
3368 "});
3369 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3370 cx.assert_editor_state(indoc! {"
3371 // Foo
3372 //
3373 // ˇ
3374 "});
3375 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
3376 cx.set_state(indoc! {"
3377 ˇ// Foo
3378 "});
3379 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3380 cx.assert_editor_state(indoc! {"
3381
3382 ˇ// Foo
3383 "});
3384 }
3385 // Ensure that comment continuations can be disabled.
3386 update_test_language_settings(cx, |settings| {
3387 settings.defaults.extend_comment_on_newline = Some(false);
3388 });
3389 let mut cx = EditorTestContext::new(cx).await;
3390 cx.set_state(indoc! {"
3391 // Fooˇ
3392 "});
3393 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3394 cx.assert_editor_state(indoc! {"
3395 // Foo
3396 ˇ
3397 "});
3398}
3399
3400#[gpui::test]
3401async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
3402 init_test(cx, |settings| {
3403 settings.defaults.tab_size = NonZeroU32::new(4)
3404 });
3405
3406 let language = Arc::new(Language::new(
3407 LanguageConfig {
3408 line_comments: vec!["// ".into(), "/// ".into()],
3409 ..LanguageConfig::default()
3410 },
3411 None,
3412 ));
3413 {
3414 let mut cx = EditorTestContext::new(cx).await;
3415 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3416 cx.set_state(indoc! {"
3417 //ˇ
3418 "});
3419 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3420 cx.assert_editor_state(indoc! {"
3421 //
3422 // ˇ
3423 "});
3424
3425 cx.set_state(indoc! {"
3426 ///ˇ
3427 "});
3428 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3429 cx.assert_editor_state(indoc! {"
3430 ///
3431 /// ˇ
3432 "});
3433 }
3434}
3435
3436#[gpui::test]
3437async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
3438 init_test(cx, |settings| {
3439 settings.defaults.tab_size = NonZeroU32::new(4)
3440 });
3441
3442 let language = Arc::new(
3443 Language::new(
3444 LanguageConfig {
3445 documentation_comment: Some(language::BlockCommentConfig {
3446 start: "/**".into(),
3447 end: "*/".into(),
3448 prefix: "* ".into(),
3449 tab_size: 1,
3450 }),
3451
3452 ..LanguageConfig::default()
3453 },
3454 Some(tree_sitter_rust::LANGUAGE.into()),
3455 )
3456 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
3457 .unwrap(),
3458 );
3459
3460 {
3461 let mut cx = EditorTestContext::new(cx).await;
3462 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3463 cx.set_state(indoc! {"
3464 /**ˇ
3465 "});
3466
3467 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3468 cx.assert_editor_state(indoc! {"
3469 /**
3470 * ˇ
3471 "});
3472 // Ensure that if cursor is before the comment start,
3473 // we do not actually insert a comment prefix.
3474 cx.set_state(indoc! {"
3475 ˇ/**
3476 "});
3477 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479
3480 ˇ/**
3481 "});
3482 // Ensure that if cursor is between it doesn't add comment prefix.
3483 cx.set_state(indoc! {"
3484 /*ˇ*
3485 "});
3486 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3487 cx.assert_editor_state(indoc! {"
3488 /*
3489 ˇ*
3490 "});
3491 // Ensure that if suffix exists on same line after cursor it adds new line.
3492 cx.set_state(indoc! {"
3493 /**ˇ*/
3494 "});
3495 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3496 cx.assert_editor_state(indoc! {"
3497 /**
3498 * ˇ
3499 */
3500 "});
3501 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3502 cx.set_state(indoc! {"
3503 /**ˇ */
3504 "});
3505 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3506 cx.assert_editor_state(indoc! {"
3507 /**
3508 * ˇ
3509 */
3510 "});
3511 // Ensure that if suffix exists on same line after cursor with space it adds new line.
3512 cx.set_state(indoc! {"
3513 /** ˇ*/
3514 "});
3515 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3516 cx.assert_editor_state(
3517 indoc! {"
3518 /**s
3519 * ˇ
3520 */
3521 "}
3522 .replace("s", " ") // s is used as space placeholder to prevent format on save
3523 .as_str(),
3524 );
3525 // Ensure that delimiter space is preserved when newline on already
3526 // spaced delimiter.
3527 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3528 cx.assert_editor_state(
3529 indoc! {"
3530 /**s
3531 *s
3532 * ˇ
3533 */
3534 "}
3535 .replace("s", " ") // s is used as space placeholder to prevent format on save
3536 .as_str(),
3537 );
3538 // Ensure that delimiter space is preserved when space is not
3539 // on existing delimiter.
3540 cx.set_state(indoc! {"
3541 /**
3542 *ˇ
3543 */
3544 "});
3545 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3546 cx.assert_editor_state(indoc! {"
3547 /**
3548 *
3549 * ˇ
3550 */
3551 "});
3552 // Ensure that if suffix exists on same line after cursor it
3553 // doesn't add extra new line if prefix is not on same line.
3554 cx.set_state(indoc! {"
3555 /**
3556 ˇ*/
3557 "});
3558 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3559 cx.assert_editor_state(indoc! {"
3560 /**
3561
3562 ˇ*/
3563 "});
3564 // Ensure that it detects suffix after existing prefix.
3565 cx.set_state(indoc! {"
3566 /**ˇ/
3567 "});
3568 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3569 cx.assert_editor_state(indoc! {"
3570 /**
3571 ˇ/
3572 "});
3573 // Ensure that if suffix exists on same line before
3574 // cursor it does not add comment prefix.
3575 cx.set_state(indoc! {"
3576 /** */ˇ
3577 "});
3578 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3579 cx.assert_editor_state(indoc! {"
3580 /** */
3581 ˇ
3582 "});
3583 // Ensure that if suffix exists on same line before
3584 // cursor it does not add comment prefix.
3585 cx.set_state(indoc! {"
3586 /**
3587 *
3588 */ˇ
3589 "});
3590 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3591 cx.assert_editor_state(indoc! {"
3592 /**
3593 *
3594 */
3595 ˇ
3596 "});
3597
3598 // Ensure that inline comment followed by code
3599 // doesn't add comment prefix on newline
3600 cx.set_state(indoc! {"
3601 /** */ textˇ
3602 "});
3603 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3604 cx.assert_editor_state(indoc! {"
3605 /** */ text
3606 ˇ
3607 "});
3608
3609 // Ensure that text after comment end tag
3610 // doesn't add comment prefix on newline
3611 cx.set_state(indoc! {"
3612 /**
3613 *
3614 */ˇtext
3615 "});
3616 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3617 cx.assert_editor_state(indoc! {"
3618 /**
3619 *
3620 */
3621 ˇtext
3622 "});
3623
3624 // Ensure if not comment block it doesn't
3625 // add comment prefix on newline
3626 cx.set_state(indoc! {"
3627 * textˇ
3628 "});
3629 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3630 cx.assert_editor_state(indoc! {"
3631 * text
3632 ˇ
3633 "});
3634 }
3635 // Ensure that comment continuations can be disabled.
3636 update_test_language_settings(cx, |settings| {
3637 settings.defaults.extend_comment_on_newline = Some(false);
3638 });
3639 let mut cx = EditorTestContext::new(cx).await;
3640 cx.set_state(indoc! {"
3641 /**ˇ
3642 "});
3643 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3644 cx.assert_editor_state(indoc! {"
3645 /**
3646 ˇ
3647 "});
3648}
3649
3650#[gpui::test]
3651async fn test_newline_comments_with_block_comment(cx: &mut TestAppContext) {
3652 init_test(cx, |settings| {
3653 settings.defaults.tab_size = NonZeroU32::new(4)
3654 });
3655
3656 let lua_language = Arc::new(Language::new(
3657 LanguageConfig {
3658 line_comments: vec!["--".into()],
3659 block_comment: Some(language::BlockCommentConfig {
3660 start: "--[[".into(),
3661 prefix: "".into(),
3662 end: "]]".into(),
3663 tab_size: 0,
3664 }),
3665 ..LanguageConfig::default()
3666 },
3667 None,
3668 ));
3669
3670 let mut cx = EditorTestContext::new(cx).await;
3671 cx.update_buffer(|buffer, cx| buffer.set_language(Some(lua_language), cx));
3672
3673 // Line with line comment should extend
3674 cx.set_state(indoc! {"
3675 --ˇ
3676 "});
3677 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3678 cx.assert_editor_state(indoc! {"
3679 --
3680 --ˇ
3681 "});
3682
3683 // Line with block comment that matches line comment should not extend
3684 cx.set_state(indoc! {"
3685 --[[ˇ
3686 "});
3687 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3688 cx.assert_editor_state(indoc! {"
3689 --[[
3690 ˇ
3691 "});
3692}
3693
3694#[gpui::test]
3695fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3696 init_test(cx, |_| {});
3697
3698 let editor = cx.add_window(|window, cx| {
3699 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3700 let mut editor = build_editor(buffer, window, cx);
3701 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
3702 s.select_ranges([3..4, 11..12, 19..20])
3703 });
3704 editor
3705 });
3706
3707 _ = editor.update(cx, |editor, window, cx| {
3708 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3709 editor.buffer.update(cx, |buffer, cx| {
3710 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3711 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3712 });
3713 assert_eq!(
3714 editor.selections.ranges(&editor.display_snapshot(cx)),
3715 &[2..2, 7..7, 12..12],
3716 );
3717
3718 editor.insert("Z", window, cx);
3719 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3720
3721 // The selections are moved after the inserted characters
3722 assert_eq!(
3723 editor.selections.ranges(&editor.display_snapshot(cx)),
3724 &[3..3, 9..9, 15..15],
3725 );
3726 });
3727}
3728
3729#[gpui::test]
3730async fn test_tab(cx: &mut TestAppContext) {
3731 init_test(cx, |settings| {
3732 settings.defaults.tab_size = NonZeroU32::new(3)
3733 });
3734
3735 let mut cx = EditorTestContext::new(cx).await;
3736 cx.set_state(indoc! {"
3737 ˇabˇc
3738 ˇ🏀ˇ🏀ˇefg
3739 dˇ
3740 "});
3741 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3742 cx.assert_editor_state(indoc! {"
3743 ˇab ˇc
3744 ˇ🏀 ˇ🏀 ˇefg
3745 d ˇ
3746 "});
3747
3748 cx.set_state(indoc! {"
3749 a
3750 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3751 "});
3752 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3753 cx.assert_editor_state(indoc! {"
3754 a
3755 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3756 "});
3757}
3758
3759#[gpui::test]
3760async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3761 init_test(cx, |_| {});
3762
3763 let mut cx = EditorTestContext::new(cx).await;
3764 let language = Arc::new(
3765 Language::new(
3766 LanguageConfig::default(),
3767 Some(tree_sitter_rust::LANGUAGE.into()),
3768 )
3769 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3770 .unwrap(),
3771 );
3772 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3773
3774 // test when all cursors are not at suggested indent
3775 // then simply move to their suggested indent location
3776 cx.set_state(indoc! {"
3777 const a: B = (
3778 c(
3779 ˇ
3780 ˇ )
3781 );
3782 "});
3783 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3784 cx.assert_editor_state(indoc! {"
3785 const a: B = (
3786 c(
3787 ˇ
3788 ˇ)
3789 );
3790 "});
3791
3792 // test cursor already at suggested indent not moving when
3793 // other cursors are yet to reach their suggested indents
3794 cx.set_state(indoc! {"
3795 ˇ
3796 const a: B = (
3797 c(
3798 d(
3799 ˇ
3800 )
3801 ˇ
3802 ˇ )
3803 );
3804 "});
3805 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3806 cx.assert_editor_state(indoc! {"
3807 ˇ
3808 const a: B = (
3809 c(
3810 d(
3811 ˇ
3812 )
3813 ˇ
3814 ˇ)
3815 );
3816 "});
3817 // test when all cursors are at suggested indent then tab is inserted
3818 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3819 cx.assert_editor_state(indoc! {"
3820 ˇ
3821 const a: B = (
3822 c(
3823 d(
3824 ˇ
3825 )
3826 ˇ
3827 ˇ)
3828 );
3829 "});
3830
3831 // test when current indent is less than suggested indent,
3832 // we adjust line to match suggested indent and move cursor to it
3833 //
3834 // when no other cursor is at word boundary, all of them should move
3835 cx.set_state(indoc! {"
3836 const a: B = (
3837 c(
3838 d(
3839 ˇ
3840 ˇ )
3841 ˇ )
3842 );
3843 "});
3844 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3845 cx.assert_editor_state(indoc! {"
3846 const a: B = (
3847 c(
3848 d(
3849 ˇ
3850 ˇ)
3851 ˇ)
3852 );
3853 "});
3854
3855 // test when current indent is less than suggested indent,
3856 // we adjust line to match suggested indent and move cursor to it
3857 //
3858 // when some other cursor is at word boundary, it should not move
3859 cx.set_state(indoc! {"
3860 const a: B = (
3861 c(
3862 d(
3863 ˇ
3864 ˇ )
3865 ˇ)
3866 );
3867 "});
3868 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3869 cx.assert_editor_state(indoc! {"
3870 const a: B = (
3871 c(
3872 d(
3873 ˇ
3874 ˇ)
3875 ˇ)
3876 );
3877 "});
3878
3879 // test when current indent is more than suggested indent,
3880 // we just move cursor to current indent instead of suggested indent
3881 //
3882 // when no other cursor is at word boundary, all of them should move
3883 cx.set_state(indoc! {"
3884 const a: B = (
3885 c(
3886 d(
3887 ˇ
3888 ˇ )
3889 ˇ )
3890 );
3891 "});
3892 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3893 cx.assert_editor_state(indoc! {"
3894 const a: B = (
3895 c(
3896 d(
3897 ˇ
3898 ˇ)
3899 ˇ)
3900 );
3901 "});
3902 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3903 cx.assert_editor_state(indoc! {"
3904 const a: B = (
3905 c(
3906 d(
3907 ˇ
3908 ˇ)
3909 ˇ)
3910 );
3911 "});
3912
3913 // test when current indent is more than suggested indent,
3914 // we just move cursor to current indent instead of suggested indent
3915 //
3916 // when some other cursor is at word boundary, it doesn't move
3917 cx.set_state(indoc! {"
3918 const a: B = (
3919 c(
3920 d(
3921 ˇ
3922 ˇ )
3923 ˇ)
3924 );
3925 "});
3926 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3927 cx.assert_editor_state(indoc! {"
3928 const a: B = (
3929 c(
3930 d(
3931 ˇ
3932 ˇ)
3933 ˇ)
3934 );
3935 "});
3936
3937 // handle auto-indent when there are multiple cursors on the same line
3938 cx.set_state(indoc! {"
3939 const a: B = (
3940 c(
3941 ˇ ˇ
3942 ˇ )
3943 );
3944 "});
3945 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3946 cx.assert_editor_state(indoc! {"
3947 const a: B = (
3948 c(
3949 ˇ
3950 ˇ)
3951 );
3952 "});
3953}
3954
3955#[gpui::test]
3956async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3957 init_test(cx, |settings| {
3958 settings.defaults.tab_size = NonZeroU32::new(3)
3959 });
3960
3961 let mut cx = EditorTestContext::new(cx).await;
3962 cx.set_state(indoc! {"
3963 ˇ
3964 \t ˇ
3965 \t ˇ
3966 \t ˇ
3967 \t \t\t \t \t\t \t\t \t \t ˇ
3968 "});
3969
3970 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3971 cx.assert_editor_state(indoc! {"
3972 ˇ
3973 \t ˇ
3974 \t ˇ
3975 \t ˇ
3976 \t \t\t \t \t\t \t\t \t \t ˇ
3977 "});
3978}
3979
3980#[gpui::test]
3981async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3982 init_test(cx, |settings| {
3983 settings.defaults.tab_size = NonZeroU32::new(4)
3984 });
3985
3986 let language = Arc::new(
3987 Language::new(
3988 LanguageConfig::default(),
3989 Some(tree_sitter_rust::LANGUAGE.into()),
3990 )
3991 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3992 .unwrap(),
3993 );
3994
3995 let mut cx = EditorTestContext::new(cx).await;
3996 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3997 cx.set_state(indoc! {"
3998 fn a() {
3999 if b {
4000 \t ˇc
4001 }
4002 }
4003 "});
4004
4005 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4006 cx.assert_editor_state(indoc! {"
4007 fn a() {
4008 if b {
4009 ˇc
4010 }
4011 }
4012 "});
4013}
4014
4015#[gpui::test]
4016async fn test_indent_outdent(cx: &mut TestAppContext) {
4017 init_test(cx, |settings| {
4018 settings.defaults.tab_size = NonZeroU32::new(4);
4019 });
4020
4021 let mut cx = EditorTestContext::new(cx).await;
4022
4023 cx.set_state(indoc! {"
4024 «oneˇ» «twoˇ»
4025 three
4026 four
4027 "});
4028 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4029 cx.assert_editor_state(indoc! {"
4030 «oneˇ» «twoˇ»
4031 three
4032 four
4033 "});
4034
4035 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4036 cx.assert_editor_state(indoc! {"
4037 «oneˇ» «twoˇ»
4038 three
4039 four
4040 "});
4041
4042 // select across line ending
4043 cx.set_state(indoc! {"
4044 one two
4045 t«hree
4046 ˇ» four
4047 "});
4048 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 one two
4051 t«hree
4052 ˇ» four
4053 "});
4054
4055 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4056 cx.assert_editor_state(indoc! {"
4057 one two
4058 t«hree
4059 ˇ» four
4060 "});
4061
4062 // Ensure that indenting/outdenting works when the cursor is at column 0.
4063 cx.set_state(indoc! {"
4064 one two
4065 ˇthree
4066 four
4067 "});
4068 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4069 cx.assert_editor_state(indoc! {"
4070 one two
4071 ˇthree
4072 four
4073 "});
4074
4075 cx.set_state(indoc! {"
4076 one two
4077 ˇ three
4078 four
4079 "});
4080 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4081 cx.assert_editor_state(indoc! {"
4082 one two
4083 ˇthree
4084 four
4085 "});
4086}
4087
4088#[gpui::test]
4089async fn test_indent_yaml_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4090 // This is a regression test for issue #33761
4091 init_test(cx, |_| {});
4092
4093 let mut cx = EditorTestContext::new(cx).await;
4094 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4095 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4096
4097 cx.set_state(
4098 r#"ˇ# ingress:
4099ˇ# api:
4100ˇ# enabled: false
4101ˇ# pathType: Prefix
4102ˇ# console:
4103ˇ# enabled: false
4104ˇ# pathType: Prefix
4105"#,
4106 );
4107
4108 // Press tab to indent all lines
4109 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4110
4111 cx.assert_editor_state(
4112 r#" ˇ# ingress:
4113 ˇ# api:
4114 ˇ# enabled: false
4115 ˇ# pathType: Prefix
4116 ˇ# console:
4117 ˇ# enabled: false
4118 ˇ# pathType: Prefix
4119"#,
4120 );
4121}
4122
4123#[gpui::test]
4124async fn test_indent_yaml_non_comments_with_multiple_cursors(cx: &mut TestAppContext) {
4125 // This is a test to make sure our fix for issue #33761 didn't break anything
4126 init_test(cx, |_| {});
4127
4128 let mut cx = EditorTestContext::new(cx).await;
4129 let yaml_language = languages::language("yaml", tree_sitter_yaml::LANGUAGE.into());
4130 cx.update_buffer(|buffer, cx| buffer.set_language(Some(yaml_language), cx));
4131
4132 cx.set_state(
4133 r#"ˇingress:
4134ˇ api:
4135ˇ enabled: false
4136ˇ pathType: Prefix
4137"#,
4138 );
4139
4140 // Press tab to indent all lines
4141 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4142
4143 cx.assert_editor_state(
4144 r#"ˇingress:
4145 ˇapi:
4146 ˇenabled: false
4147 ˇpathType: Prefix
4148"#,
4149 );
4150}
4151
4152#[gpui::test]
4153async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
4154 init_test(cx, |settings| {
4155 settings.defaults.hard_tabs = Some(true);
4156 });
4157
4158 let mut cx = EditorTestContext::new(cx).await;
4159
4160 // select two ranges on one line
4161 cx.set_state(indoc! {"
4162 «oneˇ» «twoˇ»
4163 three
4164 four
4165 "});
4166 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4167 cx.assert_editor_state(indoc! {"
4168 \t«oneˇ» «twoˇ»
4169 three
4170 four
4171 "});
4172 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4173 cx.assert_editor_state(indoc! {"
4174 \t\t«oneˇ» «twoˇ»
4175 three
4176 four
4177 "});
4178 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4179 cx.assert_editor_state(indoc! {"
4180 \t«oneˇ» «twoˇ»
4181 three
4182 four
4183 "});
4184 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4185 cx.assert_editor_state(indoc! {"
4186 «oneˇ» «twoˇ»
4187 three
4188 four
4189 "});
4190
4191 // select across a line ending
4192 cx.set_state(indoc! {"
4193 one two
4194 t«hree
4195 ˇ»four
4196 "});
4197 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4198 cx.assert_editor_state(indoc! {"
4199 one two
4200 \tt«hree
4201 ˇ»four
4202 "});
4203 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4204 cx.assert_editor_state(indoc! {"
4205 one two
4206 \t\tt«hree
4207 ˇ»four
4208 "});
4209 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4210 cx.assert_editor_state(indoc! {"
4211 one two
4212 \tt«hree
4213 ˇ»four
4214 "});
4215 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4216 cx.assert_editor_state(indoc! {"
4217 one two
4218 t«hree
4219 ˇ»four
4220 "});
4221
4222 // Ensure that indenting/outdenting works when the cursor is at column 0.
4223 cx.set_state(indoc! {"
4224 one two
4225 ˇthree
4226 four
4227 "});
4228 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4229 cx.assert_editor_state(indoc! {"
4230 one two
4231 ˇthree
4232 four
4233 "});
4234 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
4235 cx.assert_editor_state(indoc! {"
4236 one two
4237 \tˇthree
4238 four
4239 "});
4240 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
4241 cx.assert_editor_state(indoc! {"
4242 one two
4243 ˇthree
4244 four
4245 "});
4246}
4247
4248#[gpui::test]
4249fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
4250 init_test(cx, |settings| {
4251 settings.languages.0.extend([
4252 (
4253 "TOML".into(),
4254 LanguageSettingsContent {
4255 tab_size: NonZeroU32::new(2),
4256 ..Default::default()
4257 },
4258 ),
4259 (
4260 "Rust".into(),
4261 LanguageSettingsContent {
4262 tab_size: NonZeroU32::new(4),
4263 ..Default::default()
4264 },
4265 ),
4266 ]);
4267 });
4268
4269 let toml_language = Arc::new(Language::new(
4270 LanguageConfig {
4271 name: "TOML".into(),
4272 ..Default::default()
4273 },
4274 None,
4275 ));
4276 let rust_language = Arc::new(Language::new(
4277 LanguageConfig {
4278 name: "Rust".into(),
4279 ..Default::default()
4280 },
4281 None,
4282 ));
4283
4284 let toml_buffer =
4285 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
4286 let rust_buffer =
4287 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
4288 let multibuffer = cx.new(|cx| {
4289 let mut multibuffer = MultiBuffer::new(ReadWrite);
4290 multibuffer.push_excerpts(
4291 toml_buffer.clone(),
4292 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
4293 cx,
4294 );
4295 multibuffer.push_excerpts(
4296 rust_buffer.clone(),
4297 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
4298 cx,
4299 );
4300 multibuffer
4301 });
4302
4303 cx.add_window(|window, cx| {
4304 let mut editor = build_editor(multibuffer, window, cx);
4305
4306 assert_eq!(
4307 editor.text(cx),
4308 indoc! {"
4309 a = 1
4310 b = 2
4311
4312 const c: usize = 3;
4313 "}
4314 );
4315
4316 select_ranges(
4317 &mut editor,
4318 indoc! {"
4319 «aˇ» = 1
4320 b = 2
4321
4322 «const c:ˇ» usize = 3;
4323 "},
4324 window,
4325 cx,
4326 );
4327
4328 editor.tab(&Tab, window, cx);
4329 assert_text_with_selections(
4330 &mut editor,
4331 indoc! {"
4332 «aˇ» = 1
4333 b = 2
4334
4335 «const c:ˇ» usize = 3;
4336 "},
4337 cx,
4338 );
4339 editor.backtab(&Backtab, window, cx);
4340 assert_text_with_selections(
4341 &mut editor,
4342 indoc! {"
4343 «aˇ» = 1
4344 b = 2
4345
4346 «const c:ˇ» usize = 3;
4347 "},
4348 cx,
4349 );
4350
4351 editor
4352 });
4353}
4354
4355#[gpui::test]
4356async fn test_backspace(cx: &mut TestAppContext) {
4357 init_test(cx, |_| {});
4358
4359 let mut cx = EditorTestContext::new(cx).await;
4360
4361 // Basic backspace
4362 cx.set_state(indoc! {"
4363 onˇe two three
4364 fou«rˇ» five six
4365 seven «ˇeight nine
4366 »ten
4367 "});
4368 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4369 cx.assert_editor_state(indoc! {"
4370 oˇe two three
4371 fouˇ five six
4372 seven ˇten
4373 "});
4374
4375 // Test backspace inside and around indents
4376 cx.set_state(indoc! {"
4377 zero
4378 ˇone
4379 ˇtwo
4380 ˇ ˇ ˇ three
4381 ˇ ˇ four
4382 "});
4383 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
4384 cx.assert_editor_state(indoc! {"
4385 zero
4386 ˇone
4387 ˇtwo
4388 ˇ threeˇ four
4389 "});
4390}
4391
4392#[gpui::test]
4393async fn test_delete(cx: &mut TestAppContext) {
4394 init_test(cx, |_| {});
4395
4396 let mut cx = EditorTestContext::new(cx).await;
4397 cx.set_state(indoc! {"
4398 onˇe two three
4399 fou«rˇ» five six
4400 seven «ˇeight nine
4401 »ten
4402 "});
4403 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
4404 cx.assert_editor_state(indoc! {"
4405 onˇ two three
4406 fouˇ five six
4407 seven ˇten
4408 "});
4409}
4410
4411#[gpui::test]
4412fn test_delete_line(cx: &mut TestAppContext) {
4413 init_test(cx, |_| {});
4414
4415 let editor = cx.add_window(|window, cx| {
4416 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4417 build_editor(buffer, window, cx)
4418 });
4419 _ = editor.update(cx, |editor, window, cx| {
4420 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4421 s.select_display_ranges([
4422 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4423 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4424 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4425 ])
4426 });
4427 editor.delete_line(&DeleteLine, window, cx);
4428 assert_eq!(editor.display_text(cx), "ghi");
4429 assert_eq!(
4430 display_ranges(editor, cx),
4431 vec![
4432 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
4433 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4434 ]
4435 );
4436 });
4437
4438 let editor = cx.add_window(|window, cx| {
4439 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4440 build_editor(buffer, window, cx)
4441 });
4442 _ = editor.update(cx, |editor, window, cx| {
4443 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4444 s.select_display_ranges([
4445 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
4446 ])
4447 });
4448 editor.delete_line(&DeleteLine, window, cx);
4449 assert_eq!(editor.display_text(cx), "ghi\n");
4450 assert_eq!(
4451 display_ranges(editor, cx),
4452 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
4453 );
4454 });
4455
4456 let editor = cx.add_window(|window, cx| {
4457 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n\njkl\nmno", cx);
4458 build_editor(buffer, window, cx)
4459 });
4460 _ = editor.update(cx, |editor, window, cx| {
4461 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4462 s.select_display_ranges([
4463 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(2), 1)
4464 ])
4465 });
4466 editor.delete_line(&DeleteLine, window, cx);
4467 assert_eq!(editor.display_text(cx), "\njkl\nmno");
4468 assert_eq!(
4469 display_ranges(editor, cx),
4470 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
4471 );
4472 });
4473}
4474
4475#[gpui::test]
4476fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
4477 init_test(cx, |_| {});
4478
4479 cx.add_window(|window, cx| {
4480 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4481 let mut editor = build_editor(buffer.clone(), window, cx);
4482 let buffer = buffer.read(cx).as_singleton().unwrap();
4483
4484 assert_eq!(
4485 editor
4486 .selections
4487 .ranges::<Point>(&editor.display_snapshot(cx)),
4488 &[Point::new(0, 0)..Point::new(0, 0)]
4489 );
4490
4491 // When on single line, replace newline at end by space
4492 editor.join_lines(&JoinLines, window, cx);
4493 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4494 assert_eq!(
4495 editor
4496 .selections
4497 .ranges::<Point>(&editor.display_snapshot(cx)),
4498 &[Point::new(0, 3)..Point::new(0, 3)]
4499 );
4500
4501 // When multiple lines are selected, remove newlines that are spanned by the selection
4502 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4503 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
4504 });
4505 editor.join_lines(&JoinLines, window, cx);
4506 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
4507 assert_eq!(
4508 editor
4509 .selections
4510 .ranges::<Point>(&editor.display_snapshot(cx)),
4511 &[Point::new(0, 11)..Point::new(0, 11)]
4512 );
4513
4514 // Undo should be transactional
4515 editor.undo(&Undo, window, cx);
4516 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
4517 assert_eq!(
4518 editor
4519 .selections
4520 .ranges::<Point>(&editor.display_snapshot(cx)),
4521 &[Point::new(0, 5)..Point::new(2, 2)]
4522 );
4523
4524 // When joining an empty line don't insert a space
4525 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4526 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
4527 });
4528 editor.join_lines(&JoinLines, window, cx);
4529 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
4530 assert_eq!(
4531 editor
4532 .selections
4533 .ranges::<Point>(&editor.display_snapshot(cx)),
4534 [Point::new(2, 3)..Point::new(2, 3)]
4535 );
4536
4537 // We can remove trailing newlines
4538 editor.join_lines(&JoinLines, window, cx);
4539 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4540 assert_eq!(
4541 editor
4542 .selections
4543 .ranges::<Point>(&editor.display_snapshot(cx)),
4544 [Point::new(2, 3)..Point::new(2, 3)]
4545 );
4546
4547 // We don't blow up on the last line
4548 editor.join_lines(&JoinLines, window, cx);
4549 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
4550 assert_eq!(
4551 editor
4552 .selections
4553 .ranges::<Point>(&editor.display_snapshot(cx)),
4554 [Point::new(2, 3)..Point::new(2, 3)]
4555 );
4556
4557 // reset to test indentation
4558 editor.buffer.update(cx, |buffer, cx| {
4559 buffer.edit(
4560 [
4561 (Point::new(1, 0)..Point::new(1, 2), " "),
4562 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
4563 ],
4564 None,
4565 cx,
4566 )
4567 });
4568
4569 // We remove any leading spaces
4570 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
4571 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4572 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
4573 });
4574 editor.join_lines(&JoinLines, window, cx);
4575 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
4576
4577 // We don't insert a space for a line containing only spaces
4578 editor.join_lines(&JoinLines, window, cx);
4579 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
4580
4581 // We ignore any leading tabs
4582 editor.join_lines(&JoinLines, window, cx);
4583 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
4584
4585 editor
4586 });
4587}
4588
4589#[gpui::test]
4590fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
4591 init_test(cx, |_| {});
4592
4593 cx.add_window(|window, cx| {
4594 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
4595 let mut editor = build_editor(buffer.clone(), window, cx);
4596 let buffer = buffer.read(cx).as_singleton().unwrap();
4597
4598 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
4599 s.select_ranges([
4600 Point::new(0, 2)..Point::new(1, 1),
4601 Point::new(1, 2)..Point::new(1, 2),
4602 Point::new(3, 1)..Point::new(3, 2),
4603 ])
4604 });
4605
4606 editor.join_lines(&JoinLines, window, cx);
4607 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
4608
4609 assert_eq!(
4610 editor
4611 .selections
4612 .ranges::<Point>(&editor.display_snapshot(cx)),
4613 [
4614 Point::new(0, 7)..Point::new(0, 7),
4615 Point::new(1, 3)..Point::new(1, 3)
4616 ]
4617 );
4618 editor
4619 });
4620}
4621
4622#[gpui::test]
4623async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
4624 init_test(cx, |_| {});
4625
4626 let mut cx = EditorTestContext::new(cx).await;
4627
4628 let diff_base = r#"
4629 Line 0
4630 Line 1
4631 Line 2
4632 Line 3
4633 "#
4634 .unindent();
4635
4636 cx.set_state(
4637 &r#"
4638 ˇLine 0
4639 Line 1
4640 Line 2
4641 Line 3
4642 "#
4643 .unindent(),
4644 );
4645
4646 cx.set_head_text(&diff_base);
4647 executor.run_until_parked();
4648
4649 // Join lines
4650 cx.update_editor(|editor, window, cx| {
4651 editor.join_lines(&JoinLines, window, cx);
4652 });
4653 executor.run_until_parked();
4654
4655 cx.assert_editor_state(
4656 &r#"
4657 Line 0ˇ Line 1
4658 Line 2
4659 Line 3
4660 "#
4661 .unindent(),
4662 );
4663 // Join again
4664 cx.update_editor(|editor, window, cx| {
4665 editor.join_lines(&JoinLines, window, cx);
4666 });
4667 executor.run_until_parked();
4668
4669 cx.assert_editor_state(
4670 &r#"
4671 Line 0 Line 1ˇ Line 2
4672 Line 3
4673 "#
4674 .unindent(),
4675 );
4676}
4677
4678#[gpui::test]
4679async fn test_custom_newlines_cause_no_false_positive_diffs(
4680 executor: BackgroundExecutor,
4681 cx: &mut TestAppContext,
4682) {
4683 init_test(cx, |_| {});
4684 let mut cx = EditorTestContext::new(cx).await;
4685 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
4686 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
4687 executor.run_until_parked();
4688
4689 cx.update_editor(|editor, window, cx| {
4690 let snapshot = editor.snapshot(window, cx);
4691 assert_eq!(
4692 snapshot
4693 .buffer_snapshot()
4694 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
4695 .collect::<Vec<_>>(),
4696 Vec::new(),
4697 "Should not have any diffs for files with custom newlines"
4698 );
4699 });
4700}
4701
4702#[gpui::test]
4703async fn test_manipulate_immutable_lines_with_single_selection(cx: &mut TestAppContext) {
4704 init_test(cx, |_| {});
4705
4706 let mut cx = EditorTestContext::new(cx).await;
4707
4708 // Test sort_lines_case_insensitive()
4709 cx.set_state(indoc! {"
4710 «z
4711 y
4712 x
4713 Z
4714 Y
4715 Xˇ»
4716 "});
4717 cx.update_editor(|e, window, cx| {
4718 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
4719 });
4720 cx.assert_editor_state(indoc! {"
4721 «x
4722 X
4723 y
4724 Y
4725 z
4726 Zˇ»
4727 "});
4728
4729 // Test sort_lines_by_length()
4730 //
4731 // Demonstrates:
4732 // - ∞ is 3 bytes UTF-8, but sorted by its char count (1)
4733 // - sort is stable
4734 cx.set_state(indoc! {"
4735 «123
4736 æ
4737 12
4738 ∞
4739 1
4740 æˇ»
4741 "});
4742 cx.update_editor(|e, window, cx| e.sort_lines_by_length(&SortLinesByLength, window, cx));
4743 cx.assert_editor_state(indoc! {"
4744 «æ
4745 ∞
4746 1
4747 æ
4748 12
4749 123ˇ»
4750 "});
4751
4752 // Test reverse_lines()
4753 cx.set_state(indoc! {"
4754 «5
4755 4
4756 3
4757 2
4758 1ˇ»
4759 "});
4760 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4761 cx.assert_editor_state(indoc! {"
4762 «1
4763 2
4764 3
4765 4
4766 5ˇ»
4767 "});
4768
4769 // Skip testing shuffle_line()
4770
4771 // From here on out, test more complex cases of manipulate_immutable_lines() with a single driver method: sort_lines_case_sensitive()
4772 // Since all methods calling manipulate_immutable_lines() are doing the exact same general thing (reordering lines)
4773
4774 // Don't manipulate when cursor is on single line, but expand the selection
4775 cx.set_state(indoc! {"
4776 ddˇdd
4777 ccc
4778 bb
4779 a
4780 "});
4781 cx.update_editor(|e, window, cx| {
4782 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4783 });
4784 cx.assert_editor_state(indoc! {"
4785 «ddddˇ»
4786 ccc
4787 bb
4788 a
4789 "});
4790
4791 // Basic manipulate case
4792 // Start selection moves to column 0
4793 // End of selection shrinks to fit shorter line
4794 cx.set_state(indoc! {"
4795 dd«d
4796 ccc
4797 bb
4798 aaaaaˇ»
4799 "});
4800 cx.update_editor(|e, window, cx| {
4801 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4802 });
4803 cx.assert_editor_state(indoc! {"
4804 «aaaaa
4805 bb
4806 ccc
4807 dddˇ»
4808 "});
4809
4810 // Manipulate case with newlines
4811 cx.set_state(indoc! {"
4812 dd«d
4813 ccc
4814
4815 bb
4816 aaaaa
4817
4818 ˇ»
4819 "});
4820 cx.update_editor(|e, window, cx| {
4821 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4822 });
4823 cx.assert_editor_state(indoc! {"
4824 «
4825
4826 aaaaa
4827 bb
4828 ccc
4829 dddˇ»
4830
4831 "});
4832
4833 // Adding new line
4834 cx.set_state(indoc! {"
4835 aa«a
4836 bbˇ»b
4837 "});
4838 cx.update_editor(|e, window, cx| {
4839 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added_line"))
4840 });
4841 cx.assert_editor_state(indoc! {"
4842 «aaa
4843 bbb
4844 added_lineˇ»
4845 "});
4846
4847 // Removing line
4848 cx.set_state(indoc! {"
4849 aa«a
4850 bbbˇ»
4851 "});
4852 cx.update_editor(|e, window, cx| {
4853 e.manipulate_immutable_lines(window, cx, |lines| {
4854 lines.pop();
4855 })
4856 });
4857 cx.assert_editor_state(indoc! {"
4858 «aaaˇ»
4859 "});
4860
4861 // Removing all lines
4862 cx.set_state(indoc! {"
4863 aa«a
4864 bbbˇ»
4865 "});
4866 cx.update_editor(|e, window, cx| {
4867 e.manipulate_immutable_lines(window, cx, |lines| {
4868 lines.drain(..);
4869 })
4870 });
4871 cx.assert_editor_state(indoc! {"
4872 ˇ
4873 "});
4874}
4875
4876#[gpui::test]
4877async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4878 init_test(cx, |_| {});
4879
4880 let mut cx = EditorTestContext::new(cx).await;
4881
4882 // Consider continuous selection as single selection
4883 cx.set_state(indoc! {"
4884 Aaa«aa
4885 cˇ»c«c
4886 bb
4887 aaaˇ»aa
4888 "});
4889 cx.update_editor(|e, window, cx| {
4890 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4891 });
4892 cx.assert_editor_state(indoc! {"
4893 «Aaaaa
4894 ccc
4895 bb
4896 aaaaaˇ»
4897 "});
4898
4899 cx.set_state(indoc! {"
4900 Aaa«aa
4901 cˇ»c«c
4902 bb
4903 aaaˇ»aa
4904 "});
4905 cx.update_editor(|e, window, cx| {
4906 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4907 });
4908 cx.assert_editor_state(indoc! {"
4909 «Aaaaa
4910 ccc
4911 bbˇ»
4912 "});
4913
4914 // Consider non continuous selection as distinct dedup operations
4915 cx.set_state(indoc! {"
4916 «aaaaa
4917 bb
4918 aaaaa
4919 aaaaaˇ»
4920
4921 aaa«aaˇ»
4922 "});
4923 cx.update_editor(|e, window, cx| {
4924 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4925 });
4926 cx.assert_editor_state(indoc! {"
4927 «aaaaa
4928 bbˇ»
4929
4930 «aaaaaˇ»
4931 "});
4932}
4933
4934#[gpui::test]
4935async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4936 init_test(cx, |_| {});
4937
4938 let mut cx = EditorTestContext::new(cx).await;
4939
4940 cx.set_state(indoc! {"
4941 «Aaa
4942 aAa
4943 Aaaˇ»
4944 "});
4945 cx.update_editor(|e, window, cx| {
4946 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4947 });
4948 cx.assert_editor_state(indoc! {"
4949 «Aaa
4950 aAaˇ»
4951 "});
4952
4953 cx.set_state(indoc! {"
4954 «Aaa
4955 aAa
4956 aaAˇ»
4957 "});
4958 cx.update_editor(|e, window, cx| {
4959 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4960 });
4961 cx.assert_editor_state(indoc! {"
4962 «Aaaˇ»
4963 "});
4964}
4965
4966#[gpui::test]
4967async fn test_wrap_in_tag_single_selection(cx: &mut TestAppContext) {
4968 init_test(cx, |_| {});
4969
4970 let mut cx = EditorTestContext::new(cx).await;
4971
4972 let js_language = Arc::new(Language::new(
4973 LanguageConfig {
4974 name: "JavaScript".into(),
4975 wrap_characters: Some(language::WrapCharactersConfig {
4976 start_prefix: "<".into(),
4977 start_suffix: ">".into(),
4978 end_prefix: "</".into(),
4979 end_suffix: ">".into(),
4980 }),
4981 ..LanguageConfig::default()
4982 },
4983 None,
4984 ));
4985
4986 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
4987
4988 cx.set_state(indoc! {"
4989 «testˇ»
4990 "});
4991 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
4992 cx.assert_editor_state(indoc! {"
4993 <«ˇ»>test</«ˇ»>
4994 "});
4995
4996 cx.set_state(indoc! {"
4997 «test
4998 testˇ»
4999 "});
5000 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5001 cx.assert_editor_state(indoc! {"
5002 <«ˇ»>test
5003 test</«ˇ»>
5004 "});
5005
5006 cx.set_state(indoc! {"
5007 teˇst
5008 "});
5009 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5010 cx.assert_editor_state(indoc! {"
5011 te<«ˇ»></«ˇ»>st
5012 "});
5013}
5014
5015#[gpui::test]
5016async fn test_wrap_in_tag_multi_selection(cx: &mut TestAppContext) {
5017 init_test(cx, |_| {});
5018
5019 let mut cx = EditorTestContext::new(cx).await;
5020
5021 let js_language = Arc::new(Language::new(
5022 LanguageConfig {
5023 name: "JavaScript".into(),
5024 wrap_characters: Some(language::WrapCharactersConfig {
5025 start_prefix: "<".into(),
5026 start_suffix: ">".into(),
5027 end_prefix: "</".into(),
5028 end_suffix: ">".into(),
5029 }),
5030 ..LanguageConfig::default()
5031 },
5032 None,
5033 ));
5034
5035 cx.update_buffer(|buffer, cx| buffer.set_language(Some(js_language), cx));
5036
5037 cx.set_state(indoc! {"
5038 «testˇ»
5039 «testˇ» «testˇ»
5040 «testˇ»
5041 "});
5042 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5043 cx.assert_editor_state(indoc! {"
5044 <«ˇ»>test</«ˇ»>
5045 <«ˇ»>test</«ˇ»> <«ˇ»>test</«ˇ»>
5046 <«ˇ»>test</«ˇ»>
5047 "});
5048
5049 cx.set_state(indoc! {"
5050 «test
5051 testˇ»
5052 «test
5053 testˇ»
5054 "});
5055 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5056 cx.assert_editor_state(indoc! {"
5057 <«ˇ»>test
5058 test</«ˇ»>
5059 <«ˇ»>test
5060 test</«ˇ»>
5061 "});
5062}
5063
5064#[gpui::test]
5065async fn test_wrap_in_tag_does_nothing_in_unsupported_languages(cx: &mut TestAppContext) {
5066 init_test(cx, |_| {});
5067
5068 let mut cx = EditorTestContext::new(cx).await;
5069
5070 let plaintext_language = Arc::new(Language::new(
5071 LanguageConfig {
5072 name: "Plain Text".into(),
5073 ..LanguageConfig::default()
5074 },
5075 None,
5076 ));
5077
5078 cx.update_buffer(|buffer, cx| buffer.set_language(Some(plaintext_language), cx));
5079
5080 cx.set_state(indoc! {"
5081 «testˇ»
5082 "});
5083 cx.update_editor(|e, window, cx| e.wrap_selections_in_tag(&WrapSelectionsInTag, window, cx));
5084 cx.assert_editor_state(indoc! {"
5085 «testˇ»
5086 "});
5087}
5088
5089#[gpui::test]
5090async fn test_manipulate_immutable_lines_with_multi_selection(cx: &mut TestAppContext) {
5091 init_test(cx, |_| {});
5092
5093 let mut cx = EditorTestContext::new(cx).await;
5094
5095 // Manipulate with multiple selections on a single line
5096 cx.set_state(indoc! {"
5097 dd«dd
5098 cˇ»c«c
5099 bb
5100 aaaˇ»aa
5101 "});
5102 cx.update_editor(|e, window, cx| {
5103 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5104 });
5105 cx.assert_editor_state(indoc! {"
5106 «aaaaa
5107 bb
5108 ccc
5109 ddddˇ»
5110 "});
5111
5112 // Manipulate with multiple disjoin selections
5113 cx.set_state(indoc! {"
5114 5«
5115 4
5116 3
5117 2
5118 1ˇ»
5119
5120 dd«dd
5121 ccc
5122 bb
5123 aaaˇ»aa
5124 "});
5125 cx.update_editor(|e, window, cx| {
5126 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
5127 });
5128 cx.assert_editor_state(indoc! {"
5129 «1
5130 2
5131 3
5132 4
5133 5ˇ»
5134
5135 «aaaaa
5136 bb
5137 ccc
5138 ddddˇ»
5139 "});
5140
5141 // Adding lines on each selection
5142 cx.set_state(indoc! {"
5143 2«
5144 1ˇ»
5145
5146 bb«bb
5147 aaaˇ»aa
5148 "});
5149 cx.update_editor(|e, window, cx| {
5150 e.manipulate_immutable_lines(window, cx, |lines| lines.push("added line"))
5151 });
5152 cx.assert_editor_state(indoc! {"
5153 «2
5154 1
5155 added lineˇ»
5156
5157 «bbbb
5158 aaaaa
5159 added lineˇ»
5160 "});
5161
5162 // Removing lines on each selection
5163 cx.set_state(indoc! {"
5164 2«
5165 1ˇ»
5166
5167 bb«bb
5168 aaaˇ»aa
5169 "});
5170 cx.update_editor(|e, window, cx| {
5171 e.manipulate_immutable_lines(window, cx, |lines| {
5172 lines.pop();
5173 })
5174 });
5175 cx.assert_editor_state(indoc! {"
5176 «2ˇ»
5177
5178 «bbbbˇ»
5179 "});
5180}
5181
5182#[gpui::test]
5183async fn test_convert_indentation_to_spaces(cx: &mut TestAppContext) {
5184 init_test(cx, |settings| {
5185 settings.defaults.tab_size = NonZeroU32::new(3)
5186 });
5187
5188 let mut cx = EditorTestContext::new(cx).await;
5189
5190 // MULTI SELECTION
5191 // Ln.1 "«" tests empty lines
5192 // Ln.9 tests just leading whitespace
5193 cx.set_state(indoc! {"
5194 «
5195 abc // No indentationˇ»
5196 «\tabc // 1 tabˇ»
5197 \t\tabc « ˇ» // 2 tabs
5198 \t ab«c // Tab followed by space
5199 \tabc // Space followed by tab (3 spaces should be the result)
5200 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5201 abˇ»ˇc ˇ ˇ // Already space indented«
5202 \t
5203 \tabc\tdef // Only the leading tab is manipulatedˇ»
5204 "});
5205 cx.update_editor(|e, window, cx| {
5206 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5207 });
5208 cx.assert_editor_state(
5209 indoc! {"
5210 «
5211 abc // No indentation
5212 abc // 1 tab
5213 abc // 2 tabs
5214 abc // Tab followed by space
5215 abc // Space followed by tab (3 spaces should be the result)
5216 abc // Mixed indentation (tab conversion depends on the column)
5217 abc // Already space indented
5218 ·
5219 abc\tdef // Only the leading tab is manipulatedˇ»
5220 "}
5221 .replace("·", "")
5222 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5223 );
5224
5225 // Test on just a few lines, the others should remain unchanged
5226 // Only lines (3, 5, 10, 11) should change
5227 cx.set_state(
5228 indoc! {"
5229 ·
5230 abc // No indentation
5231 \tabcˇ // 1 tab
5232 \t\tabc // 2 tabs
5233 \t abcˇ // Tab followed by space
5234 \tabc // Space followed by tab (3 spaces should be the result)
5235 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5236 abc // Already space indented
5237 «\t
5238 \tabc\tdef // Only the leading tab is manipulatedˇ»
5239 "}
5240 .replace("·", "")
5241 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5242 );
5243 cx.update_editor(|e, window, cx| {
5244 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5245 });
5246 cx.assert_editor_state(
5247 indoc! {"
5248 ·
5249 abc // No indentation
5250 « abc // 1 tabˇ»
5251 \t\tabc // 2 tabs
5252 « abc // Tab followed by spaceˇ»
5253 \tabc // Space followed by tab (3 spaces should be the result)
5254 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5255 abc // Already space indented
5256 « ·
5257 abc\tdef // Only the leading tab is manipulatedˇ»
5258 "}
5259 .replace("·", "")
5260 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5261 );
5262
5263 // SINGLE SELECTION
5264 // Ln.1 "«" tests empty lines
5265 // Ln.9 tests just leading whitespace
5266 cx.set_state(indoc! {"
5267 «
5268 abc // No indentation
5269 \tabc // 1 tab
5270 \t\tabc // 2 tabs
5271 \t abc // Tab followed by space
5272 \tabc // Space followed by tab (3 spaces should be the result)
5273 \t \t \t \tabc // Mixed indentation (tab conversion depends on the column)
5274 abc // Already space indented
5275 \t
5276 \tabc\tdef // Only the leading tab is manipulatedˇ»
5277 "});
5278 cx.update_editor(|e, window, cx| {
5279 e.convert_indentation_to_spaces(&ConvertIndentationToSpaces, window, cx);
5280 });
5281 cx.assert_editor_state(
5282 indoc! {"
5283 «
5284 abc // No indentation
5285 abc // 1 tab
5286 abc // 2 tabs
5287 abc // Tab followed by space
5288 abc // Space followed by tab (3 spaces should be the result)
5289 abc // Mixed indentation (tab conversion depends on the column)
5290 abc // Already space indented
5291 ·
5292 abc\tdef // Only the leading tab is manipulatedˇ»
5293 "}
5294 .replace("·", "")
5295 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5296 );
5297}
5298
5299#[gpui::test]
5300async fn test_convert_indentation_to_tabs(cx: &mut TestAppContext) {
5301 init_test(cx, |settings| {
5302 settings.defaults.tab_size = NonZeroU32::new(3)
5303 });
5304
5305 let mut cx = EditorTestContext::new(cx).await;
5306
5307 // MULTI SELECTION
5308 // Ln.1 "«" tests empty lines
5309 // Ln.11 tests just leading whitespace
5310 cx.set_state(indoc! {"
5311 «
5312 abˇ»ˇc // No indentation
5313 abc ˇ ˇ // 1 space (< 3 so dont convert)
5314 abc « // 2 spaces (< 3 so dont convert)
5315 abc // 3 spaces (convert)
5316 abc ˇ» // 5 spaces (1 tab + 2 spaces)
5317 «\tˇ»\t«\tˇ»abc // Already tab indented
5318 «\t abc // Tab followed by space
5319 \tabc // Space followed by tab (should be consumed due to tab)
5320 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5321 \tˇ» «\t
5322 abcˇ» \t ˇˇˇ // Only the leading spaces should be converted
5323 "});
5324 cx.update_editor(|e, window, cx| {
5325 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5326 });
5327 cx.assert_editor_state(indoc! {"
5328 «
5329 abc // No indentation
5330 abc // 1 space (< 3 so dont convert)
5331 abc // 2 spaces (< 3 so dont convert)
5332 \tabc // 3 spaces (convert)
5333 \t abc // 5 spaces (1 tab + 2 spaces)
5334 \t\t\tabc // Already tab indented
5335 \t abc // Tab followed by space
5336 \tabc // Space followed by tab (should be consumed due to tab)
5337 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5338 \t\t\t
5339 \tabc \t // Only the leading spaces should be convertedˇ»
5340 "});
5341
5342 // Test on just a few lines, the other should remain unchanged
5343 // Only lines (4, 8, 11, 12) should change
5344 cx.set_state(
5345 indoc! {"
5346 ·
5347 abc // No indentation
5348 abc // 1 space (< 3 so dont convert)
5349 abc // 2 spaces (< 3 so dont convert)
5350 « abc // 3 spaces (convert)ˇ»
5351 abc // 5 spaces (1 tab + 2 spaces)
5352 \t\t\tabc // Already tab indented
5353 \t abc // Tab followed by space
5354 \tabc ˇ // Space followed by tab (should be consumed due to tab)
5355 \t\t \tabc // Mixed indentation
5356 \t \t \t \tabc // Mixed indentation
5357 \t \tˇ
5358 « abc \t // Only the leading spaces should be convertedˇ»
5359 "}
5360 .replace("·", "")
5361 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5362 );
5363 cx.update_editor(|e, window, cx| {
5364 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5365 });
5366 cx.assert_editor_state(
5367 indoc! {"
5368 ·
5369 abc // No indentation
5370 abc // 1 space (< 3 so dont convert)
5371 abc // 2 spaces (< 3 so dont convert)
5372 «\tabc // 3 spaces (convert)ˇ»
5373 abc // 5 spaces (1 tab + 2 spaces)
5374 \t\t\tabc // Already tab indented
5375 \t abc // Tab followed by space
5376 «\tabc // Space followed by tab (should be consumed due to tab)ˇ»
5377 \t\t \tabc // Mixed indentation
5378 \t \t \t \tabc // Mixed indentation
5379 «\t\t\t
5380 \tabc \t // Only the leading spaces should be convertedˇ»
5381 "}
5382 .replace("·", "")
5383 .as_str(), // · used as placeholder to prevent format-on-save from removing whitespace
5384 );
5385
5386 // SINGLE SELECTION
5387 // Ln.1 "«" tests empty lines
5388 // Ln.11 tests just leading whitespace
5389 cx.set_state(indoc! {"
5390 «
5391 abc // No indentation
5392 abc // 1 space (< 3 so dont convert)
5393 abc // 2 spaces (< 3 so dont convert)
5394 abc // 3 spaces (convert)
5395 abc // 5 spaces (1 tab + 2 spaces)
5396 \t\t\tabc // Already tab indented
5397 \t abc // Tab followed by space
5398 \tabc // Space followed by tab (should be consumed due to tab)
5399 \t \t \t \tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5400 \t \t
5401 abc \t // Only the leading spaces should be convertedˇ»
5402 "});
5403 cx.update_editor(|e, window, cx| {
5404 e.convert_indentation_to_tabs(&ConvertIndentationToTabs, window, cx);
5405 });
5406 cx.assert_editor_state(indoc! {"
5407 «
5408 abc // No indentation
5409 abc // 1 space (< 3 so dont convert)
5410 abc // 2 spaces (< 3 so dont convert)
5411 \tabc // 3 spaces (convert)
5412 \t abc // 5 spaces (1 tab + 2 spaces)
5413 \t\t\tabc // Already tab indented
5414 \t abc // Tab followed by space
5415 \tabc // Space followed by tab (should be consumed due to tab)
5416 \t\t\t\t\tabc // Mixed indentation (first 3 spaces are consumed, the others are converted)
5417 \t\t\t
5418 \tabc \t // Only the leading spaces should be convertedˇ»
5419 "});
5420}
5421
5422#[gpui::test]
5423async fn test_toggle_case(cx: &mut TestAppContext) {
5424 init_test(cx, |_| {});
5425
5426 let mut cx = EditorTestContext::new(cx).await;
5427
5428 // If all lower case -> upper case
5429 cx.set_state(indoc! {"
5430 «hello worldˇ»
5431 "});
5432 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5433 cx.assert_editor_state(indoc! {"
5434 «HELLO WORLDˇ»
5435 "});
5436
5437 // If all upper case -> lower case
5438 cx.set_state(indoc! {"
5439 «HELLO WORLDˇ»
5440 "});
5441 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5442 cx.assert_editor_state(indoc! {"
5443 «hello worldˇ»
5444 "});
5445
5446 // If any upper case characters are identified -> lower case
5447 // This matches JetBrains IDEs
5448 cx.set_state(indoc! {"
5449 «hEllo worldˇ»
5450 "});
5451 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
5452 cx.assert_editor_state(indoc! {"
5453 «hello worldˇ»
5454 "});
5455}
5456
5457#[gpui::test]
5458async fn test_convert_to_sentence_case(cx: &mut TestAppContext) {
5459 init_test(cx, |_| {});
5460
5461 let mut cx = EditorTestContext::new(cx).await;
5462
5463 cx.set_state(indoc! {"
5464 «implement-windows-supportˇ»
5465 "});
5466 cx.update_editor(|e, window, cx| {
5467 e.convert_to_sentence_case(&ConvertToSentenceCase, window, cx)
5468 });
5469 cx.assert_editor_state(indoc! {"
5470 «Implement windows supportˇ»
5471 "});
5472}
5473
5474#[gpui::test]
5475async fn test_manipulate_text(cx: &mut TestAppContext) {
5476 init_test(cx, |_| {});
5477
5478 let mut cx = EditorTestContext::new(cx).await;
5479
5480 // Test convert_to_upper_case()
5481 cx.set_state(indoc! {"
5482 «hello worldˇ»
5483 "});
5484 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5485 cx.assert_editor_state(indoc! {"
5486 «HELLO WORLDˇ»
5487 "});
5488
5489 // Test convert_to_lower_case()
5490 cx.set_state(indoc! {"
5491 «HELLO WORLDˇ»
5492 "});
5493 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
5494 cx.assert_editor_state(indoc! {"
5495 «hello worldˇ»
5496 "});
5497
5498 // Test multiple line, single selection case
5499 cx.set_state(indoc! {"
5500 «The quick brown
5501 fox jumps over
5502 the lazy dogˇ»
5503 "});
5504 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
5505 cx.assert_editor_state(indoc! {"
5506 «The Quick Brown
5507 Fox Jumps Over
5508 The Lazy Dogˇ»
5509 "});
5510
5511 // Test multiple line, single selection case
5512 cx.set_state(indoc! {"
5513 «The quick brown
5514 fox jumps over
5515 the lazy dogˇ»
5516 "});
5517 cx.update_editor(|e, window, cx| {
5518 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
5519 });
5520 cx.assert_editor_state(indoc! {"
5521 «TheQuickBrown
5522 FoxJumpsOver
5523 TheLazyDogˇ»
5524 "});
5525
5526 // From here on out, test more complex cases of manipulate_text()
5527
5528 // Test no selection case - should affect words cursors are in
5529 // Cursor at beginning, middle, and end of word
5530 cx.set_state(indoc! {"
5531 ˇhello big beauˇtiful worldˇ
5532 "});
5533 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5534 cx.assert_editor_state(indoc! {"
5535 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
5536 "});
5537
5538 // Test multiple selections on a single line and across multiple lines
5539 cx.set_state(indoc! {"
5540 «Theˇ» quick «brown
5541 foxˇ» jumps «overˇ»
5542 the «lazyˇ» dog
5543 "});
5544 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5545 cx.assert_editor_state(indoc! {"
5546 «THEˇ» quick «BROWN
5547 FOXˇ» jumps «OVERˇ»
5548 the «LAZYˇ» dog
5549 "});
5550
5551 // Test case where text length grows
5552 cx.set_state(indoc! {"
5553 «tschüߡ»
5554 "});
5555 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5556 cx.assert_editor_state(indoc! {"
5557 «TSCHÜSSˇ»
5558 "});
5559
5560 // Test to make sure we don't crash when text shrinks
5561 cx.set_state(indoc! {"
5562 aaa_bbbˇ
5563 "});
5564 cx.update_editor(|e, window, cx| {
5565 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5566 });
5567 cx.assert_editor_state(indoc! {"
5568 «aaaBbbˇ»
5569 "});
5570
5571 // Test to make sure we all aware of the fact that each word can grow and shrink
5572 // Final selections should be aware of this fact
5573 cx.set_state(indoc! {"
5574 aaa_bˇbb bbˇb_ccc ˇccc_ddd
5575 "});
5576 cx.update_editor(|e, window, cx| {
5577 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
5578 });
5579 cx.assert_editor_state(indoc! {"
5580 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
5581 "});
5582
5583 cx.set_state(indoc! {"
5584 «hElLo, WoRld!ˇ»
5585 "});
5586 cx.update_editor(|e, window, cx| {
5587 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
5588 });
5589 cx.assert_editor_state(indoc! {"
5590 «HeLlO, wOrLD!ˇ»
5591 "});
5592
5593 // Test selections with `line_mode() = true`.
5594 cx.update_editor(|editor, _window, _cx| editor.selections.set_line_mode(true));
5595 cx.set_state(indoc! {"
5596 «The quick brown
5597 fox jumps over
5598 tˇ»he lazy dog
5599 "});
5600 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
5601 cx.assert_editor_state(indoc! {"
5602 «THE QUICK BROWN
5603 FOX JUMPS OVER
5604 THE LAZY DOGˇ»
5605 "});
5606}
5607
5608#[gpui::test]
5609fn test_duplicate_line(cx: &mut TestAppContext) {
5610 init_test(cx, |_| {});
5611
5612 let editor = cx.add_window(|window, cx| {
5613 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5614 build_editor(buffer, window, cx)
5615 });
5616 _ = editor.update(cx, |editor, window, cx| {
5617 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5618 s.select_display_ranges([
5619 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5620 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5621 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5622 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5623 ])
5624 });
5625 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5626 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5627 assert_eq!(
5628 display_ranges(editor, cx),
5629 vec![
5630 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
5631 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
5632 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5633 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
5634 ]
5635 );
5636 });
5637
5638 let editor = cx.add_window(|window, cx| {
5639 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5640 build_editor(buffer, window, cx)
5641 });
5642 _ = editor.update(cx, |editor, window, cx| {
5643 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5644 s.select_display_ranges([
5645 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5646 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5647 ])
5648 });
5649 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
5650 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5651 assert_eq!(
5652 display_ranges(editor, cx),
5653 vec![
5654 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
5655 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
5656 ]
5657 );
5658 });
5659
5660 // With `duplicate_line_up` the selections move to the duplicated lines,
5661 // which are inserted above the original lines
5662 let editor = cx.add_window(|window, cx| {
5663 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5664 build_editor(buffer, window, cx)
5665 });
5666 _ = editor.update(cx, |editor, window, cx| {
5667 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5668 s.select_display_ranges([
5669 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5670 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5671 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5672 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
5673 ])
5674 });
5675 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5676 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
5677 assert_eq!(
5678 display_ranges(editor, cx),
5679 vec![
5680 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5681 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5682 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
5683 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0),
5684 ]
5685 );
5686 });
5687
5688 let editor = cx.add_window(|window, cx| {
5689 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5690 build_editor(buffer, window, cx)
5691 });
5692 _ = editor.update(cx, |editor, window, cx| {
5693 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5694 s.select_display_ranges([
5695 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5696 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5697 ])
5698 });
5699 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
5700 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
5701 assert_eq!(
5702 display_ranges(editor, cx),
5703 vec![
5704 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5705 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5706 ]
5707 );
5708 });
5709
5710 let editor = cx.add_window(|window, cx| {
5711 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
5712 build_editor(buffer, window, cx)
5713 });
5714 _ = editor.update(cx, |editor, window, cx| {
5715 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5716 s.select_display_ranges([
5717 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5718 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
5719 ])
5720 });
5721 editor.duplicate_selection(&DuplicateSelection, window, cx);
5722 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
5723 assert_eq!(
5724 display_ranges(editor, cx),
5725 vec![
5726 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
5727 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
5728 ]
5729 );
5730 });
5731}
5732
5733#[gpui::test]
5734fn test_move_line_up_down(cx: &mut TestAppContext) {
5735 init_test(cx, |_| {});
5736
5737 let editor = cx.add_window(|window, cx| {
5738 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5739 build_editor(buffer, window, cx)
5740 });
5741 _ = editor.update(cx, |editor, window, cx| {
5742 editor.fold_creases(
5743 vec![
5744 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5745 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5746 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5747 ],
5748 true,
5749 window,
5750 cx,
5751 );
5752 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5753 s.select_display_ranges([
5754 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5755 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5756 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5757 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
5758 ])
5759 });
5760 assert_eq!(
5761 editor.display_text(cx),
5762 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
5763 );
5764
5765 editor.move_line_up(&MoveLineUp, window, cx);
5766 assert_eq!(
5767 editor.display_text(cx),
5768 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
5769 );
5770 assert_eq!(
5771 display_ranges(editor, cx),
5772 vec![
5773 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
5774 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5775 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5776 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5777 ]
5778 );
5779 });
5780
5781 _ = editor.update(cx, |editor, window, cx| {
5782 editor.move_line_down(&MoveLineDown, window, cx);
5783 assert_eq!(
5784 editor.display_text(cx),
5785 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
5786 );
5787 assert_eq!(
5788 display_ranges(editor, cx),
5789 vec![
5790 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5791 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5792 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5793 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5794 ]
5795 );
5796 });
5797
5798 _ = editor.update(cx, |editor, window, cx| {
5799 editor.move_line_down(&MoveLineDown, window, cx);
5800 assert_eq!(
5801 editor.display_text(cx),
5802 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
5803 );
5804 assert_eq!(
5805 display_ranges(editor, cx),
5806 vec![
5807 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5808 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
5809 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
5810 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
5811 ]
5812 );
5813 });
5814
5815 _ = editor.update(cx, |editor, window, cx| {
5816 editor.move_line_up(&MoveLineUp, window, cx);
5817 assert_eq!(
5818 editor.display_text(cx),
5819 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
5820 );
5821 assert_eq!(
5822 display_ranges(editor, cx),
5823 vec![
5824 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
5825 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
5826 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
5827 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
5828 ]
5829 );
5830 });
5831}
5832
5833#[gpui::test]
5834fn test_move_line_up_selection_at_end_of_fold(cx: &mut TestAppContext) {
5835 init_test(cx, |_| {});
5836 let editor = cx.add_window(|window, cx| {
5837 let buffer = MultiBuffer::build_simple("\n\n\n\n\n\naaaa\nbbbb\ncccc", cx);
5838 build_editor(buffer, window, cx)
5839 });
5840 _ = editor.update(cx, |editor, window, cx| {
5841 editor.fold_creases(
5842 vec![Crease::simple(
5843 Point::new(6, 4)..Point::new(7, 4),
5844 FoldPlaceholder::test(),
5845 )],
5846 true,
5847 window,
5848 cx,
5849 );
5850 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5851 s.select_ranges([Point::new(7, 4)..Point::new(7, 4)])
5852 });
5853 assert_eq!(editor.display_text(cx), "\n\n\n\n\n\naaaa⋯\ncccc");
5854 editor.move_line_up(&MoveLineUp, window, cx);
5855 let buffer_text = editor.buffer.read(cx).snapshot(cx).text();
5856 assert_eq!(buffer_text, "\n\n\n\n\naaaa\nbbbb\n\ncccc");
5857 });
5858}
5859
5860#[gpui::test]
5861fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
5862 init_test(cx, |_| {});
5863
5864 let editor = cx.add_window(|window, cx| {
5865 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
5866 build_editor(buffer, window, cx)
5867 });
5868 _ = editor.update(cx, |editor, window, cx| {
5869 let snapshot = editor.buffer.read(cx).snapshot(cx);
5870 editor.insert_blocks(
5871 [BlockProperties {
5872 style: BlockStyle::Fixed,
5873 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
5874 height: Some(1),
5875 render: Arc::new(|_| div().into_any()),
5876 priority: 0,
5877 }],
5878 Some(Autoscroll::fit()),
5879 cx,
5880 );
5881 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5882 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
5883 });
5884 editor.move_line_down(&MoveLineDown, window, cx);
5885 });
5886}
5887
5888#[gpui::test]
5889async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
5890 init_test(cx, |_| {});
5891
5892 let mut cx = EditorTestContext::new(cx).await;
5893 cx.set_state(
5894 &"
5895 ˇzero
5896 one
5897 two
5898 three
5899 four
5900 five
5901 "
5902 .unindent(),
5903 );
5904
5905 // Create a four-line block that replaces three lines of text.
5906 cx.update_editor(|editor, window, cx| {
5907 let snapshot = editor.snapshot(window, cx);
5908 let snapshot = &snapshot.buffer_snapshot();
5909 let placement = BlockPlacement::Replace(
5910 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
5911 );
5912 editor.insert_blocks(
5913 [BlockProperties {
5914 placement,
5915 height: Some(4),
5916 style: BlockStyle::Sticky,
5917 render: Arc::new(|_| gpui::div().into_any_element()),
5918 priority: 0,
5919 }],
5920 None,
5921 cx,
5922 );
5923 });
5924
5925 // Move down so that the cursor touches the block.
5926 cx.update_editor(|editor, window, cx| {
5927 editor.move_down(&Default::default(), window, cx);
5928 });
5929 cx.assert_editor_state(
5930 &"
5931 zero
5932 «one
5933 two
5934 threeˇ»
5935 four
5936 five
5937 "
5938 .unindent(),
5939 );
5940
5941 // Move down past the block.
5942 cx.update_editor(|editor, window, cx| {
5943 editor.move_down(&Default::default(), window, cx);
5944 });
5945 cx.assert_editor_state(
5946 &"
5947 zero
5948 one
5949 two
5950 three
5951 ˇfour
5952 five
5953 "
5954 .unindent(),
5955 );
5956}
5957
5958#[gpui::test]
5959fn test_transpose(cx: &mut TestAppContext) {
5960 init_test(cx, |_| {});
5961
5962 _ = cx.add_window(|window, cx| {
5963 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
5964 editor.set_style(EditorStyle::default(), window, cx);
5965 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5966 s.select_ranges([1..1])
5967 });
5968 editor.transpose(&Default::default(), window, cx);
5969 assert_eq!(editor.text(cx), "bac");
5970 assert_eq!(
5971 editor.selections.ranges(&editor.display_snapshot(cx)),
5972 [2..2]
5973 );
5974
5975 editor.transpose(&Default::default(), window, cx);
5976 assert_eq!(editor.text(cx), "bca");
5977 assert_eq!(
5978 editor.selections.ranges(&editor.display_snapshot(cx)),
5979 [3..3]
5980 );
5981
5982 editor.transpose(&Default::default(), window, cx);
5983 assert_eq!(editor.text(cx), "bac");
5984 assert_eq!(
5985 editor.selections.ranges(&editor.display_snapshot(cx)),
5986 [3..3]
5987 );
5988
5989 editor
5990 });
5991
5992 _ = cx.add_window(|window, cx| {
5993 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
5994 editor.set_style(EditorStyle::default(), window, cx);
5995 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
5996 s.select_ranges([3..3])
5997 });
5998 editor.transpose(&Default::default(), window, cx);
5999 assert_eq!(editor.text(cx), "acb\nde");
6000 assert_eq!(
6001 editor.selections.ranges(&editor.display_snapshot(cx)),
6002 [3..3]
6003 );
6004
6005 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6006 s.select_ranges([4..4])
6007 });
6008 editor.transpose(&Default::default(), window, cx);
6009 assert_eq!(editor.text(cx), "acbd\ne");
6010 assert_eq!(
6011 editor.selections.ranges(&editor.display_snapshot(cx)),
6012 [5..5]
6013 );
6014
6015 editor.transpose(&Default::default(), window, cx);
6016 assert_eq!(editor.text(cx), "acbde\n");
6017 assert_eq!(
6018 editor.selections.ranges(&editor.display_snapshot(cx)),
6019 [6..6]
6020 );
6021
6022 editor.transpose(&Default::default(), window, cx);
6023 assert_eq!(editor.text(cx), "acbd\ne");
6024 assert_eq!(
6025 editor.selections.ranges(&editor.display_snapshot(cx)),
6026 [6..6]
6027 );
6028
6029 editor
6030 });
6031
6032 _ = cx.add_window(|window, cx| {
6033 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
6034 editor.set_style(EditorStyle::default(), window, cx);
6035 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6036 s.select_ranges([1..1, 2..2, 4..4])
6037 });
6038 editor.transpose(&Default::default(), window, cx);
6039 assert_eq!(editor.text(cx), "bacd\ne");
6040 assert_eq!(
6041 editor.selections.ranges(&editor.display_snapshot(cx)),
6042 [2..2, 3..3, 5..5]
6043 );
6044
6045 editor.transpose(&Default::default(), window, cx);
6046 assert_eq!(editor.text(cx), "bcade\n");
6047 assert_eq!(
6048 editor.selections.ranges(&editor.display_snapshot(cx)),
6049 [3..3, 4..4, 6..6]
6050 );
6051
6052 editor.transpose(&Default::default(), window, cx);
6053 assert_eq!(editor.text(cx), "bcda\ne");
6054 assert_eq!(
6055 editor.selections.ranges(&editor.display_snapshot(cx)),
6056 [4..4, 6..6]
6057 );
6058
6059 editor.transpose(&Default::default(), window, cx);
6060 assert_eq!(editor.text(cx), "bcade\n");
6061 assert_eq!(
6062 editor.selections.ranges(&editor.display_snapshot(cx)),
6063 [4..4, 6..6]
6064 );
6065
6066 editor.transpose(&Default::default(), window, cx);
6067 assert_eq!(editor.text(cx), "bcaed\n");
6068 assert_eq!(
6069 editor.selections.ranges(&editor.display_snapshot(cx)),
6070 [5..5, 6..6]
6071 );
6072
6073 editor
6074 });
6075
6076 _ = cx.add_window(|window, cx| {
6077 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
6078 editor.set_style(EditorStyle::default(), window, cx);
6079 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
6080 s.select_ranges([4..4])
6081 });
6082 editor.transpose(&Default::default(), window, cx);
6083 assert_eq!(editor.text(cx), "🏀🍐✋");
6084 assert_eq!(
6085 editor.selections.ranges(&editor.display_snapshot(cx)),
6086 [8..8]
6087 );
6088
6089 editor.transpose(&Default::default(), window, cx);
6090 assert_eq!(editor.text(cx), "🏀✋🍐");
6091 assert_eq!(
6092 editor.selections.ranges(&editor.display_snapshot(cx)),
6093 [11..11]
6094 );
6095
6096 editor.transpose(&Default::default(), window, cx);
6097 assert_eq!(editor.text(cx), "🏀🍐✋");
6098 assert_eq!(
6099 editor.selections.ranges(&editor.display_snapshot(cx)),
6100 [11..11]
6101 );
6102
6103 editor
6104 });
6105}
6106
6107#[gpui::test]
6108async fn test_rewrap(cx: &mut TestAppContext) {
6109 init_test(cx, |settings| {
6110 settings.languages.0.extend([
6111 (
6112 "Markdown".into(),
6113 LanguageSettingsContent {
6114 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6115 preferred_line_length: Some(40),
6116 ..Default::default()
6117 },
6118 ),
6119 (
6120 "Plain Text".into(),
6121 LanguageSettingsContent {
6122 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
6123 preferred_line_length: Some(40),
6124 ..Default::default()
6125 },
6126 ),
6127 (
6128 "C++".into(),
6129 LanguageSettingsContent {
6130 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6131 preferred_line_length: Some(40),
6132 ..Default::default()
6133 },
6134 ),
6135 (
6136 "Python".into(),
6137 LanguageSettingsContent {
6138 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6139 preferred_line_length: Some(40),
6140 ..Default::default()
6141 },
6142 ),
6143 (
6144 "Rust".into(),
6145 LanguageSettingsContent {
6146 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6147 preferred_line_length: Some(40),
6148 ..Default::default()
6149 },
6150 ),
6151 ])
6152 });
6153
6154 let mut cx = EditorTestContext::new(cx).await;
6155
6156 let cpp_language = Arc::new(Language::new(
6157 LanguageConfig {
6158 name: "C++".into(),
6159 line_comments: vec!["// ".into()],
6160 ..LanguageConfig::default()
6161 },
6162 None,
6163 ));
6164 let python_language = Arc::new(Language::new(
6165 LanguageConfig {
6166 name: "Python".into(),
6167 line_comments: vec!["# ".into()],
6168 ..LanguageConfig::default()
6169 },
6170 None,
6171 ));
6172 let markdown_language = Arc::new(Language::new(
6173 LanguageConfig {
6174 name: "Markdown".into(),
6175 rewrap_prefixes: vec![
6176 regex::Regex::new("\\d+\\.\\s+").unwrap(),
6177 regex::Regex::new("[-*+]\\s+").unwrap(),
6178 ],
6179 ..LanguageConfig::default()
6180 },
6181 None,
6182 ));
6183 let rust_language = Arc::new(
6184 Language::new(
6185 LanguageConfig {
6186 name: "Rust".into(),
6187 line_comments: vec!["// ".into(), "/// ".into()],
6188 ..LanguageConfig::default()
6189 },
6190 Some(tree_sitter_rust::LANGUAGE.into()),
6191 )
6192 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
6193 .unwrap(),
6194 );
6195
6196 let plaintext_language = Arc::new(Language::new(
6197 LanguageConfig {
6198 name: "Plain Text".into(),
6199 ..LanguageConfig::default()
6200 },
6201 None,
6202 ));
6203
6204 // Test basic rewrapping of a long line with a cursor
6205 assert_rewrap(
6206 indoc! {"
6207 // ˇThis is a long comment that needs to be wrapped.
6208 "},
6209 indoc! {"
6210 // ˇThis is a long comment that needs to
6211 // be wrapped.
6212 "},
6213 cpp_language.clone(),
6214 &mut cx,
6215 );
6216
6217 // Test rewrapping a full selection
6218 assert_rewrap(
6219 indoc! {"
6220 «// This selected long comment needs to be wrapped.ˇ»"
6221 },
6222 indoc! {"
6223 «// This selected long comment needs to
6224 // be wrapped.ˇ»"
6225 },
6226 cpp_language.clone(),
6227 &mut cx,
6228 );
6229
6230 // Test multiple cursors on different lines within the same paragraph are preserved after rewrapping
6231 assert_rewrap(
6232 indoc! {"
6233 // ˇThis is the first line.
6234 // Thisˇ is the second line.
6235 // This is the thirdˇ line, all part of one paragraph.
6236 "},
6237 indoc! {"
6238 // ˇThis is the first line. Thisˇ is the
6239 // second line. This is the thirdˇ line,
6240 // all part of one paragraph.
6241 "},
6242 cpp_language.clone(),
6243 &mut cx,
6244 );
6245
6246 // Test multiple cursors in different paragraphs trigger separate rewraps
6247 assert_rewrap(
6248 indoc! {"
6249 // ˇThis is the first paragraph, first line.
6250 // ˇThis is the first paragraph, second line.
6251
6252 // ˇThis is the second paragraph, first line.
6253 // ˇThis is the second paragraph, second line.
6254 "},
6255 indoc! {"
6256 // ˇThis is the first paragraph, first
6257 // line. ˇThis is the first paragraph,
6258 // second line.
6259
6260 // ˇThis is the second paragraph, first
6261 // line. ˇThis is the second paragraph,
6262 // second line.
6263 "},
6264 cpp_language.clone(),
6265 &mut cx,
6266 );
6267
6268 // Test that change in comment prefix (e.g., `//` to `///`) trigger seperate rewraps
6269 assert_rewrap(
6270 indoc! {"
6271 «// A regular long long comment to be wrapped.
6272 /// A documentation long comment to be wrapped.ˇ»
6273 "},
6274 indoc! {"
6275 «// A regular long long comment to be
6276 // wrapped.
6277 /// A documentation long comment to be
6278 /// wrapped.ˇ»
6279 "},
6280 rust_language.clone(),
6281 &mut cx,
6282 );
6283
6284 // Test that change in indentation level trigger seperate rewraps
6285 assert_rewrap(
6286 indoc! {"
6287 fn foo() {
6288 «// This is a long comment at the base indent.
6289 // This is a long comment at the next indent.ˇ»
6290 }
6291 "},
6292 indoc! {"
6293 fn foo() {
6294 «// This is a long comment at the
6295 // base indent.
6296 // This is a long comment at the
6297 // next indent.ˇ»
6298 }
6299 "},
6300 rust_language.clone(),
6301 &mut cx,
6302 );
6303
6304 // Test that different comment prefix characters (e.g., '#') are handled correctly
6305 assert_rewrap(
6306 indoc! {"
6307 # ˇThis is a long comment using a pound sign.
6308 "},
6309 indoc! {"
6310 # ˇThis is a long comment using a pound
6311 # sign.
6312 "},
6313 python_language,
6314 &mut cx,
6315 );
6316
6317 // Test rewrapping only affects comments, not code even when selected
6318 assert_rewrap(
6319 indoc! {"
6320 «/// This doc comment is long and should be wrapped.
6321 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6322 "},
6323 indoc! {"
6324 «/// This doc comment is long and should
6325 /// be wrapped.
6326 fn my_func(a: u32, b: u32, c: u32, d: u32, e: u32, f: u32) {}ˇ»
6327 "},
6328 rust_language.clone(),
6329 &mut cx,
6330 );
6331
6332 // Test that rewrapping works in Markdown documents where `allow_rewrap` is `Anywhere`
6333 assert_rewrap(
6334 indoc! {"
6335 # Header
6336
6337 A long long long line of markdown text to wrap.ˇ
6338 "},
6339 indoc! {"
6340 # Header
6341
6342 A long long long line of markdown text
6343 to wrap.ˇ
6344 "},
6345 markdown_language.clone(),
6346 &mut cx,
6347 );
6348
6349 // Test that rewrapping boundary works and preserves relative indent for Markdown documents
6350 assert_rewrap(
6351 indoc! {"
6352 «1. This is a numbered list item that is very long and needs to be wrapped properly.
6353 2. This is a numbered list item that is very long and needs to be wrapped properly.
6354 - This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
6355 "},
6356 indoc! {"
6357 «1. This is a numbered list item that is
6358 very long and needs to be wrapped
6359 properly.
6360 2. This is a numbered list item that is
6361 very long and needs to be wrapped
6362 properly.
6363 - This is an unordered list item that is
6364 also very long and should not merge
6365 with the numbered item.ˇ»
6366 "},
6367 markdown_language.clone(),
6368 &mut cx,
6369 );
6370
6371 // Test that rewrapping add indents for rewrapping boundary if not exists already.
6372 assert_rewrap(
6373 indoc! {"
6374 «1. This is a numbered list item that is
6375 very long and needs to be wrapped
6376 properly.
6377 2. This is a numbered list item that is
6378 very long and needs to be wrapped
6379 properly.
6380 - This is an unordered list item that is
6381 also very long and should not merge with
6382 the numbered item.ˇ»
6383 "},
6384 indoc! {"
6385 «1. This is a numbered list item that is
6386 very long and needs to be wrapped
6387 properly.
6388 2. This is a numbered list item that is
6389 very long and needs to be wrapped
6390 properly.
6391 - This is an unordered list item that is
6392 also very long and should not merge
6393 with the numbered item.ˇ»
6394 "},
6395 markdown_language.clone(),
6396 &mut cx,
6397 );
6398
6399 // Test that rewrapping maintain indents even when they already exists.
6400 assert_rewrap(
6401 indoc! {"
6402 «1. This is a numbered list
6403 item that is very long and needs to be wrapped properly.
6404 2. This is a numbered list
6405 item that is very long and needs to be wrapped properly.
6406 - This is an unordered list item that is also very long and
6407 should not merge with the numbered item.ˇ»
6408 "},
6409 indoc! {"
6410 «1. This is a numbered list item that is
6411 very long and needs to be wrapped
6412 properly.
6413 2. This is a numbered list item that is
6414 very long and needs to be wrapped
6415 properly.
6416 - This is an unordered list item that is
6417 also very long and should not merge
6418 with the numbered item.ˇ»
6419 "},
6420 markdown_language,
6421 &mut cx,
6422 );
6423
6424 // Test that rewrapping works in plain text where `allow_rewrap` is `Anywhere`
6425 assert_rewrap(
6426 indoc! {"
6427 ˇThis is a very long line of plain text that will be wrapped.
6428 "},
6429 indoc! {"
6430 ˇThis is a very long line of plain text
6431 that will be wrapped.
6432 "},
6433 plaintext_language.clone(),
6434 &mut cx,
6435 );
6436
6437 // Test that non-commented code acts as a paragraph boundary within a selection
6438 assert_rewrap(
6439 indoc! {"
6440 «// This is the first long comment block to be wrapped.
6441 fn my_func(a: u32);
6442 // This is the second long comment block to be wrapped.ˇ»
6443 "},
6444 indoc! {"
6445 «// This is the first long comment block
6446 // to be wrapped.
6447 fn my_func(a: u32);
6448 // This is the second long comment block
6449 // to be wrapped.ˇ»
6450 "},
6451 rust_language,
6452 &mut cx,
6453 );
6454
6455 // Test rewrapping multiple selections, including ones with blank lines or tabs
6456 assert_rewrap(
6457 indoc! {"
6458 «ˇThis is a very long line that will be wrapped.
6459
6460 This is another paragraph in the same selection.»
6461
6462 «\tThis is a very long indented line that will be wrapped.ˇ»
6463 "},
6464 indoc! {"
6465 «ˇThis is a very long line that will be
6466 wrapped.
6467
6468 This is another paragraph in the same
6469 selection.»
6470
6471 «\tThis is a very long indented line
6472 \tthat will be wrapped.ˇ»
6473 "},
6474 plaintext_language,
6475 &mut cx,
6476 );
6477
6478 // Test that an empty comment line acts as a paragraph boundary
6479 assert_rewrap(
6480 indoc! {"
6481 // ˇThis is a long comment that will be wrapped.
6482 //
6483 // And this is another long comment that will also be wrapped.ˇ
6484 "},
6485 indoc! {"
6486 // ˇThis is a long comment that will be
6487 // wrapped.
6488 //
6489 // And this is another long comment that
6490 // will also be wrapped.ˇ
6491 "},
6492 cpp_language,
6493 &mut cx,
6494 );
6495
6496 #[track_caller]
6497 fn assert_rewrap(
6498 unwrapped_text: &str,
6499 wrapped_text: &str,
6500 language: Arc<Language>,
6501 cx: &mut EditorTestContext,
6502 ) {
6503 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6504 cx.set_state(unwrapped_text);
6505 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6506 cx.assert_editor_state(wrapped_text);
6507 }
6508}
6509
6510#[gpui::test]
6511async fn test_rewrap_block_comments(cx: &mut TestAppContext) {
6512 init_test(cx, |settings| {
6513 settings.languages.0.extend([(
6514 "Rust".into(),
6515 LanguageSettingsContent {
6516 allow_rewrap: Some(language_settings::RewrapBehavior::InComments),
6517 preferred_line_length: Some(40),
6518 ..Default::default()
6519 },
6520 )])
6521 });
6522
6523 let mut cx = EditorTestContext::new(cx).await;
6524
6525 let rust_lang = Arc::new(
6526 Language::new(
6527 LanguageConfig {
6528 name: "Rust".into(),
6529 line_comments: vec!["// ".into()],
6530 block_comment: Some(BlockCommentConfig {
6531 start: "/*".into(),
6532 end: "*/".into(),
6533 prefix: "* ".into(),
6534 tab_size: 1,
6535 }),
6536 documentation_comment: Some(BlockCommentConfig {
6537 start: "/**".into(),
6538 end: "*/".into(),
6539 prefix: "* ".into(),
6540 tab_size: 1,
6541 }),
6542
6543 ..LanguageConfig::default()
6544 },
6545 Some(tree_sitter_rust::LANGUAGE.into()),
6546 )
6547 .with_override_query("[(line_comment) (block_comment)] @comment.inclusive")
6548 .unwrap(),
6549 );
6550
6551 // regular block comment
6552 assert_rewrap(
6553 indoc! {"
6554 /*
6555 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6556 */
6557 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6558 "},
6559 indoc! {"
6560 /*
6561 *ˇ Lorem ipsum dolor sit amet,
6562 * consectetur adipiscing elit.
6563 */
6564 /*
6565 *ˇ Lorem ipsum dolor sit amet,
6566 * consectetur adipiscing elit.
6567 */
6568 "},
6569 rust_lang.clone(),
6570 &mut cx,
6571 );
6572
6573 // indent is respected
6574 assert_rewrap(
6575 indoc! {"
6576 {}
6577 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6578 "},
6579 indoc! {"
6580 {}
6581 /*
6582 *ˇ Lorem ipsum dolor sit amet,
6583 * consectetur adipiscing elit.
6584 */
6585 "},
6586 rust_lang.clone(),
6587 &mut cx,
6588 );
6589
6590 // short block comments with inline delimiters
6591 assert_rewrap(
6592 indoc! {"
6593 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6594 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6595 */
6596 /*
6597 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6598 "},
6599 indoc! {"
6600 /*
6601 *ˇ Lorem ipsum dolor sit amet,
6602 * consectetur adipiscing elit.
6603 */
6604 /*
6605 *ˇ Lorem ipsum dolor sit amet,
6606 * consectetur adipiscing elit.
6607 */
6608 /*
6609 *ˇ Lorem ipsum dolor sit amet,
6610 * consectetur adipiscing elit.
6611 */
6612 "},
6613 rust_lang.clone(),
6614 &mut cx,
6615 );
6616
6617 // multiline block comment with inline start/end delimiters
6618 assert_rewrap(
6619 indoc! {"
6620 /*ˇ Lorem ipsum dolor sit amet,
6621 * consectetur adipiscing elit. */
6622 "},
6623 indoc! {"
6624 /*
6625 *ˇ Lorem ipsum dolor sit amet,
6626 * consectetur adipiscing elit.
6627 */
6628 "},
6629 rust_lang.clone(),
6630 &mut cx,
6631 );
6632
6633 // block comment rewrap still respects paragraph bounds
6634 assert_rewrap(
6635 indoc! {"
6636 /*
6637 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6638 *
6639 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6640 */
6641 "},
6642 indoc! {"
6643 /*
6644 *ˇ Lorem ipsum dolor sit amet,
6645 * consectetur adipiscing elit.
6646 *
6647 * Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6648 */
6649 "},
6650 rust_lang.clone(),
6651 &mut cx,
6652 );
6653
6654 // documentation comments
6655 assert_rewrap(
6656 indoc! {"
6657 /**ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6658 /**
6659 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6660 */
6661 "},
6662 indoc! {"
6663 /**
6664 *ˇ Lorem ipsum dolor sit amet,
6665 * consectetur adipiscing elit.
6666 */
6667 /**
6668 *ˇ Lorem ipsum dolor sit amet,
6669 * consectetur adipiscing elit.
6670 */
6671 "},
6672 rust_lang.clone(),
6673 &mut cx,
6674 );
6675
6676 // different, adjacent comments
6677 assert_rewrap(
6678 indoc! {"
6679 /**
6680 *ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6681 */
6682 /*ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6683 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6684 "},
6685 indoc! {"
6686 /**
6687 *ˇ Lorem ipsum dolor sit amet,
6688 * consectetur adipiscing elit.
6689 */
6690 /*
6691 *ˇ Lorem ipsum dolor sit amet,
6692 * consectetur adipiscing elit.
6693 */
6694 //ˇ Lorem ipsum dolor sit amet,
6695 // consectetur adipiscing elit.
6696 "},
6697 rust_lang.clone(),
6698 &mut cx,
6699 );
6700
6701 // selection w/ single short block comment
6702 assert_rewrap(
6703 indoc! {"
6704 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6705 "},
6706 indoc! {"
6707 «/*
6708 * Lorem ipsum dolor sit amet,
6709 * consectetur adipiscing elit.
6710 */ˇ»
6711 "},
6712 rust_lang.clone(),
6713 &mut cx,
6714 );
6715
6716 // rewrapping a single comment w/ abutting comments
6717 assert_rewrap(
6718 indoc! {"
6719 /* ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. */
6720 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6721 "},
6722 indoc! {"
6723 /*
6724 * ˇLorem ipsum dolor sit amet,
6725 * consectetur adipiscing elit.
6726 */
6727 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6728 "},
6729 rust_lang.clone(),
6730 &mut cx,
6731 );
6732
6733 // selection w/ non-abutting short block comments
6734 assert_rewrap(
6735 indoc! {"
6736 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6737
6738 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6739 "},
6740 indoc! {"
6741 «/*
6742 * Lorem ipsum dolor sit amet,
6743 * consectetur adipiscing elit.
6744 */
6745
6746 /*
6747 * Lorem ipsum dolor sit amet,
6748 * consectetur adipiscing elit.
6749 */ˇ»
6750 "},
6751 rust_lang.clone(),
6752 &mut cx,
6753 );
6754
6755 // selection of multiline block comments
6756 assert_rewrap(
6757 indoc! {"
6758 «/* Lorem ipsum dolor sit amet,
6759 * consectetur adipiscing elit. */ˇ»
6760 "},
6761 indoc! {"
6762 «/*
6763 * Lorem ipsum dolor sit amet,
6764 * consectetur adipiscing elit.
6765 */ˇ»
6766 "},
6767 rust_lang.clone(),
6768 &mut cx,
6769 );
6770
6771 // partial selection of multiline block comments
6772 assert_rewrap(
6773 indoc! {"
6774 «/* Lorem ipsum dolor sit amet,ˇ»
6775 * consectetur adipiscing elit. */
6776 /* Lorem ipsum dolor sit amet,
6777 «* consectetur adipiscing elit. */ˇ»
6778 "},
6779 indoc! {"
6780 «/*
6781 * Lorem ipsum dolor sit amet,ˇ»
6782 * consectetur adipiscing elit. */
6783 /* Lorem ipsum dolor sit amet,
6784 «* consectetur adipiscing elit.
6785 */ˇ»
6786 "},
6787 rust_lang.clone(),
6788 &mut cx,
6789 );
6790
6791 // selection w/ abutting short block comments
6792 // TODO: should not be combined; should rewrap as 2 comments
6793 assert_rewrap(
6794 indoc! {"
6795 «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6796 /* Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6797 "},
6798 // desired behavior:
6799 // indoc! {"
6800 // «/*
6801 // * Lorem ipsum dolor sit amet,
6802 // * consectetur adipiscing elit.
6803 // */
6804 // /*
6805 // * Lorem ipsum dolor sit amet,
6806 // * consectetur adipiscing elit.
6807 // */ˇ»
6808 // "},
6809 // actual behaviour:
6810 indoc! {"
6811 «/*
6812 * Lorem ipsum dolor sit amet,
6813 * consectetur adipiscing elit. Lorem
6814 * ipsum dolor sit amet, consectetur
6815 * adipiscing elit.
6816 */ˇ»
6817 "},
6818 rust_lang.clone(),
6819 &mut cx,
6820 );
6821
6822 // TODO: same as above, but with delimiters on separate line
6823 // assert_rewrap(
6824 // indoc! {"
6825 // «/* Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6826 // */
6827 // /*
6828 // * Lorem ipsum dolor sit amet, consectetur adipiscing elit. */ˇ»
6829 // "},
6830 // // desired:
6831 // // indoc! {"
6832 // // «/*
6833 // // * Lorem ipsum dolor sit amet,
6834 // // * consectetur adipiscing elit.
6835 // // */
6836 // // /*
6837 // // * Lorem ipsum dolor sit amet,
6838 // // * consectetur adipiscing elit.
6839 // // */ˇ»
6840 // // "},
6841 // // actual: (but with trailing w/s on the empty lines)
6842 // indoc! {"
6843 // «/*
6844 // * Lorem ipsum dolor sit amet,
6845 // * consectetur adipiscing elit.
6846 // *
6847 // */
6848 // /*
6849 // *
6850 // * Lorem ipsum dolor sit amet,
6851 // * consectetur adipiscing elit.
6852 // */ˇ»
6853 // "},
6854 // rust_lang.clone(),
6855 // &mut cx,
6856 // );
6857
6858 // TODO these are unhandled edge cases; not correct, just documenting known issues
6859 assert_rewrap(
6860 indoc! {"
6861 /*
6862 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
6863 */
6864 /*
6865 //ˇ Lorem ipsum dolor sit amet, consectetur adipiscing elit. */
6866 /*ˇ Lorem ipsum dolor sit amet */ /* consectetur adipiscing elit. */
6867 "},
6868 // desired:
6869 // indoc! {"
6870 // /*
6871 // *ˇ Lorem ipsum dolor sit amet,
6872 // * consectetur adipiscing elit.
6873 // */
6874 // /*
6875 // *ˇ Lorem ipsum dolor sit amet,
6876 // * consectetur adipiscing elit.
6877 // */
6878 // /*
6879 // *ˇ Lorem ipsum dolor sit amet
6880 // */ /* consectetur adipiscing elit. */
6881 // "},
6882 // actual:
6883 indoc! {"
6884 /*
6885 //ˇ Lorem ipsum dolor sit amet,
6886 // consectetur adipiscing elit.
6887 */
6888 /*
6889 * //ˇ Lorem ipsum dolor sit amet,
6890 * consectetur adipiscing elit.
6891 */
6892 /*
6893 *ˇ Lorem ipsum dolor sit amet */ /*
6894 * consectetur adipiscing elit.
6895 */
6896 "},
6897 rust_lang,
6898 &mut cx,
6899 );
6900
6901 #[track_caller]
6902 fn assert_rewrap(
6903 unwrapped_text: &str,
6904 wrapped_text: &str,
6905 language: Arc<Language>,
6906 cx: &mut EditorTestContext,
6907 ) {
6908 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
6909 cx.set_state(unwrapped_text);
6910 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
6911 cx.assert_editor_state(wrapped_text);
6912 }
6913}
6914
6915#[gpui::test]
6916async fn test_hard_wrap(cx: &mut TestAppContext) {
6917 init_test(cx, |_| {});
6918 let mut cx = EditorTestContext::new(cx).await;
6919
6920 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
6921 cx.update_editor(|editor, _, cx| {
6922 editor.set_hard_wrap(Some(14), cx);
6923 });
6924
6925 cx.set_state(indoc!(
6926 "
6927 one two three ˇ
6928 "
6929 ));
6930 cx.simulate_input("four");
6931 cx.run_until_parked();
6932
6933 cx.assert_editor_state(indoc!(
6934 "
6935 one two three
6936 fourˇ
6937 "
6938 ));
6939
6940 cx.update_editor(|editor, window, cx| {
6941 editor.newline(&Default::default(), window, cx);
6942 });
6943 cx.run_until_parked();
6944 cx.assert_editor_state(indoc!(
6945 "
6946 one two three
6947 four
6948 ˇ
6949 "
6950 ));
6951
6952 cx.simulate_input("five");
6953 cx.run_until_parked();
6954 cx.assert_editor_state(indoc!(
6955 "
6956 one two three
6957 four
6958 fiveˇ
6959 "
6960 ));
6961
6962 cx.update_editor(|editor, window, cx| {
6963 editor.newline(&Default::default(), window, cx);
6964 });
6965 cx.run_until_parked();
6966 cx.simulate_input("# ");
6967 cx.run_until_parked();
6968 cx.assert_editor_state(indoc!(
6969 "
6970 one two three
6971 four
6972 five
6973 # ˇ
6974 "
6975 ));
6976
6977 cx.update_editor(|editor, window, cx| {
6978 editor.newline(&Default::default(), window, cx);
6979 });
6980 cx.run_until_parked();
6981 cx.assert_editor_state(indoc!(
6982 "
6983 one two three
6984 four
6985 five
6986 #\x20
6987 #ˇ
6988 "
6989 ));
6990
6991 cx.simulate_input(" 6");
6992 cx.run_until_parked();
6993 cx.assert_editor_state(indoc!(
6994 "
6995 one two three
6996 four
6997 five
6998 #
6999 # 6ˇ
7000 "
7001 ));
7002}
7003
7004#[gpui::test]
7005async fn test_cut_line_ends(cx: &mut TestAppContext) {
7006 init_test(cx, |_| {});
7007
7008 let mut cx = EditorTestContext::new(cx).await;
7009
7010 cx.set_state(indoc! {"The quick brownˇ"});
7011 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7012 cx.assert_editor_state(indoc! {"The quick brownˇ"});
7013
7014 cx.set_state(indoc! {"The emacs foxˇ"});
7015 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7016 cx.assert_editor_state(indoc! {"The emacs foxˇ"});
7017
7018 cx.set_state(indoc! {"
7019 The quick« brownˇ»
7020 fox jumps overˇ
7021 the lazy dog"});
7022 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7023 cx.assert_editor_state(indoc! {"
7024 The quickˇ
7025 ˇthe lazy dog"});
7026
7027 cx.set_state(indoc! {"
7028 The quick« brownˇ»
7029 fox jumps overˇ
7030 the lazy dog"});
7031 cx.update_editor(|e, window, cx| e.cut_to_end_of_line(&CutToEndOfLine::default(), window, cx));
7032 cx.assert_editor_state(indoc! {"
7033 The quickˇ
7034 fox jumps overˇthe lazy dog"});
7035
7036 cx.set_state(indoc! {"
7037 The quick« brownˇ»
7038 fox jumps overˇ
7039 the lazy dog"});
7040 cx.update_editor(|e, window, cx| {
7041 e.cut_to_end_of_line(
7042 &CutToEndOfLine {
7043 stop_at_newlines: true,
7044 },
7045 window,
7046 cx,
7047 )
7048 });
7049 cx.assert_editor_state(indoc! {"
7050 The quickˇ
7051 fox jumps overˇ
7052 the lazy dog"});
7053
7054 cx.set_state(indoc! {"
7055 The quick« brownˇ»
7056 fox jumps overˇ
7057 the lazy dog"});
7058 cx.update_editor(|e, window, cx| e.kill_ring_cut(&KillRingCut, window, cx));
7059 cx.assert_editor_state(indoc! {"
7060 The quickˇ
7061 fox jumps overˇthe lazy dog"});
7062}
7063
7064#[gpui::test]
7065async fn test_clipboard(cx: &mut TestAppContext) {
7066 init_test(cx, |_| {});
7067
7068 let mut cx = EditorTestContext::new(cx).await;
7069
7070 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
7071 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7072 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
7073
7074 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
7075 cx.set_state("two ˇfour ˇsix ˇ");
7076 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7077 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
7078
7079 // Paste again but with only two cursors. Since the number of cursors doesn't
7080 // match the number of slices in the clipboard, the entire clipboard text
7081 // is pasted at each cursor.
7082 cx.set_state("ˇtwo one✅ four three six five ˇ");
7083 cx.update_editor(|e, window, cx| {
7084 e.handle_input("( ", window, cx);
7085 e.paste(&Paste, window, cx);
7086 e.handle_input(") ", window, cx);
7087 });
7088 cx.assert_editor_state(
7089 &([
7090 "( one✅ ",
7091 "three ",
7092 "five ) ˇtwo one✅ four three six five ( one✅ ",
7093 "three ",
7094 "five ) ˇ",
7095 ]
7096 .join("\n")),
7097 );
7098
7099 // Cut with three selections, one of which is full-line.
7100 cx.set_state(indoc! {"
7101 1«2ˇ»3
7102 4ˇ567
7103 «8ˇ»9"});
7104 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7105 cx.assert_editor_state(indoc! {"
7106 1ˇ3
7107 ˇ9"});
7108
7109 // Paste with three selections, noticing how the copied selection that was full-line
7110 // gets inserted before the second cursor.
7111 cx.set_state(indoc! {"
7112 1ˇ3
7113 9ˇ
7114 «oˇ»ne"});
7115 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7116 cx.assert_editor_state(indoc! {"
7117 12ˇ3
7118 4567
7119 9ˇ
7120 8ˇne"});
7121
7122 // Copy with a single cursor only, which writes the whole line into the clipboard.
7123 cx.set_state(indoc! {"
7124 The quick brown
7125 fox juˇmps over
7126 the lazy dog"});
7127 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7128 assert_eq!(
7129 cx.read_from_clipboard()
7130 .and_then(|item| item.text().as_deref().map(str::to_string)),
7131 Some("fox jumps over\n".to_string())
7132 );
7133
7134 // Paste with three selections, noticing how the copied full-line selection is inserted
7135 // before the empty selections but replaces the selection that is non-empty.
7136 cx.set_state(indoc! {"
7137 Tˇhe quick brown
7138 «foˇ»x jumps over
7139 tˇhe lazy dog"});
7140 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7141 cx.assert_editor_state(indoc! {"
7142 fox jumps over
7143 Tˇhe quick brown
7144 fox jumps over
7145 ˇx jumps over
7146 fox jumps over
7147 tˇhe lazy dog"});
7148}
7149
7150#[gpui::test]
7151async fn test_copy_trim(cx: &mut TestAppContext) {
7152 init_test(cx, |_| {});
7153
7154 let mut cx = EditorTestContext::new(cx).await;
7155 cx.set_state(
7156 r#" «for selection in selections.iter() {
7157 let mut start = selection.start;
7158 let mut end = selection.end;
7159 let is_entire_line = selection.is_empty();
7160 if is_entire_line {
7161 start = Point::new(start.row, 0);ˇ»
7162 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7163 }
7164 "#,
7165 );
7166 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7167 assert_eq!(
7168 cx.read_from_clipboard()
7169 .and_then(|item| item.text().as_deref().map(str::to_string)),
7170 Some(
7171 "for selection in selections.iter() {
7172 let mut start = selection.start;
7173 let mut end = selection.end;
7174 let is_entire_line = selection.is_empty();
7175 if is_entire_line {
7176 start = Point::new(start.row, 0);"
7177 .to_string()
7178 ),
7179 "Regular copying preserves all indentation selected",
7180 );
7181 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7182 assert_eq!(
7183 cx.read_from_clipboard()
7184 .and_then(|item| item.text().as_deref().map(str::to_string)),
7185 Some(
7186 "for selection in selections.iter() {
7187let mut start = selection.start;
7188let mut end = selection.end;
7189let is_entire_line = selection.is_empty();
7190if is_entire_line {
7191 start = Point::new(start.row, 0);"
7192 .to_string()
7193 ),
7194 "Copying with stripping should strip all leading whitespaces"
7195 );
7196
7197 cx.set_state(
7198 r#" « for selection in selections.iter() {
7199 let mut start = selection.start;
7200 let mut end = selection.end;
7201 let is_entire_line = selection.is_empty();
7202 if is_entire_line {
7203 start = Point::new(start.row, 0);ˇ»
7204 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7205 }
7206 "#,
7207 );
7208 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7209 assert_eq!(
7210 cx.read_from_clipboard()
7211 .and_then(|item| item.text().as_deref().map(str::to_string)),
7212 Some(
7213 " for selection in selections.iter() {
7214 let mut start = selection.start;
7215 let mut end = selection.end;
7216 let is_entire_line = selection.is_empty();
7217 if is_entire_line {
7218 start = Point::new(start.row, 0);"
7219 .to_string()
7220 ),
7221 "Regular copying preserves all indentation selected",
7222 );
7223 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7224 assert_eq!(
7225 cx.read_from_clipboard()
7226 .and_then(|item| item.text().as_deref().map(str::to_string)),
7227 Some(
7228 "for selection in selections.iter() {
7229let mut start = selection.start;
7230let mut end = selection.end;
7231let is_entire_line = selection.is_empty();
7232if is_entire_line {
7233 start = Point::new(start.row, 0);"
7234 .to_string()
7235 ),
7236 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
7237 );
7238
7239 cx.set_state(
7240 r#" «ˇ for selection in selections.iter() {
7241 let mut start = selection.start;
7242 let mut end = selection.end;
7243 let is_entire_line = selection.is_empty();
7244 if is_entire_line {
7245 start = Point::new(start.row, 0);»
7246 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7247 }
7248 "#,
7249 );
7250 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7251 assert_eq!(
7252 cx.read_from_clipboard()
7253 .and_then(|item| item.text().as_deref().map(str::to_string)),
7254 Some(
7255 " for selection in selections.iter() {
7256 let mut start = selection.start;
7257 let mut end = selection.end;
7258 let is_entire_line = selection.is_empty();
7259 if is_entire_line {
7260 start = Point::new(start.row, 0);"
7261 .to_string()
7262 ),
7263 "Regular copying for reverse selection works the same",
7264 );
7265 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7266 assert_eq!(
7267 cx.read_from_clipboard()
7268 .and_then(|item| item.text().as_deref().map(str::to_string)),
7269 Some(
7270 "for selection in selections.iter() {
7271let mut start = selection.start;
7272let mut end = selection.end;
7273let is_entire_line = selection.is_empty();
7274if is_entire_line {
7275 start = Point::new(start.row, 0);"
7276 .to_string()
7277 ),
7278 "Copying with stripping for reverse selection works the same"
7279 );
7280
7281 cx.set_state(
7282 r#" for selection «in selections.iter() {
7283 let mut start = selection.start;
7284 let mut end = selection.end;
7285 let is_entire_line = selection.is_empty();
7286 if is_entire_line {
7287 start = Point::new(start.row, 0);ˇ»
7288 end = cmp::min(max_point, Point::new(end.row + 1, 0));
7289 }
7290 "#,
7291 );
7292 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7293 assert_eq!(
7294 cx.read_from_clipboard()
7295 .and_then(|item| item.text().as_deref().map(str::to_string)),
7296 Some(
7297 "in selections.iter() {
7298 let mut start = selection.start;
7299 let mut end = selection.end;
7300 let is_entire_line = selection.is_empty();
7301 if is_entire_line {
7302 start = Point::new(start.row, 0);"
7303 .to_string()
7304 ),
7305 "When selecting past the indent, the copying works as usual",
7306 );
7307 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7308 assert_eq!(
7309 cx.read_from_clipboard()
7310 .and_then(|item| item.text().as_deref().map(str::to_string)),
7311 Some(
7312 "in selections.iter() {
7313 let mut start = selection.start;
7314 let mut end = selection.end;
7315 let is_entire_line = selection.is_empty();
7316 if is_entire_line {
7317 start = Point::new(start.row, 0);"
7318 .to_string()
7319 ),
7320 "When selecting past the indent, nothing is trimmed"
7321 );
7322
7323 cx.set_state(
7324 r#" «for selection in selections.iter() {
7325 let mut start = selection.start;
7326
7327 let mut end = selection.end;
7328 let is_entire_line = selection.is_empty();
7329 if is_entire_line {
7330 start = Point::new(start.row, 0);
7331ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
7332 }
7333 "#,
7334 );
7335 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
7336 assert_eq!(
7337 cx.read_from_clipboard()
7338 .and_then(|item| item.text().as_deref().map(str::to_string)),
7339 Some(
7340 "for selection in selections.iter() {
7341let mut start = selection.start;
7342
7343let mut end = selection.end;
7344let is_entire_line = selection.is_empty();
7345if is_entire_line {
7346 start = Point::new(start.row, 0);
7347"
7348 .to_string()
7349 ),
7350 "Copying with stripping should ignore empty lines"
7351 );
7352}
7353
7354#[gpui::test]
7355async fn test_paste_multiline(cx: &mut TestAppContext) {
7356 init_test(cx, |_| {});
7357
7358 let mut cx = EditorTestContext::new(cx).await;
7359 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7360
7361 // Cut an indented block, without the leading whitespace.
7362 cx.set_state(indoc! {"
7363 const a: B = (
7364 c(),
7365 «d(
7366 e,
7367 f
7368 )ˇ»
7369 );
7370 "});
7371 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7372 cx.assert_editor_state(indoc! {"
7373 const a: B = (
7374 c(),
7375 ˇ
7376 );
7377 "});
7378
7379 // Paste it at the same position.
7380 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7381 cx.assert_editor_state(indoc! {"
7382 const a: B = (
7383 c(),
7384 d(
7385 e,
7386 f
7387 )ˇ
7388 );
7389 "});
7390
7391 // Paste it at a line with a lower indent level.
7392 cx.set_state(indoc! {"
7393 ˇ
7394 const a: B = (
7395 c(),
7396 );
7397 "});
7398 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7399 cx.assert_editor_state(indoc! {"
7400 d(
7401 e,
7402 f
7403 )ˇ
7404 const a: B = (
7405 c(),
7406 );
7407 "});
7408
7409 // Cut an indented block, with the leading whitespace.
7410 cx.set_state(indoc! {"
7411 const a: B = (
7412 c(),
7413 « d(
7414 e,
7415 f
7416 )
7417 ˇ»);
7418 "});
7419 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
7420 cx.assert_editor_state(indoc! {"
7421 const a: B = (
7422 c(),
7423 ˇ);
7424 "});
7425
7426 // Paste it at the same position.
7427 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7428 cx.assert_editor_state(indoc! {"
7429 const a: B = (
7430 c(),
7431 d(
7432 e,
7433 f
7434 )
7435 ˇ);
7436 "});
7437
7438 // Paste it at a line with a higher indent level.
7439 cx.set_state(indoc! {"
7440 const a: B = (
7441 c(),
7442 d(
7443 e,
7444 fˇ
7445 )
7446 );
7447 "});
7448 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7449 cx.assert_editor_state(indoc! {"
7450 const a: B = (
7451 c(),
7452 d(
7453 e,
7454 f d(
7455 e,
7456 f
7457 )
7458 ˇ
7459 )
7460 );
7461 "});
7462
7463 // Copy an indented block, starting mid-line
7464 cx.set_state(indoc! {"
7465 const a: B = (
7466 c(),
7467 somethin«g(
7468 e,
7469 f
7470 )ˇ»
7471 );
7472 "});
7473 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
7474
7475 // Paste it on a line with a lower indent level
7476 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
7477 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7478 cx.assert_editor_state(indoc! {"
7479 const a: B = (
7480 c(),
7481 something(
7482 e,
7483 f
7484 )
7485 );
7486 g(
7487 e,
7488 f
7489 )ˇ"});
7490}
7491
7492#[gpui::test]
7493async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
7494 init_test(cx, |_| {});
7495
7496 cx.write_to_clipboard(ClipboardItem::new_string(
7497 " d(\n e\n );\n".into(),
7498 ));
7499
7500 let mut cx = EditorTestContext::new(cx).await;
7501 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7502
7503 cx.set_state(indoc! {"
7504 fn a() {
7505 b();
7506 if c() {
7507 ˇ
7508 }
7509 }
7510 "});
7511
7512 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7513 cx.assert_editor_state(indoc! {"
7514 fn a() {
7515 b();
7516 if c() {
7517 d(
7518 e
7519 );
7520 ˇ
7521 }
7522 }
7523 "});
7524
7525 cx.set_state(indoc! {"
7526 fn a() {
7527 b();
7528 ˇ
7529 }
7530 "});
7531
7532 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
7533 cx.assert_editor_state(indoc! {"
7534 fn a() {
7535 b();
7536 d(
7537 e
7538 );
7539 ˇ
7540 }
7541 "});
7542}
7543
7544#[gpui::test]
7545fn test_select_all(cx: &mut TestAppContext) {
7546 init_test(cx, |_| {});
7547
7548 let editor = cx.add_window(|window, cx| {
7549 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
7550 build_editor(buffer, window, cx)
7551 });
7552 _ = editor.update(cx, |editor, window, cx| {
7553 editor.select_all(&SelectAll, window, cx);
7554 assert_eq!(
7555 display_ranges(editor, cx),
7556 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
7557 );
7558 });
7559}
7560
7561#[gpui::test]
7562fn test_select_line(cx: &mut TestAppContext) {
7563 init_test(cx, |_| {});
7564
7565 let editor = cx.add_window(|window, cx| {
7566 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
7567 build_editor(buffer, window, cx)
7568 });
7569 _ = editor.update(cx, |editor, window, cx| {
7570 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7571 s.select_display_ranges([
7572 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7573 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7574 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7575 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
7576 ])
7577 });
7578 editor.select_line(&SelectLine, window, cx);
7579 assert_eq!(
7580 display_ranges(editor, cx),
7581 vec![
7582 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
7583 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
7584 ]
7585 );
7586 });
7587
7588 _ = editor.update(cx, |editor, window, cx| {
7589 editor.select_line(&SelectLine, window, cx);
7590 assert_eq!(
7591 display_ranges(editor, cx),
7592 vec![
7593 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
7594 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
7595 ]
7596 );
7597 });
7598
7599 _ = editor.update(cx, |editor, window, cx| {
7600 editor.select_line(&SelectLine, window, cx);
7601 assert_eq!(
7602 display_ranges(editor, cx),
7603 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
7604 );
7605 });
7606}
7607
7608#[gpui::test]
7609async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
7610 init_test(cx, |_| {});
7611 let mut cx = EditorTestContext::new(cx).await;
7612
7613 #[track_caller]
7614 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
7615 cx.set_state(initial_state);
7616 cx.update_editor(|e, window, cx| {
7617 e.split_selection_into_lines(&Default::default(), window, cx)
7618 });
7619 cx.assert_editor_state(expected_state);
7620 }
7621
7622 // Selection starts and ends at the middle of lines, left-to-right
7623 test(
7624 &mut cx,
7625 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
7626 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7627 );
7628 // Same thing, right-to-left
7629 test(
7630 &mut cx,
7631 "aa\nb«b\ncc\ndd\neˇ»e\nff",
7632 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
7633 );
7634
7635 // Whole buffer, left-to-right, last line *doesn't* end with newline
7636 test(
7637 &mut cx,
7638 "«ˇaa\nbb\ncc\ndd\nee\nff»",
7639 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7640 );
7641 // Same thing, right-to-left
7642 test(
7643 &mut cx,
7644 "«aa\nbb\ncc\ndd\nee\nffˇ»",
7645 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
7646 );
7647
7648 // Whole buffer, left-to-right, last line ends with newline
7649 test(
7650 &mut cx,
7651 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
7652 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7653 );
7654 // Same thing, right-to-left
7655 test(
7656 &mut cx,
7657 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
7658 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
7659 );
7660
7661 // Starts at the end of a line, ends at the start of another
7662 test(
7663 &mut cx,
7664 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
7665 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
7666 );
7667}
7668
7669#[gpui::test]
7670async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
7671 init_test(cx, |_| {});
7672
7673 let editor = cx.add_window(|window, cx| {
7674 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
7675 build_editor(buffer, window, cx)
7676 });
7677
7678 // setup
7679 _ = editor.update(cx, |editor, window, cx| {
7680 editor.fold_creases(
7681 vec![
7682 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
7683 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
7684 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
7685 ],
7686 true,
7687 window,
7688 cx,
7689 );
7690 assert_eq!(
7691 editor.display_text(cx),
7692 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7693 );
7694 });
7695
7696 _ = editor.update(cx, |editor, window, cx| {
7697 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7698 s.select_display_ranges([
7699 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7700 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
7701 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
7702 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
7703 ])
7704 });
7705 editor.split_selection_into_lines(&Default::default(), window, cx);
7706 assert_eq!(
7707 editor.display_text(cx),
7708 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
7709 );
7710 });
7711 EditorTestContext::for_editor(editor, cx)
7712 .await
7713 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
7714
7715 _ = editor.update(cx, |editor, window, cx| {
7716 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
7717 s.select_display_ranges([
7718 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
7719 ])
7720 });
7721 editor.split_selection_into_lines(&Default::default(), window, cx);
7722 assert_eq!(
7723 editor.display_text(cx),
7724 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
7725 );
7726 assert_eq!(
7727 display_ranges(editor, cx),
7728 [
7729 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
7730 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
7731 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
7732 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
7733 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
7734 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
7735 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
7736 ]
7737 );
7738 });
7739 EditorTestContext::for_editor(editor, cx)
7740 .await
7741 .assert_editor_state(
7742 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
7743 );
7744}
7745
7746#[gpui::test]
7747async fn test_add_selection_above_below(cx: &mut TestAppContext) {
7748 init_test(cx, |_| {});
7749
7750 let mut cx = EditorTestContext::new(cx).await;
7751
7752 cx.set_state(indoc!(
7753 r#"abc
7754 defˇghi
7755
7756 jk
7757 nlmo
7758 "#
7759 ));
7760
7761 cx.update_editor(|editor, window, cx| {
7762 editor.add_selection_above(&Default::default(), window, cx);
7763 });
7764
7765 cx.assert_editor_state(indoc!(
7766 r#"abcˇ
7767 defˇghi
7768
7769 jk
7770 nlmo
7771 "#
7772 ));
7773
7774 cx.update_editor(|editor, window, cx| {
7775 editor.add_selection_above(&Default::default(), window, cx);
7776 });
7777
7778 cx.assert_editor_state(indoc!(
7779 r#"abcˇ
7780 defˇghi
7781
7782 jk
7783 nlmo
7784 "#
7785 ));
7786
7787 cx.update_editor(|editor, window, cx| {
7788 editor.add_selection_below(&Default::default(), window, cx);
7789 });
7790
7791 cx.assert_editor_state(indoc!(
7792 r#"abc
7793 defˇghi
7794
7795 jk
7796 nlmo
7797 "#
7798 ));
7799
7800 cx.update_editor(|editor, window, cx| {
7801 editor.undo_selection(&Default::default(), window, cx);
7802 });
7803
7804 cx.assert_editor_state(indoc!(
7805 r#"abcˇ
7806 defˇghi
7807
7808 jk
7809 nlmo
7810 "#
7811 ));
7812
7813 cx.update_editor(|editor, window, cx| {
7814 editor.redo_selection(&Default::default(), window, cx);
7815 });
7816
7817 cx.assert_editor_state(indoc!(
7818 r#"abc
7819 defˇghi
7820
7821 jk
7822 nlmo
7823 "#
7824 ));
7825
7826 cx.update_editor(|editor, window, cx| {
7827 editor.add_selection_below(&Default::default(), window, cx);
7828 });
7829
7830 cx.assert_editor_state(indoc!(
7831 r#"abc
7832 defˇghi
7833 ˇ
7834 jk
7835 nlmo
7836 "#
7837 ));
7838
7839 cx.update_editor(|editor, window, cx| {
7840 editor.add_selection_below(&Default::default(), window, cx);
7841 });
7842
7843 cx.assert_editor_state(indoc!(
7844 r#"abc
7845 defˇghi
7846 ˇ
7847 jkˇ
7848 nlmo
7849 "#
7850 ));
7851
7852 cx.update_editor(|editor, window, cx| {
7853 editor.add_selection_below(&Default::default(), window, cx);
7854 });
7855
7856 cx.assert_editor_state(indoc!(
7857 r#"abc
7858 defˇghi
7859 ˇ
7860 jkˇ
7861 nlmˇo
7862 "#
7863 ));
7864
7865 cx.update_editor(|editor, window, cx| {
7866 editor.add_selection_below(&Default::default(), window, cx);
7867 });
7868
7869 cx.assert_editor_state(indoc!(
7870 r#"abc
7871 defˇghi
7872 ˇ
7873 jkˇ
7874 nlmˇo
7875 ˇ"#
7876 ));
7877
7878 // change selections
7879 cx.set_state(indoc!(
7880 r#"abc
7881 def«ˇg»hi
7882
7883 jk
7884 nlmo
7885 "#
7886 ));
7887
7888 cx.update_editor(|editor, window, cx| {
7889 editor.add_selection_below(&Default::default(), window, cx);
7890 });
7891
7892 cx.assert_editor_state(indoc!(
7893 r#"abc
7894 def«ˇg»hi
7895
7896 jk
7897 nlm«ˇo»
7898 "#
7899 ));
7900
7901 cx.update_editor(|editor, window, cx| {
7902 editor.add_selection_below(&Default::default(), window, cx);
7903 });
7904
7905 cx.assert_editor_state(indoc!(
7906 r#"abc
7907 def«ˇg»hi
7908
7909 jk
7910 nlm«ˇo»
7911 "#
7912 ));
7913
7914 cx.update_editor(|editor, window, cx| {
7915 editor.add_selection_above(&Default::default(), window, cx);
7916 });
7917
7918 cx.assert_editor_state(indoc!(
7919 r#"abc
7920 def«ˇg»hi
7921
7922 jk
7923 nlmo
7924 "#
7925 ));
7926
7927 cx.update_editor(|editor, window, cx| {
7928 editor.add_selection_above(&Default::default(), window, cx);
7929 });
7930
7931 cx.assert_editor_state(indoc!(
7932 r#"abc
7933 def«ˇg»hi
7934
7935 jk
7936 nlmo
7937 "#
7938 ));
7939
7940 // Change selections again
7941 cx.set_state(indoc!(
7942 r#"a«bc
7943 defgˇ»hi
7944
7945 jk
7946 nlmo
7947 "#
7948 ));
7949
7950 cx.update_editor(|editor, window, cx| {
7951 editor.add_selection_below(&Default::default(), window, cx);
7952 });
7953
7954 cx.assert_editor_state(indoc!(
7955 r#"a«bcˇ»
7956 d«efgˇ»hi
7957
7958 j«kˇ»
7959 nlmo
7960 "#
7961 ));
7962
7963 cx.update_editor(|editor, window, cx| {
7964 editor.add_selection_below(&Default::default(), window, cx);
7965 });
7966 cx.assert_editor_state(indoc!(
7967 r#"a«bcˇ»
7968 d«efgˇ»hi
7969
7970 j«kˇ»
7971 n«lmoˇ»
7972 "#
7973 ));
7974 cx.update_editor(|editor, window, cx| {
7975 editor.add_selection_above(&Default::default(), window, cx);
7976 });
7977
7978 cx.assert_editor_state(indoc!(
7979 r#"a«bcˇ»
7980 d«efgˇ»hi
7981
7982 j«kˇ»
7983 nlmo
7984 "#
7985 ));
7986
7987 // Change selections again
7988 cx.set_state(indoc!(
7989 r#"abc
7990 d«ˇefghi
7991
7992 jk
7993 nlm»o
7994 "#
7995 ));
7996
7997 cx.update_editor(|editor, window, cx| {
7998 editor.add_selection_above(&Default::default(), window, cx);
7999 });
8000
8001 cx.assert_editor_state(indoc!(
8002 r#"a«ˇbc»
8003 d«ˇef»ghi
8004
8005 j«ˇk»
8006 n«ˇlm»o
8007 "#
8008 ));
8009
8010 cx.update_editor(|editor, window, cx| {
8011 editor.add_selection_below(&Default::default(), window, cx);
8012 });
8013
8014 cx.assert_editor_state(indoc!(
8015 r#"abc
8016 d«ˇef»ghi
8017
8018 j«ˇk»
8019 n«ˇlm»o
8020 "#
8021 ));
8022}
8023
8024#[gpui::test]
8025async fn test_add_selection_above_below_multi_cursor(cx: &mut TestAppContext) {
8026 init_test(cx, |_| {});
8027 let mut cx = EditorTestContext::new(cx).await;
8028
8029 cx.set_state(indoc!(
8030 r#"line onˇe
8031 liˇne two
8032 line three
8033 line four"#
8034 ));
8035
8036 cx.update_editor(|editor, window, cx| {
8037 editor.add_selection_below(&Default::default(), window, cx);
8038 });
8039
8040 // test multiple cursors expand in the same direction
8041 cx.assert_editor_state(indoc!(
8042 r#"line onˇe
8043 liˇne twˇo
8044 liˇne three
8045 line four"#
8046 ));
8047
8048 cx.update_editor(|editor, window, cx| {
8049 editor.add_selection_below(&Default::default(), window, cx);
8050 });
8051
8052 cx.update_editor(|editor, window, cx| {
8053 editor.add_selection_below(&Default::default(), window, cx);
8054 });
8055
8056 // test multiple cursors expand below overflow
8057 cx.assert_editor_state(indoc!(
8058 r#"line onˇe
8059 liˇne twˇo
8060 liˇne thˇree
8061 liˇne foˇur"#
8062 ));
8063
8064 cx.update_editor(|editor, window, cx| {
8065 editor.add_selection_above(&Default::default(), window, cx);
8066 });
8067
8068 // test multiple cursors retrieves back correctly
8069 cx.assert_editor_state(indoc!(
8070 r#"line onˇe
8071 liˇne twˇo
8072 liˇne thˇree
8073 line four"#
8074 ));
8075
8076 cx.update_editor(|editor, window, cx| {
8077 editor.add_selection_above(&Default::default(), window, cx);
8078 });
8079
8080 cx.update_editor(|editor, window, cx| {
8081 editor.add_selection_above(&Default::default(), window, cx);
8082 });
8083
8084 // test multiple cursor groups maintain independent direction - first expands up, second shrinks above
8085 cx.assert_editor_state(indoc!(
8086 r#"liˇne onˇe
8087 liˇne two
8088 line three
8089 line four"#
8090 ));
8091
8092 cx.update_editor(|editor, window, cx| {
8093 editor.undo_selection(&Default::default(), window, cx);
8094 });
8095
8096 // test undo
8097 cx.assert_editor_state(indoc!(
8098 r#"line onˇe
8099 liˇne twˇo
8100 line three
8101 line four"#
8102 ));
8103
8104 cx.update_editor(|editor, window, cx| {
8105 editor.redo_selection(&Default::default(), window, cx);
8106 });
8107
8108 // test redo
8109 cx.assert_editor_state(indoc!(
8110 r#"liˇne onˇe
8111 liˇne two
8112 line three
8113 line four"#
8114 ));
8115
8116 cx.set_state(indoc!(
8117 r#"abcd
8118 ef«ghˇ»
8119 ijkl
8120 «mˇ»nop"#
8121 ));
8122
8123 cx.update_editor(|editor, window, cx| {
8124 editor.add_selection_above(&Default::default(), window, cx);
8125 });
8126
8127 // test multiple selections expand in the same direction
8128 cx.assert_editor_state(indoc!(
8129 r#"ab«cdˇ»
8130 ef«ghˇ»
8131 «iˇ»jkl
8132 «mˇ»nop"#
8133 ));
8134
8135 cx.update_editor(|editor, window, cx| {
8136 editor.add_selection_above(&Default::default(), window, cx);
8137 });
8138
8139 // test multiple selection upward overflow
8140 cx.assert_editor_state(indoc!(
8141 r#"ab«cdˇ»
8142 «eˇ»f«ghˇ»
8143 «iˇ»jkl
8144 «mˇ»nop"#
8145 ));
8146
8147 cx.update_editor(|editor, window, cx| {
8148 editor.add_selection_below(&Default::default(), window, cx);
8149 });
8150
8151 // test multiple selection retrieves back correctly
8152 cx.assert_editor_state(indoc!(
8153 r#"abcd
8154 ef«ghˇ»
8155 «iˇ»jkl
8156 «mˇ»nop"#
8157 ));
8158
8159 cx.update_editor(|editor, window, cx| {
8160 editor.add_selection_below(&Default::default(), window, cx);
8161 });
8162
8163 // test multiple cursor groups maintain independent direction - first shrinks down, second expands below
8164 cx.assert_editor_state(indoc!(
8165 r#"abcd
8166 ef«ghˇ»
8167 ij«klˇ»
8168 «mˇ»nop"#
8169 ));
8170
8171 cx.update_editor(|editor, window, cx| {
8172 editor.undo_selection(&Default::default(), window, cx);
8173 });
8174
8175 // test undo
8176 cx.assert_editor_state(indoc!(
8177 r#"abcd
8178 ef«ghˇ»
8179 «iˇ»jkl
8180 «mˇ»nop"#
8181 ));
8182
8183 cx.update_editor(|editor, window, cx| {
8184 editor.redo_selection(&Default::default(), window, cx);
8185 });
8186
8187 // test redo
8188 cx.assert_editor_state(indoc!(
8189 r#"abcd
8190 ef«ghˇ»
8191 ij«klˇ»
8192 «mˇ»nop"#
8193 ));
8194}
8195
8196#[gpui::test]
8197async fn test_add_selection_above_below_multi_cursor_existing_state(cx: &mut TestAppContext) {
8198 init_test(cx, |_| {});
8199 let mut cx = EditorTestContext::new(cx).await;
8200
8201 cx.set_state(indoc!(
8202 r#"line onˇe
8203 liˇne two
8204 line three
8205 line four"#
8206 ));
8207
8208 cx.update_editor(|editor, window, cx| {
8209 editor.add_selection_below(&Default::default(), window, cx);
8210 editor.add_selection_below(&Default::default(), window, cx);
8211 editor.add_selection_below(&Default::default(), window, cx);
8212 });
8213
8214 // initial state with two multi cursor groups
8215 cx.assert_editor_state(indoc!(
8216 r#"line onˇe
8217 liˇne twˇo
8218 liˇne thˇree
8219 liˇne foˇur"#
8220 ));
8221
8222 // add single cursor in middle - simulate opt click
8223 cx.update_editor(|editor, window, cx| {
8224 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 4);
8225 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8226 editor.end_selection(window, cx);
8227 });
8228
8229 cx.assert_editor_state(indoc!(
8230 r#"line onˇe
8231 liˇne twˇo
8232 liˇneˇ thˇree
8233 liˇne foˇur"#
8234 ));
8235
8236 cx.update_editor(|editor, window, cx| {
8237 editor.add_selection_above(&Default::default(), window, cx);
8238 });
8239
8240 // test new added selection expands above and existing selection shrinks
8241 cx.assert_editor_state(indoc!(
8242 r#"line onˇe
8243 liˇneˇ twˇo
8244 liˇneˇ thˇree
8245 line four"#
8246 ));
8247
8248 cx.update_editor(|editor, window, cx| {
8249 editor.add_selection_above(&Default::default(), window, cx);
8250 });
8251
8252 // test new added selection expands above and existing selection shrinks
8253 cx.assert_editor_state(indoc!(
8254 r#"lineˇ onˇe
8255 liˇneˇ twˇo
8256 lineˇ three
8257 line four"#
8258 ));
8259
8260 // intial state with two selection groups
8261 cx.set_state(indoc!(
8262 r#"abcd
8263 ef«ghˇ»
8264 ijkl
8265 «mˇ»nop"#
8266 ));
8267
8268 cx.update_editor(|editor, window, cx| {
8269 editor.add_selection_above(&Default::default(), window, cx);
8270 editor.add_selection_above(&Default::default(), window, cx);
8271 });
8272
8273 cx.assert_editor_state(indoc!(
8274 r#"ab«cdˇ»
8275 «eˇ»f«ghˇ»
8276 «iˇ»jkl
8277 «mˇ»nop"#
8278 ));
8279
8280 // add single selection in middle - simulate opt drag
8281 cx.update_editor(|editor, window, cx| {
8282 let new_cursor_point = DisplayPoint::new(DisplayRow(2), 3);
8283 editor.begin_selection(new_cursor_point, true, 1, window, cx);
8284 editor.update_selection(
8285 DisplayPoint::new(DisplayRow(2), 4),
8286 0,
8287 gpui::Point::<f32>::default(),
8288 window,
8289 cx,
8290 );
8291 editor.end_selection(window, cx);
8292 });
8293
8294 cx.assert_editor_state(indoc!(
8295 r#"ab«cdˇ»
8296 «eˇ»f«ghˇ»
8297 «iˇ»jk«lˇ»
8298 «mˇ»nop"#
8299 ));
8300
8301 cx.update_editor(|editor, window, cx| {
8302 editor.add_selection_below(&Default::default(), window, cx);
8303 });
8304
8305 // test new added selection expands below, others shrinks from above
8306 cx.assert_editor_state(indoc!(
8307 r#"abcd
8308 ef«ghˇ»
8309 «iˇ»jk«lˇ»
8310 «mˇ»no«pˇ»"#
8311 ));
8312}
8313
8314#[gpui::test]
8315async fn test_select_next(cx: &mut TestAppContext) {
8316 init_test(cx, |_| {});
8317
8318 let mut cx = EditorTestContext::new(cx).await;
8319 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8320
8321 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8322 .unwrap();
8323 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8324
8325 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8326 .unwrap();
8327 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8328
8329 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8330 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8331
8332 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8333 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
8334
8335 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8336 .unwrap();
8337 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8338
8339 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8340 .unwrap();
8341 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8342
8343 // Test selection direction should be preserved
8344 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8345
8346 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8347 .unwrap();
8348 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
8349}
8350
8351#[gpui::test]
8352async fn test_select_all_matches(cx: &mut TestAppContext) {
8353 init_test(cx, |_| {});
8354
8355 let mut cx = EditorTestContext::new(cx).await;
8356
8357 // Test caret-only selections
8358 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8359 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8360 .unwrap();
8361 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8362
8363 // Test left-to-right selections
8364 cx.set_state("abc\n«abcˇ»\nabc");
8365 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8366 .unwrap();
8367 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
8368
8369 // Test right-to-left selections
8370 cx.set_state("abc\n«ˇabc»\nabc");
8371 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8372 .unwrap();
8373 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
8374
8375 // Test selecting whitespace with caret selection
8376 cx.set_state("abc\nˇ abc\nabc");
8377 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8378 .unwrap();
8379 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8380
8381 // Test selecting whitespace with left-to-right selection
8382 cx.set_state("abc\n«ˇ »abc\nabc");
8383 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8384 .unwrap();
8385 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
8386
8387 // Test no matches with right-to-left selection
8388 cx.set_state("abc\n« ˇ»abc\nabc");
8389 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8390 .unwrap();
8391 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
8392
8393 // Test with a single word and clip_at_line_ends=true (#29823)
8394 cx.set_state("aˇbc");
8395 cx.update_editor(|e, window, cx| {
8396 e.set_clip_at_line_ends(true, cx);
8397 e.select_all_matches(&SelectAllMatches, window, cx).unwrap();
8398 e.set_clip_at_line_ends(false, cx);
8399 });
8400 cx.assert_editor_state("«abcˇ»");
8401}
8402
8403#[gpui::test]
8404async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
8405 init_test(cx, |_| {});
8406
8407 let mut cx = EditorTestContext::new(cx).await;
8408
8409 let large_body_1 = "\nd".repeat(200);
8410 let large_body_2 = "\ne".repeat(200);
8411
8412 cx.set_state(&format!(
8413 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
8414 ));
8415 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
8416 let scroll_position = editor.scroll_position(cx);
8417 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
8418 scroll_position
8419 });
8420
8421 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
8422 .unwrap();
8423 cx.assert_editor_state(&format!(
8424 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
8425 ));
8426 let scroll_position_after_selection =
8427 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
8428 assert_eq!(
8429 initial_scroll_position, scroll_position_after_selection,
8430 "Scroll position should not change after selecting all matches"
8431 );
8432}
8433
8434#[gpui::test]
8435async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
8436 init_test(cx, |_| {});
8437
8438 let mut cx = EditorLspTestContext::new_rust(
8439 lsp::ServerCapabilities {
8440 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8441 ..Default::default()
8442 },
8443 cx,
8444 )
8445 .await;
8446
8447 cx.set_state(indoc! {"
8448 line 1
8449 line 2
8450 linˇe 3
8451 line 4
8452 line 5
8453 "});
8454
8455 // Make an edit
8456 cx.update_editor(|editor, window, cx| {
8457 editor.handle_input("X", window, cx);
8458 });
8459
8460 // Move cursor to a different position
8461 cx.update_editor(|editor, window, cx| {
8462 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8463 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
8464 });
8465 });
8466
8467 cx.assert_editor_state(indoc! {"
8468 line 1
8469 line 2
8470 linXe 3
8471 line 4
8472 liˇne 5
8473 "});
8474
8475 cx.lsp
8476 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8477 Ok(Some(vec![lsp::TextEdit::new(
8478 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8479 "PREFIX ".to_string(),
8480 )]))
8481 });
8482
8483 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
8484 .unwrap()
8485 .await
8486 .unwrap();
8487
8488 cx.assert_editor_state(indoc! {"
8489 PREFIX line 1
8490 line 2
8491 linXe 3
8492 line 4
8493 liˇne 5
8494 "});
8495
8496 // Undo formatting
8497 cx.update_editor(|editor, window, cx| {
8498 editor.undo(&Default::default(), window, cx);
8499 });
8500
8501 // Verify cursor moved back to position after edit
8502 cx.assert_editor_state(indoc! {"
8503 line 1
8504 line 2
8505 linXˇe 3
8506 line 4
8507 line 5
8508 "});
8509}
8510
8511#[gpui::test]
8512async fn test_undo_edit_prediction_scrolls_to_edit_pos(cx: &mut TestAppContext) {
8513 init_test(cx, |_| {});
8514
8515 let mut cx = EditorTestContext::new(cx).await;
8516
8517 let provider = cx.new(|_| FakeEditPredictionProvider::default());
8518 cx.update_editor(|editor, window, cx| {
8519 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
8520 });
8521
8522 cx.set_state(indoc! {"
8523 line 1
8524 line 2
8525 linˇe 3
8526 line 4
8527 line 5
8528 line 6
8529 line 7
8530 line 8
8531 line 9
8532 line 10
8533 "});
8534
8535 let snapshot = cx.buffer_snapshot();
8536 let edit_position = snapshot.anchor_after(Point::new(2, 4));
8537
8538 cx.update(|_, cx| {
8539 provider.update(cx, |provider, _| {
8540 provider.set_edit_prediction(Some(edit_prediction::EditPrediction::Local {
8541 id: None,
8542 edits: vec![(edit_position..edit_position, "X".into())],
8543 edit_preview: None,
8544 }))
8545 })
8546 });
8547
8548 cx.update_editor(|editor, window, cx| editor.update_visible_edit_prediction(window, cx));
8549 cx.update_editor(|editor, window, cx| {
8550 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
8551 });
8552
8553 cx.assert_editor_state(indoc! {"
8554 line 1
8555 line 2
8556 lineXˇ 3
8557 line 4
8558 line 5
8559 line 6
8560 line 7
8561 line 8
8562 line 9
8563 line 10
8564 "});
8565
8566 cx.update_editor(|editor, window, cx| {
8567 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8568 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
8569 });
8570 });
8571
8572 cx.assert_editor_state(indoc! {"
8573 line 1
8574 line 2
8575 lineX 3
8576 line 4
8577 line 5
8578 line 6
8579 line 7
8580 line 8
8581 line 9
8582 liˇne 10
8583 "});
8584
8585 cx.update_editor(|editor, window, cx| {
8586 editor.undo(&Default::default(), window, cx);
8587 });
8588
8589 cx.assert_editor_state(indoc! {"
8590 line 1
8591 line 2
8592 lineˇ 3
8593 line 4
8594 line 5
8595 line 6
8596 line 7
8597 line 8
8598 line 9
8599 line 10
8600 "});
8601}
8602
8603#[gpui::test]
8604async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
8605 init_test(cx, |_| {});
8606
8607 let mut cx = EditorTestContext::new(cx).await;
8608 cx.set_state(
8609 r#"let foo = 2;
8610lˇet foo = 2;
8611let fooˇ = 2;
8612let foo = 2;
8613let foo = ˇ2;"#,
8614 );
8615
8616 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8617 .unwrap();
8618 cx.assert_editor_state(
8619 r#"let foo = 2;
8620«letˇ» foo = 2;
8621let «fooˇ» = 2;
8622let foo = 2;
8623let foo = «2ˇ»;"#,
8624 );
8625
8626 // noop for multiple selections with different contents
8627 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8628 .unwrap();
8629 cx.assert_editor_state(
8630 r#"let foo = 2;
8631«letˇ» foo = 2;
8632let «fooˇ» = 2;
8633let foo = 2;
8634let foo = «2ˇ»;"#,
8635 );
8636
8637 // Test last selection direction should be preserved
8638 cx.set_state(
8639 r#"let foo = 2;
8640let foo = 2;
8641let «fooˇ» = 2;
8642let «ˇfoo» = 2;
8643let foo = 2;"#,
8644 );
8645
8646 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
8647 .unwrap();
8648 cx.assert_editor_state(
8649 r#"let foo = 2;
8650let foo = 2;
8651let «fooˇ» = 2;
8652let «ˇfoo» = 2;
8653let «ˇfoo» = 2;"#,
8654 );
8655}
8656
8657#[gpui::test]
8658async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
8659 init_test(cx, |_| {});
8660
8661 let mut cx =
8662 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
8663
8664 cx.assert_editor_state(indoc! {"
8665 ˇbbb
8666 ccc
8667
8668 bbb
8669 ccc
8670 "});
8671 cx.dispatch_action(SelectPrevious::default());
8672 cx.assert_editor_state(indoc! {"
8673 «bbbˇ»
8674 ccc
8675
8676 bbb
8677 ccc
8678 "});
8679 cx.dispatch_action(SelectPrevious::default());
8680 cx.assert_editor_state(indoc! {"
8681 «bbbˇ»
8682 ccc
8683
8684 «bbbˇ»
8685 ccc
8686 "});
8687}
8688
8689#[gpui::test]
8690async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
8691 init_test(cx, |_| {});
8692
8693 let mut cx = EditorTestContext::new(cx).await;
8694 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
8695
8696 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8697 .unwrap();
8698 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8699
8700 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8701 .unwrap();
8702 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8703
8704 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8705 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
8706
8707 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8708 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
8709
8710 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8711 .unwrap();
8712 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
8713
8714 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8715 .unwrap();
8716 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
8717}
8718
8719#[gpui::test]
8720async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
8721 init_test(cx, |_| {});
8722
8723 let mut cx = EditorTestContext::new(cx).await;
8724 cx.set_state("aˇ");
8725
8726 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8727 .unwrap();
8728 cx.assert_editor_state("«aˇ»");
8729 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8730 .unwrap();
8731 cx.assert_editor_state("«aˇ»");
8732}
8733
8734#[gpui::test]
8735async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
8736 init_test(cx, |_| {});
8737
8738 let mut cx = EditorTestContext::new(cx).await;
8739 cx.set_state(
8740 r#"let foo = 2;
8741lˇet foo = 2;
8742let fooˇ = 2;
8743let foo = 2;
8744let foo = ˇ2;"#,
8745 );
8746
8747 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8748 .unwrap();
8749 cx.assert_editor_state(
8750 r#"let foo = 2;
8751«letˇ» foo = 2;
8752let «fooˇ» = 2;
8753let foo = 2;
8754let foo = «2ˇ»;"#,
8755 );
8756
8757 // noop for multiple selections with different contents
8758 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8759 .unwrap();
8760 cx.assert_editor_state(
8761 r#"let foo = 2;
8762«letˇ» foo = 2;
8763let «fooˇ» = 2;
8764let foo = 2;
8765let foo = «2ˇ»;"#,
8766 );
8767}
8768
8769#[gpui::test]
8770async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
8771 init_test(cx, |_| {});
8772
8773 let mut cx = EditorTestContext::new(cx).await;
8774 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
8775
8776 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8777 .unwrap();
8778 // selection direction is preserved
8779 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8780
8781 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8782 .unwrap();
8783 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8784
8785 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
8786 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
8787
8788 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
8789 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
8790
8791 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8792 .unwrap();
8793 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
8794
8795 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
8796 .unwrap();
8797 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
8798}
8799
8800#[gpui::test]
8801async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
8802 init_test(cx, |_| {});
8803
8804 let language = Arc::new(Language::new(
8805 LanguageConfig::default(),
8806 Some(tree_sitter_rust::LANGUAGE.into()),
8807 ));
8808
8809 let text = r#"
8810 use mod1::mod2::{mod3, mod4};
8811
8812 fn fn_1(param1: bool, param2: &str) {
8813 let var1 = "text";
8814 }
8815 "#
8816 .unindent();
8817
8818 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8819 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8820 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8821
8822 editor
8823 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8824 .await;
8825
8826 editor.update_in(cx, |editor, window, cx| {
8827 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
8828 s.select_display_ranges([
8829 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
8830 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
8831 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
8832 ]);
8833 });
8834 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8835 });
8836 editor.update(cx, |editor, cx| {
8837 assert_text_with_selections(
8838 editor,
8839 indoc! {r#"
8840 use mod1::mod2::{mod3, «mod4ˇ»};
8841
8842 fn fn_1«ˇ(param1: bool, param2: &str)» {
8843 let var1 = "«ˇtext»";
8844 }
8845 "#},
8846 cx,
8847 );
8848 });
8849
8850 editor.update_in(cx, |editor, window, cx| {
8851 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8852 });
8853 editor.update(cx, |editor, cx| {
8854 assert_text_with_selections(
8855 editor,
8856 indoc! {r#"
8857 use mod1::mod2::«{mod3, mod4}ˇ»;
8858
8859 «ˇfn fn_1(param1: bool, param2: &str) {
8860 let var1 = "text";
8861 }»
8862 "#},
8863 cx,
8864 );
8865 });
8866
8867 editor.update_in(cx, |editor, window, cx| {
8868 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8869 });
8870 assert_eq!(
8871 editor.update(cx, |editor, cx| editor
8872 .selections
8873 .display_ranges(&editor.display_snapshot(cx))),
8874 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8875 );
8876
8877 // Trying to expand the selected syntax node one more time has no effect.
8878 editor.update_in(cx, |editor, window, cx| {
8879 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8880 });
8881 assert_eq!(
8882 editor.update(cx, |editor, cx| editor
8883 .selections
8884 .display_ranges(&editor.display_snapshot(cx))),
8885 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
8886 );
8887
8888 editor.update_in(cx, |editor, window, cx| {
8889 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8890 });
8891 editor.update(cx, |editor, cx| {
8892 assert_text_with_selections(
8893 editor,
8894 indoc! {r#"
8895 use mod1::mod2::«{mod3, mod4}ˇ»;
8896
8897 «ˇfn fn_1(param1: bool, param2: &str) {
8898 let var1 = "text";
8899 }»
8900 "#},
8901 cx,
8902 );
8903 });
8904
8905 editor.update_in(cx, |editor, window, cx| {
8906 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8907 });
8908 editor.update(cx, |editor, cx| {
8909 assert_text_with_selections(
8910 editor,
8911 indoc! {r#"
8912 use mod1::mod2::{mod3, «mod4ˇ»};
8913
8914 fn fn_1«ˇ(param1: bool, param2: &str)» {
8915 let var1 = "«ˇtext»";
8916 }
8917 "#},
8918 cx,
8919 );
8920 });
8921
8922 editor.update_in(cx, |editor, window, cx| {
8923 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8924 });
8925 editor.update(cx, |editor, cx| {
8926 assert_text_with_selections(
8927 editor,
8928 indoc! {r#"
8929 use mod1::mod2::{mod3, moˇd4};
8930
8931 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8932 let var1 = "teˇxt";
8933 }
8934 "#},
8935 cx,
8936 );
8937 });
8938
8939 // Trying to shrink the selected syntax node one more time has no effect.
8940 editor.update_in(cx, |editor, window, cx| {
8941 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
8942 });
8943 editor.update_in(cx, |editor, _, cx| {
8944 assert_text_with_selections(
8945 editor,
8946 indoc! {r#"
8947 use mod1::mod2::{mod3, moˇd4};
8948
8949 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
8950 let var1 = "teˇxt";
8951 }
8952 "#},
8953 cx,
8954 );
8955 });
8956
8957 // Ensure that we keep expanding the selection if the larger selection starts or ends within
8958 // a fold.
8959 editor.update_in(cx, |editor, window, cx| {
8960 editor.fold_creases(
8961 vec![
8962 Crease::simple(
8963 Point::new(0, 21)..Point::new(0, 24),
8964 FoldPlaceholder::test(),
8965 ),
8966 Crease::simple(
8967 Point::new(3, 20)..Point::new(3, 22),
8968 FoldPlaceholder::test(),
8969 ),
8970 ],
8971 true,
8972 window,
8973 cx,
8974 );
8975 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
8976 });
8977 editor.update(cx, |editor, cx| {
8978 assert_text_with_selections(
8979 editor,
8980 indoc! {r#"
8981 use mod1::mod2::«{mod3, mod4}ˇ»;
8982
8983 fn fn_1«ˇ(param1: bool, param2: &str)» {
8984 let var1 = "«ˇtext»";
8985 }
8986 "#},
8987 cx,
8988 );
8989 });
8990}
8991
8992#[gpui::test]
8993async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
8994 init_test(cx, |_| {});
8995
8996 let language = Arc::new(Language::new(
8997 LanguageConfig::default(),
8998 Some(tree_sitter_rust::LANGUAGE.into()),
8999 ));
9000
9001 let text = "let a = 2;";
9002
9003 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9004 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9005 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9006
9007 editor
9008 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9009 .await;
9010
9011 // Test case 1: Cursor at end of word
9012 editor.update_in(cx, |editor, window, cx| {
9013 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9014 s.select_display_ranges([
9015 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
9016 ]);
9017 });
9018 });
9019 editor.update(cx, |editor, cx| {
9020 assert_text_with_selections(editor, "let aˇ = 2;", cx);
9021 });
9022 editor.update_in(cx, |editor, window, cx| {
9023 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9024 });
9025 editor.update(cx, |editor, cx| {
9026 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
9027 });
9028 editor.update_in(cx, |editor, window, cx| {
9029 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9030 });
9031 editor.update(cx, |editor, cx| {
9032 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9033 });
9034
9035 // Test case 2: Cursor at end of statement
9036 editor.update_in(cx, |editor, window, cx| {
9037 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9038 s.select_display_ranges([
9039 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
9040 ]);
9041 });
9042 });
9043 editor.update(cx, |editor, cx| {
9044 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
9045 });
9046 editor.update_in(cx, |editor, window, cx| {
9047 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9048 });
9049 editor.update(cx, |editor, cx| {
9050 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
9051 });
9052}
9053
9054#[gpui::test]
9055async fn test_select_larger_syntax_node_for_cursor_at_symbol(cx: &mut TestAppContext) {
9056 init_test(cx, |_| {});
9057
9058 let language = Arc::new(Language::new(
9059 LanguageConfig {
9060 name: "JavaScript".into(),
9061 ..Default::default()
9062 },
9063 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
9064 ));
9065
9066 let text = r#"
9067 let a = {
9068 key: "value",
9069 };
9070 "#
9071 .unindent();
9072
9073 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9074 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9075 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9076
9077 editor
9078 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9079 .await;
9080
9081 // Test case 1: Cursor after '{'
9082 editor.update_in(cx, |editor, window, cx| {
9083 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9084 s.select_display_ranges([
9085 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 9)
9086 ]);
9087 });
9088 });
9089 editor.update(cx, |editor, cx| {
9090 assert_text_with_selections(
9091 editor,
9092 indoc! {r#"
9093 let a = {ˇ
9094 key: "value",
9095 };
9096 "#},
9097 cx,
9098 );
9099 });
9100 editor.update_in(cx, |editor, window, cx| {
9101 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9102 });
9103 editor.update(cx, |editor, cx| {
9104 assert_text_with_selections(
9105 editor,
9106 indoc! {r#"
9107 let a = «ˇ{
9108 key: "value",
9109 }»;
9110 "#},
9111 cx,
9112 );
9113 });
9114
9115 // Test case 2: Cursor after ':'
9116 editor.update_in(cx, |editor, window, cx| {
9117 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9118 s.select_display_ranges([
9119 DisplayPoint::new(DisplayRow(1), 8)..DisplayPoint::new(DisplayRow(1), 8)
9120 ]);
9121 });
9122 });
9123 editor.update(cx, |editor, cx| {
9124 assert_text_with_selections(
9125 editor,
9126 indoc! {r#"
9127 let a = {
9128 key:ˇ "value",
9129 };
9130 "#},
9131 cx,
9132 );
9133 });
9134 editor.update_in(cx, |editor, window, cx| {
9135 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9136 });
9137 editor.update(cx, |editor, cx| {
9138 assert_text_with_selections(
9139 editor,
9140 indoc! {r#"
9141 let a = {
9142 «ˇkey: "value"»,
9143 };
9144 "#},
9145 cx,
9146 );
9147 });
9148 editor.update_in(cx, |editor, window, cx| {
9149 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9150 });
9151 editor.update(cx, |editor, cx| {
9152 assert_text_with_selections(
9153 editor,
9154 indoc! {r#"
9155 let a = «ˇ{
9156 key: "value",
9157 }»;
9158 "#},
9159 cx,
9160 );
9161 });
9162
9163 // Test case 3: Cursor after ','
9164 editor.update_in(cx, |editor, window, cx| {
9165 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9166 s.select_display_ranges([
9167 DisplayPoint::new(DisplayRow(1), 17)..DisplayPoint::new(DisplayRow(1), 17)
9168 ]);
9169 });
9170 });
9171 editor.update(cx, |editor, cx| {
9172 assert_text_with_selections(
9173 editor,
9174 indoc! {r#"
9175 let a = {
9176 key: "value",ˇ
9177 };
9178 "#},
9179 cx,
9180 );
9181 });
9182 editor.update_in(cx, |editor, window, cx| {
9183 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9184 });
9185 editor.update(cx, |editor, cx| {
9186 assert_text_with_selections(
9187 editor,
9188 indoc! {r#"
9189 let a = «ˇ{
9190 key: "value",
9191 }»;
9192 "#},
9193 cx,
9194 );
9195 });
9196
9197 // Test case 4: Cursor after ';'
9198 editor.update_in(cx, |editor, window, cx| {
9199 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9200 s.select_display_ranges([
9201 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)
9202 ]);
9203 });
9204 });
9205 editor.update(cx, |editor, cx| {
9206 assert_text_with_selections(
9207 editor,
9208 indoc! {r#"
9209 let a = {
9210 key: "value",
9211 };ˇ
9212 "#},
9213 cx,
9214 );
9215 });
9216 editor.update_in(cx, |editor, window, cx| {
9217 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9218 });
9219 editor.update(cx, |editor, cx| {
9220 assert_text_with_selections(
9221 editor,
9222 indoc! {r#"
9223 «ˇlet a = {
9224 key: "value",
9225 };
9226 »"#},
9227 cx,
9228 );
9229 });
9230}
9231
9232#[gpui::test]
9233async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
9234 init_test(cx, |_| {});
9235
9236 let language = Arc::new(Language::new(
9237 LanguageConfig::default(),
9238 Some(tree_sitter_rust::LANGUAGE.into()),
9239 ));
9240
9241 let text = r#"
9242 use mod1::mod2::{mod3, mod4};
9243
9244 fn fn_1(param1: bool, param2: &str) {
9245 let var1 = "hello world";
9246 }
9247 "#
9248 .unindent();
9249
9250 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9251 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9252 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9253
9254 editor
9255 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9256 .await;
9257
9258 // Test 1: Cursor on a letter of a string word
9259 editor.update_in(cx, |editor, window, cx| {
9260 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9261 s.select_display_ranges([
9262 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
9263 ]);
9264 });
9265 });
9266 editor.update_in(cx, |editor, window, cx| {
9267 assert_text_with_selections(
9268 editor,
9269 indoc! {r#"
9270 use mod1::mod2::{mod3, mod4};
9271
9272 fn fn_1(param1: bool, param2: &str) {
9273 let var1 = "hˇello world";
9274 }
9275 "#},
9276 cx,
9277 );
9278 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9279 assert_text_with_selections(
9280 editor,
9281 indoc! {r#"
9282 use mod1::mod2::{mod3, mod4};
9283
9284 fn fn_1(param1: bool, param2: &str) {
9285 let var1 = "«ˇhello» world";
9286 }
9287 "#},
9288 cx,
9289 );
9290 });
9291
9292 // Test 2: Partial selection within a word
9293 editor.update_in(cx, |editor, window, cx| {
9294 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9295 s.select_display_ranges([
9296 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
9297 ]);
9298 });
9299 });
9300 editor.update_in(cx, |editor, window, cx| {
9301 assert_text_with_selections(
9302 editor,
9303 indoc! {r#"
9304 use mod1::mod2::{mod3, mod4};
9305
9306 fn fn_1(param1: bool, param2: &str) {
9307 let var1 = "h«elˇ»lo world";
9308 }
9309 "#},
9310 cx,
9311 );
9312 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9313 assert_text_with_selections(
9314 editor,
9315 indoc! {r#"
9316 use mod1::mod2::{mod3, mod4};
9317
9318 fn fn_1(param1: bool, param2: &str) {
9319 let var1 = "«ˇhello» world";
9320 }
9321 "#},
9322 cx,
9323 );
9324 });
9325
9326 // Test 3: Complete word already selected
9327 editor.update_in(cx, |editor, window, cx| {
9328 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9329 s.select_display_ranges([
9330 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
9331 ]);
9332 });
9333 });
9334 editor.update_in(cx, |editor, window, cx| {
9335 assert_text_with_selections(
9336 editor,
9337 indoc! {r#"
9338 use mod1::mod2::{mod3, mod4};
9339
9340 fn fn_1(param1: bool, param2: &str) {
9341 let var1 = "«helloˇ» world";
9342 }
9343 "#},
9344 cx,
9345 );
9346 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9347 assert_text_with_selections(
9348 editor,
9349 indoc! {r#"
9350 use mod1::mod2::{mod3, mod4};
9351
9352 fn fn_1(param1: bool, param2: &str) {
9353 let var1 = "«hello worldˇ»";
9354 }
9355 "#},
9356 cx,
9357 );
9358 });
9359
9360 // Test 4: Selection spanning across words
9361 editor.update_in(cx, |editor, window, cx| {
9362 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9363 s.select_display_ranges([
9364 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
9365 ]);
9366 });
9367 });
9368 editor.update_in(cx, |editor, window, cx| {
9369 assert_text_with_selections(
9370 editor,
9371 indoc! {r#"
9372 use mod1::mod2::{mod3, mod4};
9373
9374 fn fn_1(param1: bool, param2: &str) {
9375 let var1 = "hel«lo woˇ»rld";
9376 }
9377 "#},
9378 cx,
9379 );
9380 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9381 assert_text_with_selections(
9382 editor,
9383 indoc! {r#"
9384 use mod1::mod2::{mod3, mod4};
9385
9386 fn fn_1(param1: bool, param2: &str) {
9387 let var1 = "«ˇhello world»";
9388 }
9389 "#},
9390 cx,
9391 );
9392 });
9393
9394 // Test 5: Expansion beyond string
9395 editor.update_in(cx, |editor, window, cx| {
9396 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9397 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
9398 assert_text_with_selections(
9399 editor,
9400 indoc! {r#"
9401 use mod1::mod2::{mod3, mod4};
9402
9403 fn fn_1(param1: bool, param2: &str) {
9404 «ˇlet var1 = "hello world";»
9405 }
9406 "#},
9407 cx,
9408 );
9409 });
9410}
9411
9412#[gpui::test]
9413async fn test_unwrap_syntax_nodes(cx: &mut gpui::TestAppContext) {
9414 init_test(cx, |_| {});
9415
9416 let mut cx = EditorTestContext::new(cx).await;
9417
9418 let language = Arc::new(Language::new(
9419 LanguageConfig::default(),
9420 Some(tree_sitter_rust::LANGUAGE.into()),
9421 ));
9422
9423 cx.update_buffer(|buffer, cx| {
9424 buffer.set_language(Some(language), cx);
9425 });
9426
9427 cx.set_state(indoc! { r#"use mod1::{mod2::{«mod3ˇ», mod4}, mod5::{mod6, «mod7ˇ»}};"# });
9428 cx.update_editor(|editor, window, cx| {
9429 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9430 });
9431
9432 cx.assert_editor_state(indoc! { r#"use mod1::{mod2::«mod3ˇ», mod5::«mod7ˇ»};"# });
9433
9434 cx.set_state(indoc! { r#"fn a() {
9435 // what
9436 // a
9437 // ˇlong
9438 // method
9439 // I
9440 // sure
9441 // hope
9442 // it
9443 // works
9444 }"# });
9445
9446 let buffer = cx.update_multibuffer(|multibuffer, _| multibuffer.as_singleton().unwrap());
9447 let multi_buffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
9448 cx.update(|_, cx| {
9449 multi_buffer.update(cx, |multi_buffer, cx| {
9450 multi_buffer.set_excerpts_for_path(
9451 PathKey::for_buffer(&buffer, cx),
9452 buffer,
9453 [Point::new(1, 0)..Point::new(1, 0)],
9454 3,
9455 cx,
9456 );
9457 });
9458 });
9459
9460 let editor2 = cx.new_window_entity(|window, cx| {
9461 Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
9462 });
9463
9464 let mut cx = EditorTestContext::for_editor_in(editor2, &mut cx).await;
9465 cx.update_editor(|editor, window, cx| {
9466 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
9467 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)]);
9468 })
9469 });
9470
9471 cx.assert_editor_state(indoc! { "
9472 fn a() {
9473 // what
9474 // a
9475 ˇ // long
9476 // method"});
9477
9478 cx.update_editor(|editor, window, cx| {
9479 editor.unwrap_syntax_node(&UnwrapSyntaxNode, window, cx);
9480 });
9481
9482 // Although we could potentially make the action work when the syntax node
9483 // is half-hidden, it seems a bit dangerous as you can't easily tell what it
9484 // did. Maybe we could also expand the excerpt to contain the range?
9485 cx.assert_editor_state(indoc! { "
9486 fn a() {
9487 // what
9488 // a
9489 ˇ // long
9490 // method"});
9491}
9492
9493#[gpui::test]
9494async fn test_fold_function_bodies(cx: &mut TestAppContext) {
9495 init_test(cx, |_| {});
9496
9497 let base_text = r#"
9498 impl A {
9499 // this is an uncommitted comment
9500
9501 fn b() {
9502 c();
9503 }
9504
9505 // this is another uncommitted comment
9506
9507 fn d() {
9508 // e
9509 // f
9510 }
9511 }
9512
9513 fn g() {
9514 // h
9515 }
9516 "#
9517 .unindent();
9518
9519 let text = r#"
9520 ˇimpl A {
9521
9522 fn b() {
9523 c();
9524 }
9525
9526 fn d() {
9527 // e
9528 // f
9529 }
9530 }
9531
9532 fn g() {
9533 // h
9534 }
9535 "#
9536 .unindent();
9537
9538 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9539 cx.set_state(&text);
9540 cx.set_head_text(&base_text);
9541 cx.update_editor(|editor, window, cx| {
9542 editor.expand_all_diff_hunks(&Default::default(), window, cx);
9543 });
9544
9545 cx.assert_state_with_diff(
9546 "
9547 ˇimpl A {
9548 - // this is an uncommitted comment
9549
9550 fn b() {
9551 c();
9552 }
9553
9554 - // this is another uncommitted comment
9555 -
9556 fn d() {
9557 // e
9558 // f
9559 }
9560 }
9561
9562 fn g() {
9563 // h
9564 }
9565 "
9566 .unindent(),
9567 );
9568
9569 let expected_display_text = "
9570 impl A {
9571 // this is an uncommitted comment
9572
9573 fn b() {
9574 ⋯
9575 }
9576
9577 // this is another uncommitted comment
9578
9579 fn d() {
9580 ⋯
9581 }
9582 }
9583
9584 fn g() {
9585 ⋯
9586 }
9587 "
9588 .unindent();
9589
9590 cx.update_editor(|editor, window, cx| {
9591 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
9592 assert_eq!(editor.display_text(cx), expected_display_text);
9593 });
9594}
9595
9596#[gpui::test]
9597async fn test_autoindent(cx: &mut TestAppContext) {
9598 init_test(cx, |_| {});
9599
9600 let language = Arc::new(
9601 Language::new(
9602 LanguageConfig {
9603 brackets: BracketPairConfig {
9604 pairs: vec![
9605 BracketPair {
9606 start: "{".to_string(),
9607 end: "}".to_string(),
9608 close: false,
9609 surround: false,
9610 newline: true,
9611 },
9612 BracketPair {
9613 start: "(".to_string(),
9614 end: ")".to_string(),
9615 close: false,
9616 surround: false,
9617 newline: true,
9618 },
9619 ],
9620 ..Default::default()
9621 },
9622 ..Default::default()
9623 },
9624 Some(tree_sitter_rust::LANGUAGE.into()),
9625 )
9626 .with_indents_query(
9627 r#"
9628 (_ "(" ")" @end) @indent
9629 (_ "{" "}" @end) @indent
9630 "#,
9631 )
9632 .unwrap(),
9633 );
9634
9635 let text = "fn a() {}";
9636
9637 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9638 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9639 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9640 editor
9641 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9642 .await;
9643
9644 editor.update_in(cx, |editor, window, cx| {
9645 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9646 s.select_ranges([5..5, 8..8, 9..9])
9647 });
9648 editor.newline(&Newline, window, cx);
9649 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
9650 assert_eq!(
9651 editor.selections.ranges(&editor.display_snapshot(cx)),
9652 &[
9653 Point::new(1, 4)..Point::new(1, 4),
9654 Point::new(3, 4)..Point::new(3, 4),
9655 Point::new(5, 0)..Point::new(5, 0)
9656 ]
9657 );
9658 });
9659}
9660
9661#[gpui::test]
9662async fn test_autoindent_disabled(cx: &mut TestAppContext) {
9663 init_test(cx, |settings| settings.defaults.auto_indent = Some(false));
9664
9665 let language = Arc::new(
9666 Language::new(
9667 LanguageConfig {
9668 brackets: BracketPairConfig {
9669 pairs: vec![
9670 BracketPair {
9671 start: "{".to_string(),
9672 end: "}".to_string(),
9673 close: false,
9674 surround: false,
9675 newline: true,
9676 },
9677 BracketPair {
9678 start: "(".to_string(),
9679 end: ")".to_string(),
9680 close: false,
9681 surround: false,
9682 newline: true,
9683 },
9684 ],
9685 ..Default::default()
9686 },
9687 ..Default::default()
9688 },
9689 Some(tree_sitter_rust::LANGUAGE.into()),
9690 )
9691 .with_indents_query(
9692 r#"
9693 (_ "(" ")" @end) @indent
9694 (_ "{" "}" @end) @indent
9695 "#,
9696 )
9697 .unwrap(),
9698 );
9699
9700 let text = "fn a() {}";
9701
9702 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
9703 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9704 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
9705 editor
9706 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
9707 .await;
9708
9709 editor.update_in(cx, |editor, window, cx| {
9710 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
9711 s.select_ranges([5..5, 8..8, 9..9])
9712 });
9713 editor.newline(&Newline, window, cx);
9714 assert_eq!(
9715 editor.text(cx),
9716 indoc!(
9717 "
9718 fn a(
9719
9720 ) {
9721
9722 }
9723 "
9724 )
9725 );
9726 assert_eq!(
9727 editor.selections.ranges(&editor.display_snapshot(cx)),
9728 &[
9729 Point::new(1, 0)..Point::new(1, 0),
9730 Point::new(3, 0)..Point::new(3, 0),
9731 Point::new(5, 0)..Point::new(5, 0)
9732 ]
9733 );
9734 });
9735}
9736
9737#[gpui::test]
9738async fn test_autoindent_disabled_with_nested_language(cx: &mut TestAppContext) {
9739 init_test(cx, |settings| {
9740 settings.defaults.auto_indent = Some(true);
9741 settings.languages.0.insert(
9742 "python".into(),
9743 LanguageSettingsContent {
9744 auto_indent: Some(false),
9745 ..Default::default()
9746 },
9747 );
9748 });
9749
9750 let mut cx = EditorTestContext::new(cx).await;
9751
9752 let injected_language = Arc::new(
9753 Language::new(
9754 LanguageConfig {
9755 brackets: BracketPairConfig {
9756 pairs: vec![
9757 BracketPair {
9758 start: "{".to_string(),
9759 end: "}".to_string(),
9760 close: false,
9761 surround: false,
9762 newline: true,
9763 },
9764 BracketPair {
9765 start: "(".to_string(),
9766 end: ")".to_string(),
9767 close: true,
9768 surround: false,
9769 newline: true,
9770 },
9771 ],
9772 ..Default::default()
9773 },
9774 name: "python".into(),
9775 ..Default::default()
9776 },
9777 Some(tree_sitter_python::LANGUAGE.into()),
9778 )
9779 .with_indents_query(
9780 r#"
9781 (_ "(" ")" @end) @indent
9782 (_ "{" "}" @end) @indent
9783 "#,
9784 )
9785 .unwrap(),
9786 );
9787
9788 let language = Arc::new(
9789 Language::new(
9790 LanguageConfig {
9791 brackets: BracketPairConfig {
9792 pairs: vec![
9793 BracketPair {
9794 start: "{".to_string(),
9795 end: "}".to_string(),
9796 close: false,
9797 surround: false,
9798 newline: true,
9799 },
9800 BracketPair {
9801 start: "(".to_string(),
9802 end: ")".to_string(),
9803 close: true,
9804 surround: false,
9805 newline: true,
9806 },
9807 ],
9808 ..Default::default()
9809 },
9810 name: LanguageName::new("rust"),
9811 ..Default::default()
9812 },
9813 Some(tree_sitter_rust::LANGUAGE.into()),
9814 )
9815 .with_indents_query(
9816 r#"
9817 (_ "(" ")" @end) @indent
9818 (_ "{" "}" @end) @indent
9819 "#,
9820 )
9821 .unwrap()
9822 .with_injection_query(
9823 r#"
9824 (macro_invocation
9825 macro: (identifier) @_macro_name
9826 (token_tree) @injection.content
9827 (#set! injection.language "python"))
9828 "#,
9829 )
9830 .unwrap(),
9831 );
9832
9833 cx.language_registry().add(injected_language);
9834 cx.language_registry().add(language.clone());
9835
9836 cx.update_buffer(|buffer, cx| {
9837 buffer.set_language(Some(language), cx);
9838 });
9839
9840 cx.set_state(r#"struct A {ˇ}"#);
9841
9842 cx.update_editor(|editor, window, cx| {
9843 editor.newline(&Default::default(), window, cx);
9844 });
9845
9846 cx.assert_editor_state(indoc!(
9847 "struct A {
9848 ˇ
9849 }"
9850 ));
9851
9852 cx.set_state(r#"select_biased!(ˇ)"#);
9853
9854 cx.update_editor(|editor, window, cx| {
9855 editor.newline(&Default::default(), window, cx);
9856 editor.handle_input("def ", window, cx);
9857 editor.handle_input("(", window, cx);
9858 editor.newline(&Default::default(), window, cx);
9859 editor.handle_input("a", window, cx);
9860 });
9861
9862 cx.assert_editor_state(indoc!(
9863 "select_biased!(
9864 def (
9865 aˇ
9866 )
9867 )"
9868 ));
9869}
9870
9871#[gpui::test]
9872async fn test_autoindent_selections(cx: &mut TestAppContext) {
9873 init_test(cx, |_| {});
9874
9875 {
9876 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
9877 cx.set_state(indoc! {"
9878 impl A {
9879
9880 fn b() {}
9881
9882 «fn c() {
9883
9884 }ˇ»
9885 }
9886 "});
9887
9888 cx.update_editor(|editor, window, cx| {
9889 editor.autoindent(&Default::default(), window, cx);
9890 });
9891
9892 cx.assert_editor_state(indoc! {"
9893 impl A {
9894
9895 fn b() {}
9896
9897 «fn c() {
9898
9899 }ˇ»
9900 }
9901 "});
9902 }
9903
9904 {
9905 let mut cx = EditorTestContext::new_multibuffer(
9906 cx,
9907 [indoc! { "
9908 impl A {
9909 «
9910 // a
9911 fn b(){}
9912 »
9913 «
9914 }
9915 fn c(){}
9916 »
9917 "}],
9918 );
9919
9920 let buffer = cx.update_editor(|editor, _, cx| {
9921 let buffer = editor.buffer().update(cx, |buffer, _| {
9922 buffer.all_buffers().iter().next().unwrap().clone()
9923 });
9924 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
9925 buffer
9926 });
9927
9928 cx.run_until_parked();
9929 cx.update_editor(|editor, window, cx| {
9930 editor.select_all(&Default::default(), window, cx);
9931 editor.autoindent(&Default::default(), window, cx)
9932 });
9933 cx.run_until_parked();
9934
9935 cx.update(|_, cx| {
9936 assert_eq!(
9937 buffer.read(cx).text(),
9938 indoc! { "
9939 impl A {
9940
9941 // a
9942 fn b(){}
9943
9944
9945 }
9946 fn c(){}
9947
9948 " }
9949 )
9950 });
9951 }
9952}
9953
9954#[gpui::test]
9955async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
9956 init_test(cx, |_| {});
9957
9958 let mut cx = EditorTestContext::new(cx).await;
9959
9960 let language = Arc::new(Language::new(
9961 LanguageConfig {
9962 brackets: BracketPairConfig {
9963 pairs: vec![
9964 BracketPair {
9965 start: "{".to_string(),
9966 end: "}".to_string(),
9967 close: true,
9968 surround: true,
9969 newline: true,
9970 },
9971 BracketPair {
9972 start: "(".to_string(),
9973 end: ")".to_string(),
9974 close: true,
9975 surround: true,
9976 newline: true,
9977 },
9978 BracketPair {
9979 start: "/*".to_string(),
9980 end: " */".to_string(),
9981 close: true,
9982 surround: true,
9983 newline: true,
9984 },
9985 BracketPair {
9986 start: "[".to_string(),
9987 end: "]".to_string(),
9988 close: false,
9989 surround: false,
9990 newline: true,
9991 },
9992 BracketPair {
9993 start: "\"".to_string(),
9994 end: "\"".to_string(),
9995 close: true,
9996 surround: true,
9997 newline: false,
9998 },
9999 BracketPair {
10000 start: "<".to_string(),
10001 end: ">".to_string(),
10002 close: false,
10003 surround: true,
10004 newline: true,
10005 },
10006 ],
10007 ..Default::default()
10008 },
10009 autoclose_before: "})]".to_string(),
10010 ..Default::default()
10011 },
10012 Some(tree_sitter_rust::LANGUAGE.into()),
10013 ));
10014
10015 cx.language_registry().add(language.clone());
10016 cx.update_buffer(|buffer, cx| {
10017 buffer.set_language(Some(language), cx);
10018 });
10019
10020 cx.set_state(
10021 &r#"
10022 🏀ˇ
10023 εˇ
10024 ❤️ˇ
10025 "#
10026 .unindent(),
10027 );
10028
10029 // autoclose multiple nested brackets at multiple cursors
10030 cx.update_editor(|editor, window, cx| {
10031 editor.handle_input("{", window, cx);
10032 editor.handle_input("{", window, cx);
10033 editor.handle_input("{", window, cx);
10034 });
10035 cx.assert_editor_state(
10036 &"
10037 🏀{{{ˇ}}}
10038 ε{{{ˇ}}}
10039 ❤️{{{ˇ}}}
10040 "
10041 .unindent(),
10042 );
10043
10044 // insert a different closing bracket
10045 cx.update_editor(|editor, window, cx| {
10046 editor.handle_input(")", window, cx);
10047 });
10048 cx.assert_editor_state(
10049 &"
10050 🏀{{{)ˇ}}}
10051 ε{{{)ˇ}}}
10052 ❤️{{{)ˇ}}}
10053 "
10054 .unindent(),
10055 );
10056
10057 // skip over the auto-closed brackets when typing a closing bracket
10058 cx.update_editor(|editor, window, cx| {
10059 editor.move_right(&MoveRight, window, cx);
10060 editor.handle_input("}", window, cx);
10061 editor.handle_input("}", window, cx);
10062 editor.handle_input("}", window, cx);
10063 });
10064 cx.assert_editor_state(
10065 &"
10066 🏀{{{)}}}}ˇ
10067 ε{{{)}}}}ˇ
10068 ❤️{{{)}}}}ˇ
10069 "
10070 .unindent(),
10071 );
10072
10073 // autoclose multi-character pairs
10074 cx.set_state(
10075 &"
10076 ˇ
10077 ˇ
10078 "
10079 .unindent(),
10080 );
10081 cx.update_editor(|editor, window, cx| {
10082 editor.handle_input("/", window, cx);
10083 editor.handle_input("*", window, cx);
10084 });
10085 cx.assert_editor_state(
10086 &"
10087 /*ˇ */
10088 /*ˇ */
10089 "
10090 .unindent(),
10091 );
10092
10093 // one cursor autocloses a multi-character pair, one cursor
10094 // does not autoclose.
10095 cx.set_state(
10096 &"
10097 /ˇ
10098 ˇ
10099 "
10100 .unindent(),
10101 );
10102 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
10103 cx.assert_editor_state(
10104 &"
10105 /*ˇ */
10106 *ˇ
10107 "
10108 .unindent(),
10109 );
10110
10111 // Don't autoclose if the next character isn't whitespace and isn't
10112 // listed in the language's "autoclose_before" section.
10113 cx.set_state("ˇa b");
10114 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10115 cx.assert_editor_state("{ˇa b");
10116
10117 // Don't autoclose if `close` is false for the bracket pair
10118 cx.set_state("ˇ");
10119 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
10120 cx.assert_editor_state("[ˇ");
10121
10122 // Surround with brackets if text is selected
10123 cx.set_state("«aˇ» b");
10124 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10125 cx.assert_editor_state("{«aˇ»} b");
10126
10127 // Autoclose when not immediately after a word character
10128 cx.set_state("a ˇ");
10129 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10130 cx.assert_editor_state("a \"ˇ\"");
10131
10132 // Autoclose pair where the start and end characters are the same
10133 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10134 cx.assert_editor_state("a \"\"ˇ");
10135
10136 // Don't autoclose when immediately after a word character
10137 cx.set_state("aˇ");
10138 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10139 cx.assert_editor_state("a\"ˇ");
10140
10141 // Do autoclose when after a non-word character
10142 cx.set_state("{ˇ");
10143 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
10144 cx.assert_editor_state("{\"ˇ\"");
10145
10146 // Non identical pairs autoclose regardless of preceding character
10147 cx.set_state("aˇ");
10148 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
10149 cx.assert_editor_state("a{ˇ}");
10150
10151 // Don't autoclose pair if autoclose is disabled
10152 cx.set_state("ˇ");
10153 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10154 cx.assert_editor_state("<ˇ");
10155
10156 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
10157 cx.set_state("«aˇ» b");
10158 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
10159 cx.assert_editor_state("<«aˇ»> b");
10160}
10161
10162#[gpui::test]
10163async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
10164 init_test(cx, |settings| {
10165 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10166 });
10167
10168 let mut cx = EditorTestContext::new(cx).await;
10169
10170 let language = Arc::new(Language::new(
10171 LanguageConfig {
10172 brackets: BracketPairConfig {
10173 pairs: vec![
10174 BracketPair {
10175 start: "{".to_string(),
10176 end: "}".to_string(),
10177 close: true,
10178 surround: true,
10179 newline: true,
10180 },
10181 BracketPair {
10182 start: "(".to_string(),
10183 end: ")".to_string(),
10184 close: true,
10185 surround: true,
10186 newline: true,
10187 },
10188 BracketPair {
10189 start: "[".to_string(),
10190 end: "]".to_string(),
10191 close: false,
10192 surround: false,
10193 newline: true,
10194 },
10195 ],
10196 ..Default::default()
10197 },
10198 autoclose_before: "})]".to_string(),
10199 ..Default::default()
10200 },
10201 Some(tree_sitter_rust::LANGUAGE.into()),
10202 ));
10203
10204 cx.language_registry().add(language.clone());
10205 cx.update_buffer(|buffer, cx| {
10206 buffer.set_language(Some(language), cx);
10207 });
10208
10209 cx.set_state(
10210 &"
10211 ˇ
10212 ˇ
10213 ˇ
10214 "
10215 .unindent(),
10216 );
10217
10218 // ensure only matching closing brackets are skipped over
10219 cx.update_editor(|editor, window, cx| {
10220 editor.handle_input("}", window, cx);
10221 editor.move_left(&MoveLeft, window, cx);
10222 editor.handle_input(")", window, cx);
10223 editor.move_left(&MoveLeft, window, cx);
10224 });
10225 cx.assert_editor_state(
10226 &"
10227 ˇ)}
10228 ˇ)}
10229 ˇ)}
10230 "
10231 .unindent(),
10232 );
10233
10234 // skip-over closing brackets at multiple cursors
10235 cx.update_editor(|editor, window, cx| {
10236 editor.handle_input(")", window, cx);
10237 editor.handle_input("}", window, cx);
10238 });
10239 cx.assert_editor_state(
10240 &"
10241 )}ˇ
10242 )}ˇ
10243 )}ˇ
10244 "
10245 .unindent(),
10246 );
10247
10248 // ignore non-close brackets
10249 cx.update_editor(|editor, window, cx| {
10250 editor.handle_input("]", window, cx);
10251 editor.move_left(&MoveLeft, window, cx);
10252 editor.handle_input("]", window, cx);
10253 });
10254 cx.assert_editor_state(
10255 &"
10256 )}]ˇ]
10257 )}]ˇ]
10258 )}]ˇ]
10259 "
10260 .unindent(),
10261 );
10262}
10263
10264#[gpui::test]
10265async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
10266 init_test(cx, |_| {});
10267
10268 let mut cx = EditorTestContext::new(cx).await;
10269
10270 let html_language = Arc::new(
10271 Language::new(
10272 LanguageConfig {
10273 name: "HTML".into(),
10274 brackets: BracketPairConfig {
10275 pairs: vec![
10276 BracketPair {
10277 start: "<".into(),
10278 end: ">".into(),
10279 close: true,
10280 ..Default::default()
10281 },
10282 BracketPair {
10283 start: "{".into(),
10284 end: "}".into(),
10285 close: true,
10286 ..Default::default()
10287 },
10288 BracketPair {
10289 start: "(".into(),
10290 end: ")".into(),
10291 close: true,
10292 ..Default::default()
10293 },
10294 ],
10295 ..Default::default()
10296 },
10297 autoclose_before: "})]>".into(),
10298 ..Default::default()
10299 },
10300 Some(tree_sitter_html::LANGUAGE.into()),
10301 )
10302 .with_injection_query(
10303 r#"
10304 (script_element
10305 (raw_text) @injection.content
10306 (#set! injection.language "javascript"))
10307 "#,
10308 )
10309 .unwrap(),
10310 );
10311
10312 let javascript_language = Arc::new(Language::new(
10313 LanguageConfig {
10314 name: "JavaScript".into(),
10315 brackets: BracketPairConfig {
10316 pairs: vec![
10317 BracketPair {
10318 start: "/*".into(),
10319 end: " */".into(),
10320 close: true,
10321 ..Default::default()
10322 },
10323 BracketPair {
10324 start: "{".into(),
10325 end: "}".into(),
10326 close: true,
10327 ..Default::default()
10328 },
10329 BracketPair {
10330 start: "(".into(),
10331 end: ")".into(),
10332 close: true,
10333 ..Default::default()
10334 },
10335 ],
10336 ..Default::default()
10337 },
10338 autoclose_before: "})]>".into(),
10339 ..Default::default()
10340 },
10341 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
10342 ));
10343
10344 cx.language_registry().add(html_language.clone());
10345 cx.language_registry().add(javascript_language);
10346 cx.executor().run_until_parked();
10347
10348 cx.update_buffer(|buffer, cx| {
10349 buffer.set_language(Some(html_language), cx);
10350 });
10351
10352 cx.set_state(
10353 &r#"
10354 <body>ˇ
10355 <script>
10356 var x = 1;ˇ
10357 </script>
10358 </body>ˇ
10359 "#
10360 .unindent(),
10361 );
10362
10363 // Precondition: different languages are active at different locations.
10364 cx.update_editor(|editor, window, cx| {
10365 let snapshot = editor.snapshot(window, cx);
10366 let cursors = editor
10367 .selections
10368 .ranges::<usize>(&editor.display_snapshot(cx));
10369 let languages = cursors
10370 .iter()
10371 .map(|c| snapshot.language_at(c.start).unwrap().name())
10372 .collect::<Vec<_>>();
10373 assert_eq!(
10374 languages,
10375 &["HTML".into(), "JavaScript".into(), "HTML".into()]
10376 );
10377 });
10378
10379 // Angle brackets autoclose in HTML, but not JavaScript.
10380 cx.update_editor(|editor, window, cx| {
10381 editor.handle_input("<", window, cx);
10382 editor.handle_input("a", window, cx);
10383 });
10384 cx.assert_editor_state(
10385 &r#"
10386 <body><aˇ>
10387 <script>
10388 var x = 1;<aˇ
10389 </script>
10390 </body><aˇ>
10391 "#
10392 .unindent(),
10393 );
10394
10395 // Curly braces and parens autoclose in both HTML and JavaScript.
10396 cx.update_editor(|editor, window, cx| {
10397 editor.handle_input(" b=", window, cx);
10398 editor.handle_input("{", window, cx);
10399 editor.handle_input("c", window, cx);
10400 editor.handle_input("(", window, cx);
10401 });
10402 cx.assert_editor_state(
10403 &r#"
10404 <body><a b={c(ˇ)}>
10405 <script>
10406 var x = 1;<a b={c(ˇ)}
10407 </script>
10408 </body><a b={c(ˇ)}>
10409 "#
10410 .unindent(),
10411 );
10412
10413 // Brackets that were already autoclosed are skipped.
10414 cx.update_editor(|editor, window, cx| {
10415 editor.handle_input(")", window, cx);
10416 editor.handle_input("d", window, cx);
10417 editor.handle_input("}", window, cx);
10418 });
10419 cx.assert_editor_state(
10420 &r#"
10421 <body><a b={c()d}ˇ>
10422 <script>
10423 var x = 1;<a b={c()d}ˇ
10424 </script>
10425 </body><a b={c()d}ˇ>
10426 "#
10427 .unindent(),
10428 );
10429 cx.update_editor(|editor, window, cx| {
10430 editor.handle_input(">", window, cx);
10431 });
10432 cx.assert_editor_state(
10433 &r#"
10434 <body><a b={c()d}>ˇ
10435 <script>
10436 var x = 1;<a b={c()d}>ˇ
10437 </script>
10438 </body><a b={c()d}>ˇ
10439 "#
10440 .unindent(),
10441 );
10442
10443 // Reset
10444 cx.set_state(
10445 &r#"
10446 <body>ˇ
10447 <script>
10448 var x = 1;ˇ
10449 </script>
10450 </body>ˇ
10451 "#
10452 .unindent(),
10453 );
10454
10455 cx.update_editor(|editor, window, cx| {
10456 editor.handle_input("<", window, cx);
10457 });
10458 cx.assert_editor_state(
10459 &r#"
10460 <body><ˇ>
10461 <script>
10462 var x = 1;<ˇ
10463 </script>
10464 </body><ˇ>
10465 "#
10466 .unindent(),
10467 );
10468
10469 // When backspacing, the closing angle brackets are removed.
10470 cx.update_editor(|editor, window, cx| {
10471 editor.backspace(&Backspace, window, cx);
10472 });
10473 cx.assert_editor_state(
10474 &r#"
10475 <body>ˇ
10476 <script>
10477 var x = 1;ˇ
10478 </script>
10479 </body>ˇ
10480 "#
10481 .unindent(),
10482 );
10483
10484 // Block comments autoclose in JavaScript, but not HTML.
10485 cx.update_editor(|editor, window, cx| {
10486 editor.handle_input("/", window, cx);
10487 editor.handle_input("*", window, cx);
10488 });
10489 cx.assert_editor_state(
10490 &r#"
10491 <body>/*ˇ
10492 <script>
10493 var x = 1;/*ˇ */
10494 </script>
10495 </body>/*ˇ
10496 "#
10497 .unindent(),
10498 );
10499}
10500
10501#[gpui::test]
10502async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
10503 init_test(cx, |_| {});
10504
10505 let mut cx = EditorTestContext::new(cx).await;
10506
10507 let rust_language = Arc::new(
10508 Language::new(
10509 LanguageConfig {
10510 name: "Rust".into(),
10511 brackets: serde_json::from_value(json!([
10512 { "start": "{", "end": "}", "close": true, "newline": true },
10513 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
10514 ]))
10515 .unwrap(),
10516 autoclose_before: "})]>".into(),
10517 ..Default::default()
10518 },
10519 Some(tree_sitter_rust::LANGUAGE.into()),
10520 )
10521 .with_override_query("(string_literal) @string")
10522 .unwrap(),
10523 );
10524
10525 cx.language_registry().add(rust_language.clone());
10526 cx.update_buffer(|buffer, cx| {
10527 buffer.set_language(Some(rust_language), cx);
10528 });
10529
10530 cx.set_state(
10531 &r#"
10532 let x = ˇ
10533 "#
10534 .unindent(),
10535 );
10536
10537 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
10538 cx.update_editor(|editor, window, cx| {
10539 editor.handle_input("\"", window, cx);
10540 });
10541 cx.assert_editor_state(
10542 &r#"
10543 let x = "ˇ"
10544 "#
10545 .unindent(),
10546 );
10547
10548 // Inserting another quotation mark. The cursor moves across the existing
10549 // automatically-inserted quotation mark.
10550 cx.update_editor(|editor, window, cx| {
10551 editor.handle_input("\"", window, cx);
10552 });
10553 cx.assert_editor_state(
10554 &r#"
10555 let x = ""ˇ
10556 "#
10557 .unindent(),
10558 );
10559
10560 // Reset
10561 cx.set_state(
10562 &r#"
10563 let x = ˇ
10564 "#
10565 .unindent(),
10566 );
10567
10568 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
10569 cx.update_editor(|editor, window, cx| {
10570 editor.handle_input("\"", window, cx);
10571 editor.handle_input(" ", window, cx);
10572 editor.move_left(&Default::default(), window, cx);
10573 editor.handle_input("\\", window, cx);
10574 editor.handle_input("\"", window, cx);
10575 });
10576 cx.assert_editor_state(
10577 &r#"
10578 let x = "\"ˇ "
10579 "#
10580 .unindent(),
10581 );
10582
10583 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
10584 // mark. Nothing is inserted.
10585 cx.update_editor(|editor, window, cx| {
10586 editor.move_right(&Default::default(), window, cx);
10587 editor.handle_input("\"", window, cx);
10588 });
10589 cx.assert_editor_state(
10590 &r#"
10591 let x = "\" "ˇ
10592 "#
10593 .unindent(),
10594 );
10595}
10596
10597#[gpui::test]
10598async fn test_surround_with_pair(cx: &mut TestAppContext) {
10599 init_test(cx, |_| {});
10600
10601 let language = Arc::new(Language::new(
10602 LanguageConfig {
10603 brackets: BracketPairConfig {
10604 pairs: vec![
10605 BracketPair {
10606 start: "{".to_string(),
10607 end: "}".to_string(),
10608 close: true,
10609 surround: true,
10610 newline: true,
10611 },
10612 BracketPair {
10613 start: "/* ".to_string(),
10614 end: "*/".to_string(),
10615 close: true,
10616 surround: true,
10617 ..Default::default()
10618 },
10619 ],
10620 ..Default::default()
10621 },
10622 ..Default::default()
10623 },
10624 Some(tree_sitter_rust::LANGUAGE.into()),
10625 ));
10626
10627 let text = r#"
10628 a
10629 b
10630 c
10631 "#
10632 .unindent();
10633
10634 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10635 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10636 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10637 editor
10638 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10639 .await;
10640
10641 editor.update_in(cx, |editor, window, cx| {
10642 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10643 s.select_display_ranges([
10644 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10645 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10646 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
10647 ])
10648 });
10649
10650 editor.handle_input("{", window, cx);
10651 editor.handle_input("{", window, cx);
10652 editor.handle_input("{", window, cx);
10653 assert_eq!(
10654 editor.text(cx),
10655 "
10656 {{{a}}}
10657 {{{b}}}
10658 {{{c}}}
10659 "
10660 .unindent()
10661 );
10662 assert_eq!(
10663 display_ranges(editor, cx),
10664 [
10665 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
10666 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
10667 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
10668 ]
10669 );
10670
10671 editor.undo(&Undo, window, cx);
10672 editor.undo(&Undo, window, cx);
10673 editor.undo(&Undo, window, cx);
10674 assert_eq!(
10675 editor.text(cx),
10676 "
10677 a
10678 b
10679 c
10680 "
10681 .unindent()
10682 );
10683 assert_eq!(
10684 display_ranges(editor, cx),
10685 [
10686 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10687 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10688 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10689 ]
10690 );
10691
10692 // Ensure inserting the first character of a multi-byte bracket pair
10693 // doesn't surround the selections with the bracket.
10694 editor.handle_input("/", window, cx);
10695 assert_eq!(
10696 editor.text(cx),
10697 "
10698 /
10699 /
10700 /
10701 "
10702 .unindent()
10703 );
10704 assert_eq!(
10705 display_ranges(editor, cx),
10706 [
10707 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10708 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10709 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10710 ]
10711 );
10712
10713 editor.undo(&Undo, window, cx);
10714 assert_eq!(
10715 editor.text(cx),
10716 "
10717 a
10718 b
10719 c
10720 "
10721 .unindent()
10722 );
10723 assert_eq!(
10724 display_ranges(editor, cx),
10725 [
10726 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
10727 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
10728 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
10729 ]
10730 );
10731
10732 // Ensure inserting the last character of a multi-byte bracket pair
10733 // doesn't surround the selections with the bracket.
10734 editor.handle_input("*", window, cx);
10735 assert_eq!(
10736 editor.text(cx),
10737 "
10738 *
10739 *
10740 *
10741 "
10742 .unindent()
10743 );
10744 assert_eq!(
10745 display_ranges(editor, cx),
10746 [
10747 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
10748 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
10749 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
10750 ]
10751 );
10752 });
10753}
10754
10755#[gpui::test]
10756async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
10757 init_test(cx, |_| {});
10758
10759 let language = Arc::new(Language::new(
10760 LanguageConfig {
10761 brackets: BracketPairConfig {
10762 pairs: vec![BracketPair {
10763 start: "{".to_string(),
10764 end: "}".to_string(),
10765 close: true,
10766 surround: true,
10767 newline: true,
10768 }],
10769 ..Default::default()
10770 },
10771 autoclose_before: "}".to_string(),
10772 ..Default::default()
10773 },
10774 Some(tree_sitter_rust::LANGUAGE.into()),
10775 ));
10776
10777 let text = r#"
10778 a
10779 b
10780 c
10781 "#
10782 .unindent();
10783
10784 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
10785 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10786 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10787 editor
10788 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10789 .await;
10790
10791 editor.update_in(cx, |editor, window, cx| {
10792 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
10793 s.select_ranges([
10794 Point::new(0, 1)..Point::new(0, 1),
10795 Point::new(1, 1)..Point::new(1, 1),
10796 Point::new(2, 1)..Point::new(2, 1),
10797 ])
10798 });
10799
10800 editor.handle_input("{", window, cx);
10801 editor.handle_input("{", window, cx);
10802 editor.handle_input("_", window, cx);
10803 assert_eq!(
10804 editor.text(cx),
10805 "
10806 a{{_}}
10807 b{{_}}
10808 c{{_}}
10809 "
10810 .unindent()
10811 );
10812 assert_eq!(
10813 editor
10814 .selections
10815 .ranges::<Point>(&editor.display_snapshot(cx)),
10816 [
10817 Point::new(0, 4)..Point::new(0, 4),
10818 Point::new(1, 4)..Point::new(1, 4),
10819 Point::new(2, 4)..Point::new(2, 4)
10820 ]
10821 );
10822
10823 editor.backspace(&Default::default(), window, cx);
10824 editor.backspace(&Default::default(), window, cx);
10825 assert_eq!(
10826 editor.text(cx),
10827 "
10828 a{}
10829 b{}
10830 c{}
10831 "
10832 .unindent()
10833 );
10834 assert_eq!(
10835 editor
10836 .selections
10837 .ranges::<Point>(&editor.display_snapshot(cx)),
10838 [
10839 Point::new(0, 2)..Point::new(0, 2),
10840 Point::new(1, 2)..Point::new(1, 2),
10841 Point::new(2, 2)..Point::new(2, 2)
10842 ]
10843 );
10844
10845 editor.delete_to_previous_word_start(&Default::default(), window, cx);
10846 assert_eq!(
10847 editor.text(cx),
10848 "
10849 a
10850 b
10851 c
10852 "
10853 .unindent()
10854 );
10855 assert_eq!(
10856 editor
10857 .selections
10858 .ranges::<Point>(&editor.display_snapshot(cx)),
10859 [
10860 Point::new(0, 1)..Point::new(0, 1),
10861 Point::new(1, 1)..Point::new(1, 1),
10862 Point::new(2, 1)..Point::new(2, 1)
10863 ]
10864 );
10865 });
10866}
10867
10868#[gpui::test]
10869async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
10870 init_test(cx, |settings| {
10871 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
10872 });
10873
10874 let mut cx = EditorTestContext::new(cx).await;
10875
10876 let language = Arc::new(Language::new(
10877 LanguageConfig {
10878 brackets: BracketPairConfig {
10879 pairs: vec![
10880 BracketPair {
10881 start: "{".to_string(),
10882 end: "}".to_string(),
10883 close: true,
10884 surround: true,
10885 newline: true,
10886 },
10887 BracketPair {
10888 start: "(".to_string(),
10889 end: ")".to_string(),
10890 close: true,
10891 surround: true,
10892 newline: true,
10893 },
10894 BracketPair {
10895 start: "[".to_string(),
10896 end: "]".to_string(),
10897 close: false,
10898 surround: true,
10899 newline: true,
10900 },
10901 ],
10902 ..Default::default()
10903 },
10904 autoclose_before: "})]".to_string(),
10905 ..Default::default()
10906 },
10907 Some(tree_sitter_rust::LANGUAGE.into()),
10908 ));
10909
10910 cx.language_registry().add(language.clone());
10911 cx.update_buffer(|buffer, cx| {
10912 buffer.set_language(Some(language), cx);
10913 });
10914
10915 cx.set_state(
10916 &"
10917 {(ˇ)}
10918 [[ˇ]]
10919 {(ˇ)}
10920 "
10921 .unindent(),
10922 );
10923
10924 cx.update_editor(|editor, window, cx| {
10925 editor.backspace(&Default::default(), window, cx);
10926 editor.backspace(&Default::default(), window, cx);
10927 });
10928
10929 cx.assert_editor_state(
10930 &"
10931 ˇ
10932 ˇ]]
10933 ˇ
10934 "
10935 .unindent(),
10936 );
10937
10938 cx.update_editor(|editor, window, cx| {
10939 editor.handle_input("{", window, cx);
10940 editor.handle_input("{", window, cx);
10941 editor.move_right(&MoveRight, window, cx);
10942 editor.move_right(&MoveRight, window, cx);
10943 editor.move_left(&MoveLeft, window, cx);
10944 editor.move_left(&MoveLeft, window, cx);
10945 editor.backspace(&Default::default(), window, cx);
10946 });
10947
10948 cx.assert_editor_state(
10949 &"
10950 {ˇ}
10951 {ˇ}]]
10952 {ˇ}
10953 "
10954 .unindent(),
10955 );
10956
10957 cx.update_editor(|editor, window, cx| {
10958 editor.backspace(&Default::default(), window, cx);
10959 });
10960
10961 cx.assert_editor_state(
10962 &"
10963 ˇ
10964 ˇ]]
10965 ˇ
10966 "
10967 .unindent(),
10968 );
10969}
10970
10971#[gpui::test]
10972async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
10973 init_test(cx, |_| {});
10974
10975 let language = Arc::new(Language::new(
10976 LanguageConfig::default(),
10977 Some(tree_sitter_rust::LANGUAGE.into()),
10978 ));
10979
10980 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
10981 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
10982 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
10983 editor
10984 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
10985 .await;
10986
10987 editor.update_in(cx, |editor, window, cx| {
10988 editor.set_auto_replace_emoji_shortcode(true);
10989
10990 editor.handle_input("Hello ", window, cx);
10991 editor.handle_input(":wave", window, cx);
10992 assert_eq!(editor.text(cx), "Hello :wave".unindent());
10993
10994 editor.handle_input(":", window, cx);
10995 assert_eq!(editor.text(cx), "Hello 👋".unindent());
10996
10997 editor.handle_input(" :smile", window, cx);
10998 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
10999
11000 editor.handle_input(":", window, cx);
11001 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
11002
11003 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
11004 editor.handle_input(":wave", window, cx);
11005 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
11006
11007 editor.handle_input(":", window, cx);
11008 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
11009
11010 editor.handle_input(":1", window, cx);
11011 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
11012
11013 editor.handle_input(":", window, cx);
11014 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
11015
11016 // Ensure shortcode does not get replaced when it is part of a word
11017 editor.handle_input(" Test:wave", window, cx);
11018 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
11019
11020 editor.handle_input(":", window, cx);
11021 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
11022
11023 editor.set_auto_replace_emoji_shortcode(false);
11024
11025 // Ensure shortcode does not get replaced when auto replace is off
11026 editor.handle_input(" :wave", window, cx);
11027 assert_eq!(
11028 editor.text(cx),
11029 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
11030 );
11031
11032 editor.handle_input(":", window, cx);
11033 assert_eq!(
11034 editor.text(cx),
11035 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
11036 );
11037 });
11038}
11039
11040#[gpui::test]
11041async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
11042 init_test(cx, |_| {});
11043
11044 let (text, insertion_ranges) = marked_text_ranges(
11045 indoc! {"
11046 ˇ
11047 "},
11048 false,
11049 );
11050
11051 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11052 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11053
11054 _ = editor.update_in(cx, |editor, window, cx| {
11055 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
11056
11057 editor
11058 .insert_snippet(&insertion_ranges, snippet, window, cx)
11059 .unwrap();
11060
11061 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11062 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11063 assert_eq!(editor.text(cx), expected_text);
11064 assert_eq!(
11065 editor
11066 .selections
11067 .ranges::<usize>(&editor.display_snapshot(cx)),
11068 selection_ranges
11069 );
11070 }
11071
11072 assert(
11073 editor,
11074 cx,
11075 indoc! {"
11076 type «» =•
11077 "},
11078 );
11079
11080 assert!(editor.context_menu_visible(), "There should be a matches");
11081 });
11082}
11083
11084#[gpui::test]
11085async fn test_snippet_tabstop_navigation_with_placeholders(cx: &mut TestAppContext) {
11086 init_test(cx, |_| {});
11087
11088 fn assert_state(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
11089 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
11090 assert_eq!(editor.text(cx), expected_text);
11091 assert_eq!(
11092 editor
11093 .selections
11094 .ranges::<usize>(&editor.display_snapshot(cx)),
11095 selection_ranges
11096 );
11097 }
11098
11099 let (text, insertion_ranges) = marked_text_ranges(
11100 indoc! {"
11101 ˇ
11102 "},
11103 false,
11104 );
11105
11106 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
11107 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
11108
11109 _ = editor.update_in(cx, |editor, window, cx| {
11110 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2; $3").unwrap();
11111
11112 editor
11113 .insert_snippet(&insertion_ranges, snippet, window, cx)
11114 .unwrap();
11115
11116 assert_state(
11117 editor,
11118 cx,
11119 indoc! {"
11120 type «» = ;•
11121 "},
11122 );
11123
11124 assert!(
11125 editor.context_menu_visible(),
11126 "Context menu should be visible for placeholder choices"
11127 );
11128
11129 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11130
11131 assert_state(
11132 editor,
11133 cx,
11134 indoc! {"
11135 type = «»;•
11136 "},
11137 );
11138
11139 assert!(
11140 !editor.context_menu_visible(),
11141 "Context menu should be hidden after moving to next tabstop"
11142 );
11143
11144 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11145
11146 assert_state(
11147 editor,
11148 cx,
11149 indoc! {"
11150 type = ; ˇ
11151 "},
11152 );
11153
11154 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11155
11156 assert_state(
11157 editor,
11158 cx,
11159 indoc! {"
11160 type = ; ˇ
11161 "},
11162 );
11163 });
11164
11165 _ = editor.update_in(cx, |editor, window, cx| {
11166 editor.select_all(&SelectAll, window, cx);
11167 editor.backspace(&Backspace, window, cx);
11168
11169 let snippet = Snippet::parse("fn ${1|,foo,bar|} = ${2:value}; $3").unwrap();
11170 let insertion_ranges = editor
11171 .selections
11172 .all(&editor.display_snapshot(cx))
11173 .iter()
11174 .map(|s| s.range())
11175 .collect::<Vec<_>>();
11176
11177 editor
11178 .insert_snippet(&insertion_ranges, snippet, window, cx)
11179 .unwrap();
11180
11181 assert_state(editor, cx, "fn «» = value;•");
11182
11183 assert!(
11184 editor.context_menu_visible(),
11185 "Context menu should be visible for placeholder choices"
11186 );
11187
11188 editor.next_snippet_tabstop(&NextSnippetTabstop, window, cx);
11189
11190 assert_state(editor, cx, "fn = «valueˇ»;•");
11191
11192 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11193
11194 assert_state(editor, cx, "fn «» = value;•");
11195
11196 assert!(
11197 editor.context_menu_visible(),
11198 "Context menu should be visible again after returning to first tabstop"
11199 );
11200
11201 editor.previous_snippet_tabstop(&PreviousSnippetTabstop, window, cx);
11202
11203 assert_state(editor, cx, "fn «» = value;•");
11204 });
11205}
11206
11207#[gpui::test]
11208async fn test_snippets(cx: &mut TestAppContext) {
11209 init_test(cx, |_| {});
11210
11211 let mut cx = EditorTestContext::new(cx).await;
11212
11213 cx.set_state(indoc! {"
11214 a.ˇ b
11215 a.ˇ b
11216 a.ˇ b
11217 "});
11218
11219 cx.update_editor(|editor, window, cx| {
11220 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
11221 let insertion_ranges = editor
11222 .selections
11223 .all(&editor.display_snapshot(cx))
11224 .iter()
11225 .map(|s| s.range())
11226 .collect::<Vec<_>>();
11227 editor
11228 .insert_snippet(&insertion_ranges, snippet, window, cx)
11229 .unwrap();
11230 });
11231
11232 cx.assert_editor_state(indoc! {"
11233 a.f(«oneˇ», two, «threeˇ») b
11234 a.f(«oneˇ», two, «threeˇ») b
11235 a.f(«oneˇ», two, «threeˇ») b
11236 "});
11237
11238 // Can't move earlier than the first tab stop
11239 cx.update_editor(|editor, window, cx| {
11240 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11241 });
11242 cx.assert_editor_state(indoc! {"
11243 a.f(«oneˇ», two, «threeˇ») b
11244 a.f(«oneˇ», two, «threeˇ») b
11245 a.f(«oneˇ», two, «threeˇ») b
11246 "});
11247
11248 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11249 cx.assert_editor_state(indoc! {"
11250 a.f(one, «twoˇ», three) b
11251 a.f(one, «twoˇ», three) b
11252 a.f(one, «twoˇ», three) b
11253 "});
11254
11255 cx.update_editor(|editor, window, cx| assert!(editor.move_to_prev_snippet_tabstop(window, cx)));
11256 cx.assert_editor_state(indoc! {"
11257 a.f(«oneˇ», two, «threeˇ») b
11258 a.f(«oneˇ», two, «threeˇ») b
11259 a.f(«oneˇ», two, «threeˇ») b
11260 "});
11261
11262 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11263 cx.assert_editor_state(indoc! {"
11264 a.f(one, «twoˇ», three) b
11265 a.f(one, «twoˇ», three) b
11266 a.f(one, «twoˇ», three) b
11267 "});
11268 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11269 cx.assert_editor_state(indoc! {"
11270 a.f(one, two, three)ˇ b
11271 a.f(one, two, three)ˇ b
11272 a.f(one, two, three)ˇ b
11273 "});
11274
11275 // As soon as the last tab stop is reached, snippet state is gone
11276 cx.update_editor(|editor, window, cx| {
11277 assert!(!editor.move_to_prev_snippet_tabstop(window, cx))
11278 });
11279 cx.assert_editor_state(indoc! {"
11280 a.f(one, two, three)ˇ b
11281 a.f(one, two, three)ˇ b
11282 a.f(one, two, three)ˇ b
11283 "});
11284}
11285
11286#[gpui::test]
11287async fn test_snippet_indentation(cx: &mut TestAppContext) {
11288 init_test(cx, |_| {});
11289
11290 let mut cx = EditorTestContext::new(cx).await;
11291
11292 cx.update_editor(|editor, window, cx| {
11293 let snippet = Snippet::parse(indoc! {"
11294 /*
11295 * Multiline comment with leading indentation
11296 *
11297 * $1
11298 */
11299 $0"})
11300 .unwrap();
11301 let insertion_ranges = editor
11302 .selections
11303 .all(&editor.display_snapshot(cx))
11304 .iter()
11305 .map(|s| s.range())
11306 .collect::<Vec<_>>();
11307 editor
11308 .insert_snippet(&insertion_ranges, snippet, window, cx)
11309 .unwrap();
11310 });
11311
11312 cx.assert_editor_state(indoc! {"
11313 /*
11314 * Multiline comment with leading indentation
11315 *
11316 * ˇ
11317 */
11318 "});
11319
11320 cx.update_editor(|editor, window, cx| assert!(editor.move_to_next_snippet_tabstop(window, cx)));
11321 cx.assert_editor_state(indoc! {"
11322 /*
11323 * Multiline comment with leading indentation
11324 *
11325 *•
11326 */
11327 ˇ"});
11328}
11329
11330#[gpui::test]
11331async fn test_document_format_during_save(cx: &mut TestAppContext) {
11332 init_test(cx, |_| {});
11333
11334 let fs = FakeFs::new(cx.executor());
11335 fs.insert_file(path!("/file.rs"), Default::default()).await;
11336
11337 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
11338
11339 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11340 language_registry.add(rust_lang());
11341 let mut fake_servers = language_registry.register_fake_lsp(
11342 "Rust",
11343 FakeLspAdapter {
11344 capabilities: lsp::ServerCapabilities {
11345 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11346 ..Default::default()
11347 },
11348 ..Default::default()
11349 },
11350 );
11351
11352 let buffer = project
11353 .update(cx, |project, cx| {
11354 project.open_local_buffer(path!("/file.rs"), cx)
11355 })
11356 .await
11357 .unwrap();
11358
11359 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11360 let (editor, cx) = cx.add_window_view(|window, cx| {
11361 build_editor_with_project(project.clone(), buffer, window, cx)
11362 });
11363 editor.update_in(cx, |editor, window, cx| {
11364 editor.set_text("one\ntwo\nthree\n", window, cx)
11365 });
11366 assert!(cx.read(|cx| editor.is_dirty(cx)));
11367
11368 cx.executor().start_waiting();
11369 let fake_server = fake_servers.next().await.unwrap();
11370
11371 {
11372 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11373 move |params, _| async move {
11374 assert_eq!(
11375 params.text_document.uri,
11376 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11377 );
11378 assert_eq!(params.options.tab_size, 4);
11379 Ok(Some(vec![lsp::TextEdit::new(
11380 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11381 ", ".to_string(),
11382 )]))
11383 },
11384 );
11385 let save = editor
11386 .update_in(cx, |editor, window, cx| {
11387 editor.save(
11388 SaveOptions {
11389 format: true,
11390 autosave: false,
11391 },
11392 project.clone(),
11393 window,
11394 cx,
11395 )
11396 })
11397 .unwrap();
11398 cx.executor().start_waiting();
11399 save.await;
11400
11401 assert_eq!(
11402 editor.update(cx, |editor, cx| editor.text(cx)),
11403 "one, two\nthree\n"
11404 );
11405 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11406 }
11407
11408 {
11409 editor.update_in(cx, |editor, window, cx| {
11410 editor.set_text("one\ntwo\nthree\n", window, cx)
11411 });
11412 assert!(cx.read(|cx| editor.is_dirty(cx)));
11413
11414 // Ensure we can still save even if formatting hangs.
11415 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
11416 move |params, _| async move {
11417 assert_eq!(
11418 params.text_document.uri,
11419 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11420 );
11421 futures::future::pending::<()>().await;
11422 unreachable!()
11423 },
11424 );
11425 let save = editor
11426 .update_in(cx, |editor, window, cx| {
11427 editor.save(
11428 SaveOptions {
11429 format: true,
11430 autosave: false,
11431 },
11432 project.clone(),
11433 window,
11434 cx,
11435 )
11436 })
11437 .unwrap();
11438 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
11439 cx.executor().start_waiting();
11440 save.await;
11441 assert_eq!(
11442 editor.update(cx, |editor, cx| editor.text(cx)),
11443 "one\ntwo\nthree\n"
11444 );
11445 }
11446
11447 // Set rust language override and assert overridden tabsize is sent to language server
11448 update_test_language_settings(cx, |settings| {
11449 settings.languages.0.insert(
11450 "Rust".into(),
11451 LanguageSettingsContent {
11452 tab_size: NonZeroU32::new(8),
11453 ..Default::default()
11454 },
11455 );
11456 });
11457
11458 {
11459 editor.update_in(cx, |editor, window, cx| {
11460 editor.set_text("somehting_new\n", window, cx)
11461 });
11462 assert!(cx.read(|cx| editor.is_dirty(cx)));
11463 let _formatting_request_signal = fake_server
11464 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
11465 assert_eq!(
11466 params.text_document.uri,
11467 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
11468 );
11469 assert_eq!(params.options.tab_size, 8);
11470 Ok(Some(vec![]))
11471 });
11472 let save = editor
11473 .update_in(cx, |editor, window, cx| {
11474 editor.save(
11475 SaveOptions {
11476 format: true,
11477 autosave: false,
11478 },
11479 project.clone(),
11480 window,
11481 cx,
11482 )
11483 })
11484 .unwrap();
11485 cx.executor().start_waiting();
11486 save.await;
11487 }
11488}
11489
11490#[gpui::test]
11491async fn test_redo_after_noop_format(cx: &mut TestAppContext) {
11492 init_test(cx, |settings| {
11493 settings.defaults.ensure_final_newline_on_save = Some(false);
11494 });
11495
11496 let fs = FakeFs::new(cx.executor());
11497 fs.insert_file(path!("/file.txt"), "foo".into()).await;
11498
11499 let project = Project::test(fs, [path!("/file.txt").as_ref()], cx).await;
11500
11501 let buffer = project
11502 .update(cx, |project, cx| {
11503 project.open_local_buffer(path!("/file.txt"), cx)
11504 })
11505 .await
11506 .unwrap();
11507
11508 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11509 let (editor, cx) = cx.add_window_view(|window, cx| {
11510 build_editor_with_project(project.clone(), buffer, window, cx)
11511 });
11512 editor.update_in(cx, |editor, window, cx| {
11513 editor.change_selections(SelectionEffects::default(), window, cx, |s| {
11514 s.select_ranges([0..0])
11515 });
11516 });
11517 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11518
11519 editor.update_in(cx, |editor, window, cx| {
11520 editor.handle_input("\n", window, cx)
11521 });
11522 cx.run_until_parked();
11523 save(&editor, &project, cx).await;
11524 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11525
11526 editor.update_in(cx, |editor, window, cx| {
11527 editor.undo(&Default::default(), window, cx);
11528 });
11529 save(&editor, &project, cx).await;
11530 assert_eq!("foo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11531
11532 editor.update_in(cx, |editor, window, cx| {
11533 editor.redo(&Default::default(), window, cx);
11534 });
11535 cx.run_until_parked();
11536 assert_eq!("\nfoo", editor.read_with(cx, |editor, cx| editor.text(cx)));
11537
11538 async fn save(editor: &Entity<Editor>, project: &Entity<Project>, cx: &mut VisualTestContext) {
11539 let save = editor
11540 .update_in(cx, |editor, window, cx| {
11541 editor.save(
11542 SaveOptions {
11543 format: true,
11544 autosave: false,
11545 },
11546 project.clone(),
11547 window,
11548 cx,
11549 )
11550 })
11551 .unwrap();
11552 cx.executor().start_waiting();
11553 save.await;
11554 assert!(!cx.read(|cx| editor.is_dirty(cx)));
11555 }
11556}
11557
11558#[gpui::test]
11559async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
11560 init_test(cx, |_| {});
11561
11562 let cols = 4;
11563 let rows = 10;
11564 let sample_text_1 = sample_text(rows, cols, 'a');
11565 assert_eq!(
11566 sample_text_1,
11567 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
11568 );
11569 let sample_text_2 = sample_text(rows, cols, 'l');
11570 assert_eq!(
11571 sample_text_2,
11572 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
11573 );
11574 let sample_text_3 = sample_text(rows, cols, 'v');
11575 assert_eq!(
11576 sample_text_3,
11577 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
11578 );
11579
11580 let fs = FakeFs::new(cx.executor());
11581 fs.insert_tree(
11582 path!("/a"),
11583 json!({
11584 "main.rs": sample_text_1,
11585 "other.rs": sample_text_2,
11586 "lib.rs": sample_text_3,
11587 }),
11588 )
11589 .await;
11590
11591 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11592 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11593 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11594
11595 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11596 language_registry.add(rust_lang());
11597 let mut fake_servers = language_registry.register_fake_lsp(
11598 "Rust",
11599 FakeLspAdapter {
11600 capabilities: lsp::ServerCapabilities {
11601 document_formatting_provider: Some(lsp::OneOf::Left(true)),
11602 ..Default::default()
11603 },
11604 ..Default::default()
11605 },
11606 );
11607
11608 let worktree = project.update(cx, |project, cx| {
11609 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
11610 assert_eq!(worktrees.len(), 1);
11611 worktrees.pop().unwrap()
11612 });
11613 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11614
11615 let buffer_1 = project
11616 .update(cx, |project, cx| {
11617 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
11618 })
11619 .await
11620 .unwrap();
11621 let buffer_2 = project
11622 .update(cx, |project, cx| {
11623 project.open_buffer((worktree_id, rel_path("other.rs")), cx)
11624 })
11625 .await
11626 .unwrap();
11627 let buffer_3 = project
11628 .update(cx, |project, cx| {
11629 project.open_buffer((worktree_id, rel_path("lib.rs")), cx)
11630 })
11631 .await
11632 .unwrap();
11633
11634 let multi_buffer = cx.new(|cx| {
11635 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11636 multi_buffer.push_excerpts(
11637 buffer_1.clone(),
11638 [
11639 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11640 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11641 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11642 ],
11643 cx,
11644 );
11645 multi_buffer.push_excerpts(
11646 buffer_2.clone(),
11647 [
11648 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11649 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11650 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11651 ],
11652 cx,
11653 );
11654 multi_buffer.push_excerpts(
11655 buffer_3.clone(),
11656 [
11657 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
11658 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
11659 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
11660 ],
11661 cx,
11662 );
11663 multi_buffer
11664 });
11665 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
11666 Editor::new(
11667 EditorMode::full(),
11668 multi_buffer,
11669 Some(project.clone()),
11670 window,
11671 cx,
11672 )
11673 });
11674
11675 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11676 editor.change_selections(
11677 SelectionEffects::scroll(Autoscroll::Next),
11678 window,
11679 cx,
11680 |s| s.select_ranges(Some(1..2)),
11681 );
11682 editor.insert("|one|two|three|", window, cx);
11683 });
11684 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11685 multi_buffer_editor.update_in(cx, |editor, window, cx| {
11686 editor.change_selections(
11687 SelectionEffects::scroll(Autoscroll::Next),
11688 window,
11689 cx,
11690 |s| s.select_ranges(Some(60..70)),
11691 );
11692 editor.insert("|four|five|six|", window, cx);
11693 });
11694 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
11695
11696 // First two buffers should be edited, but not the third one.
11697 assert_eq!(
11698 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11699 "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}",
11700 );
11701 buffer_1.update(cx, |buffer, _| {
11702 assert!(buffer.is_dirty());
11703 assert_eq!(
11704 buffer.text(),
11705 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
11706 )
11707 });
11708 buffer_2.update(cx, |buffer, _| {
11709 assert!(buffer.is_dirty());
11710 assert_eq!(
11711 buffer.text(),
11712 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
11713 )
11714 });
11715 buffer_3.update(cx, |buffer, _| {
11716 assert!(!buffer.is_dirty());
11717 assert_eq!(buffer.text(), sample_text_3,)
11718 });
11719 cx.executor().run_until_parked();
11720
11721 cx.executor().start_waiting();
11722 let save = multi_buffer_editor
11723 .update_in(cx, |editor, window, cx| {
11724 editor.save(
11725 SaveOptions {
11726 format: true,
11727 autosave: false,
11728 },
11729 project.clone(),
11730 window,
11731 cx,
11732 )
11733 })
11734 .unwrap();
11735
11736 let fake_server = fake_servers.next().await.unwrap();
11737 fake_server
11738 .server
11739 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
11740 Ok(Some(vec![lsp::TextEdit::new(
11741 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
11742 format!("[{} formatted]", params.text_document.uri),
11743 )]))
11744 })
11745 .detach();
11746 save.await;
11747
11748 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
11749 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
11750 assert_eq!(
11751 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
11752 uri!(
11753 "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}"
11754 ),
11755 );
11756 buffer_1.update(cx, |buffer, _| {
11757 assert!(!buffer.is_dirty());
11758 assert_eq!(
11759 buffer.text(),
11760 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
11761 )
11762 });
11763 buffer_2.update(cx, |buffer, _| {
11764 assert!(!buffer.is_dirty());
11765 assert_eq!(
11766 buffer.text(),
11767 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
11768 )
11769 });
11770 buffer_3.update(cx, |buffer, _| {
11771 assert!(!buffer.is_dirty());
11772 assert_eq!(buffer.text(), sample_text_3,)
11773 });
11774}
11775
11776#[gpui::test]
11777async fn test_autosave_with_dirty_buffers(cx: &mut TestAppContext) {
11778 init_test(cx, |_| {});
11779
11780 let fs = FakeFs::new(cx.executor());
11781 fs.insert_tree(
11782 path!("/dir"),
11783 json!({
11784 "file1.rs": "fn main() { println!(\"hello\"); }",
11785 "file2.rs": "fn test() { println!(\"test\"); }",
11786 "file3.rs": "fn other() { println!(\"other\"); }\n",
11787 }),
11788 )
11789 .await;
11790
11791 let project = Project::test(fs.clone(), [path!("/dir").as_ref()], cx).await;
11792 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11793 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
11794
11795 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11796 language_registry.add(rust_lang());
11797
11798 let worktree = project.update(cx, |project, cx| project.worktrees(cx).next().unwrap());
11799 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
11800
11801 // Open three buffers
11802 let buffer_1 = project
11803 .update(cx, |project, cx| {
11804 project.open_buffer((worktree_id, rel_path("file1.rs")), cx)
11805 })
11806 .await
11807 .unwrap();
11808 let buffer_2 = project
11809 .update(cx, |project, cx| {
11810 project.open_buffer((worktree_id, rel_path("file2.rs")), cx)
11811 })
11812 .await
11813 .unwrap();
11814 let buffer_3 = project
11815 .update(cx, |project, cx| {
11816 project.open_buffer((worktree_id, rel_path("file3.rs")), cx)
11817 })
11818 .await
11819 .unwrap();
11820
11821 // Create a multi-buffer with all three buffers
11822 let multi_buffer = cx.new(|cx| {
11823 let mut multi_buffer = MultiBuffer::new(ReadWrite);
11824 multi_buffer.push_excerpts(
11825 buffer_1.clone(),
11826 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11827 cx,
11828 );
11829 multi_buffer.push_excerpts(
11830 buffer_2.clone(),
11831 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11832 cx,
11833 );
11834 multi_buffer.push_excerpts(
11835 buffer_3.clone(),
11836 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
11837 cx,
11838 );
11839 multi_buffer
11840 });
11841
11842 let editor = cx.new_window_entity(|window, cx| {
11843 Editor::new(
11844 EditorMode::full(),
11845 multi_buffer,
11846 Some(project.clone()),
11847 window,
11848 cx,
11849 )
11850 });
11851
11852 // Edit only the first buffer
11853 editor.update_in(cx, |editor, window, cx| {
11854 editor.change_selections(
11855 SelectionEffects::scroll(Autoscroll::Next),
11856 window,
11857 cx,
11858 |s| s.select_ranges(Some(10..10)),
11859 );
11860 editor.insert("// edited", window, cx);
11861 });
11862
11863 // Verify that only buffer 1 is dirty
11864 buffer_1.update(cx, |buffer, _| assert!(buffer.is_dirty()));
11865 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11866 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11867
11868 // Get write counts after file creation (files were created with initial content)
11869 // We expect each file to have been written once during creation
11870 let write_count_after_creation_1 = fs.write_count_for_path(path!("/dir/file1.rs"));
11871 let write_count_after_creation_2 = fs.write_count_for_path(path!("/dir/file2.rs"));
11872 let write_count_after_creation_3 = fs.write_count_for_path(path!("/dir/file3.rs"));
11873
11874 // Perform autosave
11875 let save_task = editor.update_in(cx, |editor, window, cx| {
11876 editor.save(
11877 SaveOptions {
11878 format: true,
11879 autosave: true,
11880 },
11881 project.clone(),
11882 window,
11883 cx,
11884 )
11885 });
11886 save_task.await.unwrap();
11887
11888 // Only the dirty buffer should have been saved
11889 assert_eq!(
11890 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11891 1,
11892 "Buffer 1 was dirty, so it should have been written once during autosave"
11893 );
11894 assert_eq!(
11895 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11896 0,
11897 "Buffer 2 was clean, so it should not have been written during autosave"
11898 );
11899 assert_eq!(
11900 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11901 0,
11902 "Buffer 3 was clean, so it should not have been written during autosave"
11903 );
11904
11905 // Verify buffer states after autosave
11906 buffer_1.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11907 buffer_2.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11908 buffer_3.update(cx, |buffer, _| assert!(!buffer.is_dirty()));
11909
11910 // Now perform a manual save (format = true)
11911 let save_task = editor.update_in(cx, |editor, window, cx| {
11912 editor.save(
11913 SaveOptions {
11914 format: true,
11915 autosave: false,
11916 },
11917 project.clone(),
11918 window,
11919 cx,
11920 )
11921 });
11922 save_task.await.unwrap();
11923
11924 // During manual save, clean buffers don't get written to disk
11925 // They just get did_save called for language server notifications
11926 assert_eq!(
11927 fs.write_count_for_path(path!("/dir/file1.rs")) - write_count_after_creation_1,
11928 1,
11929 "Buffer 1 should only have been written once total (during autosave, not manual save)"
11930 );
11931 assert_eq!(
11932 fs.write_count_for_path(path!("/dir/file2.rs")) - write_count_after_creation_2,
11933 0,
11934 "Buffer 2 should not have been written at all"
11935 );
11936 assert_eq!(
11937 fs.write_count_for_path(path!("/dir/file3.rs")) - write_count_after_creation_3,
11938 0,
11939 "Buffer 3 should not have been written at all"
11940 );
11941}
11942
11943async fn setup_range_format_test(
11944 cx: &mut TestAppContext,
11945) -> (
11946 Entity<Project>,
11947 Entity<Editor>,
11948 &mut gpui::VisualTestContext,
11949 lsp::FakeLanguageServer,
11950) {
11951 init_test(cx, |_| {});
11952
11953 let fs = FakeFs::new(cx.executor());
11954 fs.insert_file(path!("/file.rs"), Default::default()).await;
11955
11956 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
11957
11958 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11959 language_registry.add(rust_lang());
11960 let mut fake_servers = language_registry.register_fake_lsp(
11961 "Rust",
11962 FakeLspAdapter {
11963 capabilities: lsp::ServerCapabilities {
11964 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
11965 ..lsp::ServerCapabilities::default()
11966 },
11967 ..FakeLspAdapter::default()
11968 },
11969 );
11970
11971 let buffer = project
11972 .update(cx, |project, cx| {
11973 project.open_local_buffer(path!("/file.rs"), cx)
11974 })
11975 .await
11976 .unwrap();
11977
11978 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
11979 let (editor, cx) = cx.add_window_view(|window, cx| {
11980 build_editor_with_project(project.clone(), buffer, window, cx)
11981 });
11982
11983 cx.executor().start_waiting();
11984 let fake_server = fake_servers.next().await.unwrap();
11985
11986 (project, editor, cx, fake_server)
11987}
11988
11989#[gpui::test]
11990async fn test_range_format_on_save_success(cx: &mut TestAppContext) {
11991 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
11992
11993 editor.update_in(cx, |editor, window, cx| {
11994 editor.set_text("one\ntwo\nthree\n", window, cx)
11995 });
11996 assert!(cx.read(|cx| editor.is_dirty(cx)));
11997
11998 let save = editor
11999 .update_in(cx, |editor, window, cx| {
12000 editor.save(
12001 SaveOptions {
12002 format: true,
12003 autosave: false,
12004 },
12005 project.clone(),
12006 window,
12007 cx,
12008 )
12009 })
12010 .unwrap();
12011 fake_server
12012 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12013 assert_eq!(
12014 params.text_document.uri,
12015 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12016 );
12017 assert_eq!(params.options.tab_size, 4);
12018 Ok(Some(vec![lsp::TextEdit::new(
12019 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12020 ", ".to_string(),
12021 )]))
12022 })
12023 .next()
12024 .await;
12025 cx.executor().start_waiting();
12026 save.await;
12027 assert_eq!(
12028 editor.update(cx, |editor, cx| editor.text(cx)),
12029 "one, two\nthree\n"
12030 );
12031 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12032}
12033
12034#[gpui::test]
12035async fn test_range_format_on_save_timeout(cx: &mut TestAppContext) {
12036 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12037
12038 editor.update_in(cx, |editor, window, cx| {
12039 editor.set_text("one\ntwo\nthree\n", window, cx)
12040 });
12041 assert!(cx.read(|cx| editor.is_dirty(cx)));
12042
12043 // Test that save still works when formatting hangs
12044 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
12045 move |params, _| async move {
12046 assert_eq!(
12047 params.text_document.uri,
12048 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12049 );
12050 futures::future::pending::<()>().await;
12051 unreachable!()
12052 },
12053 );
12054 let save = editor
12055 .update_in(cx, |editor, window, cx| {
12056 editor.save(
12057 SaveOptions {
12058 format: true,
12059 autosave: false,
12060 },
12061 project.clone(),
12062 window,
12063 cx,
12064 )
12065 })
12066 .unwrap();
12067 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12068 cx.executor().start_waiting();
12069 save.await;
12070 assert_eq!(
12071 editor.update(cx, |editor, cx| editor.text(cx)),
12072 "one\ntwo\nthree\n"
12073 );
12074 assert!(!cx.read(|cx| editor.is_dirty(cx)));
12075}
12076
12077#[gpui::test]
12078async fn test_range_format_not_called_for_clean_buffer(cx: &mut TestAppContext) {
12079 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12080
12081 // Buffer starts clean, no formatting should be requested
12082 let save = editor
12083 .update_in(cx, |editor, window, cx| {
12084 editor.save(
12085 SaveOptions {
12086 format: false,
12087 autosave: false,
12088 },
12089 project.clone(),
12090 window,
12091 cx,
12092 )
12093 })
12094 .unwrap();
12095 let _pending_format_request = fake_server
12096 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
12097 panic!("Should not be invoked");
12098 })
12099 .next();
12100 cx.executor().start_waiting();
12101 save.await;
12102 cx.run_until_parked();
12103}
12104
12105#[gpui::test]
12106async fn test_range_format_respects_language_tab_size_override(cx: &mut TestAppContext) {
12107 let (project, editor, cx, fake_server) = setup_range_format_test(cx).await;
12108
12109 // Set Rust language override and assert overridden tabsize is sent to language server
12110 update_test_language_settings(cx, |settings| {
12111 settings.languages.0.insert(
12112 "Rust".into(),
12113 LanguageSettingsContent {
12114 tab_size: NonZeroU32::new(8),
12115 ..Default::default()
12116 },
12117 );
12118 });
12119
12120 editor.update_in(cx, |editor, window, cx| {
12121 editor.set_text("something_new\n", window, cx)
12122 });
12123 assert!(cx.read(|cx| editor.is_dirty(cx)));
12124 let save = editor
12125 .update_in(cx, |editor, window, cx| {
12126 editor.save(
12127 SaveOptions {
12128 format: true,
12129 autosave: false,
12130 },
12131 project.clone(),
12132 window,
12133 cx,
12134 )
12135 })
12136 .unwrap();
12137 fake_server
12138 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
12139 assert_eq!(
12140 params.text_document.uri,
12141 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12142 );
12143 assert_eq!(params.options.tab_size, 8);
12144 Ok(Some(Vec::new()))
12145 })
12146 .next()
12147 .await;
12148 save.await;
12149}
12150
12151#[gpui::test]
12152async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
12153 init_test(cx, |settings| {
12154 settings.defaults.formatter = Some(FormatterList::Single(Formatter::LanguageServer(
12155 settings::LanguageServerFormatterSpecifier::Current,
12156 )))
12157 });
12158
12159 let fs = FakeFs::new(cx.executor());
12160 fs.insert_file(path!("/file.rs"), Default::default()).await;
12161
12162 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12163
12164 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12165 language_registry.add(Arc::new(Language::new(
12166 LanguageConfig {
12167 name: "Rust".into(),
12168 matcher: LanguageMatcher {
12169 path_suffixes: vec!["rs".to_string()],
12170 ..Default::default()
12171 },
12172 ..LanguageConfig::default()
12173 },
12174 Some(tree_sitter_rust::LANGUAGE.into()),
12175 )));
12176 update_test_language_settings(cx, |settings| {
12177 // Enable Prettier formatting for the same buffer, and ensure
12178 // LSP is called instead of Prettier.
12179 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12180 });
12181 let mut fake_servers = language_registry.register_fake_lsp(
12182 "Rust",
12183 FakeLspAdapter {
12184 capabilities: lsp::ServerCapabilities {
12185 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12186 ..Default::default()
12187 },
12188 ..Default::default()
12189 },
12190 );
12191
12192 let buffer = project
12193 .update(cx, |project, cx| {
12194 project.open_local_buffer(path!("/file.rs"), cx)
12195 })
12196 .await
12197 .unwrap();
12198
12199 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12200 let (editor, cx) = cx.add_window_view(|window, cx| {
12201 build_editor_with_project(project.clone(), buffer, window, cx)
12202 });
12203 editor.update_in(cx, |editor, window, cx| {
12204 editor.set_text("one\ntwo\nthree\n", window, cx)
12205 });
12206
12207 cx.executor().start_waiting();
12208 let fake_server = fake_servers.next().await.unwrap();
12209
12210 let format = editor
12211 .update_in(cx, |editor, window, cx| {
12212 editor.perform_format(
12213 project.clone(),
12214 FormatTrigger::Manual,
12215 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12216 window,
12217 cx,
12218 )
12219 })
12220 .unwrap();
12221 fake_server
12222 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
12223 assert_eq!(
12224 params.text_document.uri,
12225 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12226 );
12227 assert_eq!(params.options.tab_size, 4);
12228 Ok(Some(vec![lsp::TextEdit::new(
12229 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
12230 ", ".to_string(),
12231 )]))
12232 })
12233 .next()
12234 .await;
12235 cx.executor().start_waiting();
12236 format.await;
12237 assert_eq!(
12238 editor.update(cx, |editor, cx| editor.text(cx)),
12239 "one, two\nthree\n"
12240 );
12241
12242 editor.update_in(cx, |editor, window, cx| {
12243 editor.set_text("one\ntwo\nthree\n", window, cx)
12244 });
12245 // Ensure we don't lock if formatting hangs.
12246 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12247 move |params, _| async move {
12248 assert_eq!(
12249 params.text_document.uri,
12250 lsp::Uri::from_file_path(path!("/file.rs")).unwrap()
12251 );
12252 futures::future::pending::<()>().await;
12253 unreachable!()
12254 },
12255 );
12256 let format = editor
12257 .update_in(cx, |editor, window, cx| {
12258 editor.perform_format(
12259 project,
12260 FormatTrigger::Manual,
12261 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12262 window,
12263 cx,
12264 )
12265 })
12266 .unwrap();
12267 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
12268 cx.executor().start_waiting();
12269 format.await;
12270 assert_eq!(
12271 editor.update(cx, |editor, cx| editor.text(cx)),
12272 "one\ntwo\nthree\n"
12273 );
12274}
12275
12276#[gpui::test]
12277async fn test_multiple_formatters(cx: &mut TestAppContext) {
12278 init_test(cx, |settings| {
12279 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
12280 settings.defaults.formatter = Some(FormatterList::Vec(vec![
12281 Formatter::LanguageServer(settings::LanguageServerFormatterSpecifier::Current),
12282 Formatter::CodeAction("code-action-1".into()),
12283 Formatter::CodeAction("code-action-2".into()),
12284 ]))
12285 });
12286
12287 let fs = FakeFs::new(cx.executor());
12288 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
12289 .await;
12290
12291 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12292 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12293 language_registry.add(rust_lang());
12294
12295 let mut fake_servers = language_registry.register_fake_lsp(
12296 "Rust",
12297 FakeLspAdapter {
12298 capabilities: lsp::ServerCapabilities {
12299 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12300 execute_command_provider: Some(lsp::ExecuteCommandOptions {
12301 commands: vec!["the-command-for-code-action-1".into()],
12302 ..Default::default()
12303 }),
12304 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12305 ..Default::default()
12306 },
12307 ..Default::default()
12308 },
12309 );
12310
12311 let buffer = project
12312 .update(cx, |project, cx| {
12313 project.open_local_buffer(path!("/file.rs"), cx)
12314 })
12315 .await
12316 .unwrap();
12317
12318 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12319 let (editor, cx) = cx.add_window_view(|window, cx| {
12320 build_editor_with_project(project.clone(), buffer, window, cx)
12321 });
12322
12323 cx.executor().start_waiting();
12324
12325 let fake_server = fake_servers.next().await.unwrap();
12326 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
12327 move |_params, _| async move {
12328 Ok(Some(vec![lsp::TextEdit::new(
12329 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12330 "applied-formatting\n".to_string(),
12331 )]))
12332 },
12333 );
12334 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12335 move |params, _| async move {
12336 let requested_code_actions = params.context.only.expect("Expected code action request");
12337 assert_eq!(requested_code_actions.len(), 1);
12338
12339 let uri = lsp::Uri::from_file_path(path!("/file.rs")).unwrap();
12340 let code_action = match requested_code_actions[0].as_str() {
12341 "code-action-1" => lsp::CodeAction {
12342 kind: Some("code-action-1".into()),
12343 edit: Some(lsp::WorkspaceEdit::new(
12344 [(
12345 uri,
12346 vec![lsp::TextEdit::new(
12347 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12348 "applied-code-action-1-edit\n".to_string(),
12349 )],
12350 )]
12351 .into_iter()
12352 .collect(),
12353 )),
12354 command: Some(lsp::Command {
12355 command: "the-command-for-code-action-1".into(),
12356 ..Default::default()
12357 }),
12358 ..Default::default()
12359 },
12360 "code-action-2" => lsp::CodeAction {
12361 kind: Some("code-action-2".into()),
12362 edit: Some(lsp::WorkspaceEdit::new(
12363 [(
12364 uri,
12365 vec![lsp::TextEdit::new(
12366 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
12367 "applied-code-action-2-edit\n".to_string(),
12368 )],
12369 )]
12370 .into_iter()
12371 .collect(),
12372 )),
12373 ..Default::default()
12374 },
12375 req => panic!("Unexpected code action request: {:?}", req),
12376 };
12377 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12378 code_action,
12379 )]))
12380 },
12381 );
12382
12383 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
12384 move |params, _| async move { Ok(params) }
12385 });
12386
12387 let command_lock = Arc::new(futures::lock::Mutex::new(()));
12388 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
12389 let fake = fake_server.clone();
12390 let lock = command_lock.clone();
12391 move |params, _| {
12392 assert_eq!(params.command, "the-command-for-code-action-1");
12393 let fake = fake.clone();
12394 let lock = lock.clone();
12395 async move {
12396 lock.lock().await;
12397 fake.server
12398 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
12399 label: None,
12400 edit: lsp::WorkspaceEdit {
12401 changes: Some(
12402 [(
12403 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
12404 vec![lsp::TextEdit {
12405 range: lsp::Range::new(
12406 lsp::Position::new(0, 0),
12407 lsp::Position::new(0, 0),
12408 ),
12409 new_text: "applied-code-action-1-command\n".into(),
12410 }],
12411 )]
12412 .into_iter()
12413 .collect(),
12414 ),
12415 ..Default::default()
12416 },
12417 })
12418 .await
12419 .into_response()
12420 .unwrap();
12421 Ok(Some(json!(null)))
12422 }
12423 }
12424 });
12425
12426 cx.executor().start_waiting();
12427 editor
12428 .update_in(cx, |editor, window, cx| {
12429 editor.perform_format(
12430 project.clone(),
12431 FormatTrigger::Manual,
12432 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12433 window,
12434 cx,
12435 )
12436 })
12437 .unwrap()
12438 .await;
12439 editor.update(cx, |editor, cx| {
12440 assert_eq!(
12441 editor.text(cx),
12442 r#"
12443 applied-code-action-2-edit
12444 applied-code-action-1-command
12445 applied-code-action-1-edit
12446 applied-formatting
12447 one
12448 two
12449 three
12450 "#
12451 .unindent()
12452 );
12453 });
12454
12455 editor.update_in(cx, |editor, window, cx| {
12456 editor.undo(&Default::default(), window, cx);
12457 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12458 });
12459
12460 // Perform a manual edit while waiting for an LSP command
12461 // that's being run as part of a formatting code action.
12462 let lock_guard = command_lock.lock().await;
12463 let format = editor
12464 .update_in(cx, |editor, window, cx| {
12465 editor.perform_format(
12466 project.clone(),
12467 FormatTrigger::Manual,
12468 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
12469 window,
12470 cx,
12471 )
12472 })
12473 .unwrap();
12474 cx.run_until_parked();
12475 editor.update(cx, |editor, cx| {
12476 assert_eq!(
12477 editor.text(cx),
12478 r#"
12479 applied-code-action-1-edit
12480 applied-formatting
12481 one
12482 two
12483 three
12484 "#
12485 .unindent()
12486 );
12487
12488 editor.buffer.update(cx, |buffer, cx| {
12489 let ix = buffer.len(cx);
12490 buffer.edit([(ix..ix, "edited\n")], None, cx);
12491 });
12492 });
12493
12494 // Allow the LSP command to proceed. Because the buffer was edited,
12495 // the second code action will not be run.
12496 drop(lock_guard);
12497 format.await;
12498 editor.update_in(cx, |editor, window, cx| {
12499 assert_eq!(
12500 editor.text(cx),
12501 r#"
12502 applied-code-action-1-command
12503 applied-code-action-1-edit
12504 applied-formatting
12505 one
12506 two
12507 three
12508 edited
12509 "#
12510 .unindent()
12511 );
12512
12513 // The manual edit is undone first, because it is the last thing the user did
12514 // (even though the command completed afterwards).
12515 editor.undo(&Default::default(), window, cx);
12516 assert_eq!(
12517 editor.text(cx),
12518 r#"
12519 applied-code-action-1-command
12520 applied-code-action-1-edit
12521 applied-formatting
12522 one
12523 two
12524 three
12525 "#
12526 .unindent()
12527 );
12528
12529 // All the formatting (including the command, which completed after the manual edit)
12530 // is undone together.
12531 editor.undo(&Default::default(), window, cx);
12532 assert_eq!(editor.text(cx), "one \ntwo \nthree");
12533 });
12534}
12535
12536#[gpui::test]
12537async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
12538 init_test(cx, |settings| {
12539 settings.defaults.formatter = Some(FormatterList::Vec(vec![Formatter::LanguageServer(
12540 settings::LanguageServerFormatterSpecifier::Current,
12541 )]))
12542 });
12543
12544 let fs = FakeFs::new(cx.executor());
12545 fs.insert_file(path!("/file.ts"), Default::default()).await;
12546
12547 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
12548
12549 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
12550 language_registry.add(Arc::new(Language::new(
12551 LanguageConfig {
12552 name: "TypeScript".into(),
12553 matcher: LanguageMatcher {
12554 path_suffixes: vec!["ts".to_string()],
12555 ..Default::default()
12556 },
12557 ..LanguageConfig::default()
12558 },
12559 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
12560 )));
12561 update_test_language_settings(cx, |settings| {
12562 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
12563 });
12564 let mut fake_servers = language_registry.register_fake_lsp(
12565 "TypeScript",
12566 FakeLspAdapter {
12567 capabilities: lsp::ServerCapabilities {
12568 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
12569 ..Default::default()
12570 },
12571 ..Default::default()
12572 },
12573 );
12574
12575 let buffer = project
12576 .update(cx, |project, cx| {
12577 project.open_local_buffer(path!("/file.ts"), cx)
12578 })
12579 .await
12580 .unwrap();
12581
12582 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12583 let (editor, cx) = cx.add_window_view(|window, cx| {
12584 build_editor_with_project(project.clone(), buffer, window, cx)
12585 });
12586 editor.update_in(cx, |editor, window, cx| {
12587 editor.set_text(
12588 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12589 window,
12590 cx,
12591 )
12592 });
12593
12594 cx.executor().start_waiting();
12595 let fake_server = fake_servers.next().await.unwrap();
12596
12597 let format = editor
12598 .update_in(cx, |editor, window, cx| {
12599 editor.perform_code_action_kind(
12600 project.clone(),
12601 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12602 window,
12603 cx,
12604 )
12605 })
12606 .unwrap();
12607 fake_server
12608 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
12609 assert_eq!(
12610 params.text_document.uri,
12611 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12612 );
12613 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
12614 lsp::CodeAction {
12615 title: "Organize Imports".to_string(),
12616 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
12617 edit: Some(lsp::WorkspaceEdit {
12618 changes: Some(
12619 [(
12620 params.text_document.uri.clone(),
12621 vec![lsp::TextEdit::new(
12622 lsp::Range::new(
12623 lsp::Position::new(1, 0),
12624 lsp::Position::new(2, 0),
12625 ),
12626 "".to_string(),
12627 )],
12628 )]
12629 .into_iter()
12630 .collect(),
12631 ),
12632 ..Default::default()
12633 }),
12634 ..Default::default()
12635 },
12636 )]))
12637 })
12638 .next()
12639 .await;
12640 cx.executor().start_waiting();
12641 format.await;
12642 assert_eq!(
12643 editor.update(cx, |editor, cx| editor.text(cx)),
12644 "import { a } from 'module';\n\nconst x = a;\n"
12645 );
12646
12647 editor.update_in(cx, |editor, window, cx| {
12648 editor.set_text(
12649 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
12650 window,
12651 cx,
12652 )
12653 });
12654 // Ensure we don't lock if code action hangs.
12655 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
12656 move |params, _| async move {
12657 assert_eq!(
12658 params.text_document.uri,
12659 lsp::Uri::from_file_path(path!("/file.ts")).unwrap()
12660 );
12661 futures::future::pending::<()>().await;
12662 unreachable!()
12663 },
12664 );
12665 let format = editor
12666 .update_in(cx, |editor, window, cx| {
12667 editor.perform_code_action_kind(
12668 project,
12669 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
12670 window,
12671 cx,
12672 )
12673 })
12674 .unwrap();
12675 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
12676 cx.executor().start_waiting();
12677 format.await;
12678 assert_eq!(
12679 editor.update(cx, |editor, cx| editor.text(cx)),
12680 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
12681 );
12682}
12683
12684#[gpui::test]
12685async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
12686 init_test(cx, |_| {});
12687
12688 let mut cx = EditorLspTestContext::new_rust(
12689 lsp::ServerCapabilities {
12690 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12691 ..Default::default()
12692 },
12693 cx,
12694 )
12695 .await;
12696
12697 cx.set_state(indoc! {"
12698 one.twoˇ
12699 "});
12700
12701 // The format request takes a long time. When it completes, it inserts
12702 // a newline and an indent before the `.`
12703 cx.lsp
12704 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
12705 let executor = cx.background_executor().clone();
12706 async move {
12707 executor.timer(Duration::from_millis(100)).await;
12708 Ok(Some(vec![lsp::TextEdit {
12709 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
12710 new_text: "\n ".into(),
12711 }]))
12712 }
12713 });
12714
12715 // Submit a format request.
12716 let format_1 = cx
12717 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12718 .unwrap();
12719 cx.executor().run_until_parked();
12720
12721 // Submit a second format request.
12722 let format_2 = cx
12723 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12724 .unwrap();
12725 cx.executor().run_until_parked();
12726
12727 // Wait for both format requests to complete
12728 cx.executor().advance_clock(Duration::from_millis(200));
12729 cx.executor().start_waiting();
12730 format_1.await.unwrap();
12731 cx.executor().start_waiting();
12732 format_2.await.unwrap();
12733
12734 // The formatting edits only happens once.
12735 cx.assert_editor_state(indoc! {"
12736 one
12737 .twoˇ
12738 "});
12739}
12740
12741#[gpui::test]
12742async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
12743 init_test(cx, |settings| {
12744 settings.defaults.formatter = Some(FormatterList::default())
12745 });
12746
12747 let mut cx = EditorLspTestContext::new_rust(
12748 lsp::ServerCapabilities {
12749 document_formatting_provider: Some(lsp::OneOf::Left(true)),
12750 ..Default::default()
12751 },
12752 cx,
12753 )
12754 .await;
12755
12756 // Record which buffer changes have been sent to the language server
12757 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
12758 cx.lsp
12759 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
12760 let buffer_changes = buffer_changes.clone();
12761 move |params, _| {
12762 buffer_changes.lock().extend(
12763 params
12764 .content_changes
12765 .into_iter()
12766 .map(|e| (e.range.unwrap(), e.text)),
12767 );
12768 }
12769 });
12770 // Handle formatting requests to the language server.
12771 cx.lsp
12772 .set_request_handler::<lsp::request::Formatting, _, _>({
12773 let buffer_changes = buffer_changes.clone();
12774 move |_, _| {
12775 let buffer_changes = buffer_changes.clone();
12776 // Insert blank lines between each line of the buffer.
12777 async move {
12778 // When formatting is requested, trailing whitespace has already been stripped,
12779 // and the trailing newline has already been added.
12780 assert_eq!(
12781 &buffer_changes.lock()[1..],
12782 &[
12783 (
12784 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
12785 "".into()
12786 ),
12787 (
12788 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
12789 "".into()
12790 ),
12791 (
12792 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
12793 "\n".into()
12794 ),
12795 ]
12796 );
12797
12798 Ok(Some(vec![
12799 lsp::TextEdit {
12800 range: lsp::Range::new(
12801 lsp::Position::new(1, 0),
12802 lsp::Position::new(1, 0),
12803 ),
12804 new_text: "\n".into(),
12805 },
12806 lsp::TextEdit {
12807 range: lsp::Range::new(
12808 lsp::Position::new(2, 0),
12809 lsp::Position::new(2, 0),
12810 ),
12811 new_text: "\n".into(),
12812 },
12813 ]))
12814 }
12815 }
12816 });
12817
12818 // Set up a buffer white some trailing whitespace and no trailing newline.
12819 cx.set_state(
12820 &[
12821 "one ", //
12822 "twoˇ", //
12823 "three ", //
12824 "four", //
12825 ]
12826 .join("\n"),
12827 );
12828 cx.run_until_parked();
12829
12830 // Submit a format request.
12831 let format = cx
12832 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
12833 .unwrap();
12834
12835 cx.run_until_parked();
12836 // After formatting the buffer, the trailing whitespace is stripped,
12837 // a newline is appended, and the edits provided by the language server
12838 // have been applied.
12839 format.await.unwrap();
12840
12841 cx.assert_editor_state(
12842 &[
12843 "one", //
12844 "", //
12845 "twoˇ", //
12846 "", //
12847 "three", //
12848 "four", //
12849 "", //
12850 ]
12851 .join("\n"),
12852 );
12853
12854 // Undoing the formatting undoes the trailing whitespace removal, the
12855 // trailing newline, and the LSP edits.
12856 cx.update_buffer(|buffer, cx| buffer.undo(cx));
12857 cx.assert_editor_state(
12858 &[
12859 "one ", //
12860 "twoˇ", //
12861 "three ", //
12862 "four", //
12863 ]
12864 .join("\n"),
12865 );
12866}
12867
12868#[gpui::test]
12869async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
12870 cx: &mut TestAppContext,
12871) {
12872 init_test(cx, |_| {});
12873
12874 cx.update(|cx| {
12875 cx.update_global::<SettingsStore, _>(|settings, cx| {
12876 settings.update_user_settings(cx, |settings| {
12877 settings.editor.auto_signature_help = Some(true);
12878 });
12879 });
12880 });
12881
12882 let mut cx = EditorLspTestContext::new_rust(
12883 lsp::ServerCapabilities {
12884 signature_help_provider: Some(lsp::SignatureHelpOptions {
12885 ..Default::default()
12886 }),
12887 ..Default::default()
12888 },
12889 cx,
12890 )
12891 .await;
12892
12893 let language = Language::new(
12894 LanguageConfig {
12895 name: "Rust".into(),
12896 brackets: BracketPairConfig {
12897 pairs: vec![
12898 BracketPair {
12899 start: "{".to_string(),
12900 end: "}".to_string(),
12901 close: true,
12902 surround: true,
12903 newline: true,
12904 },
12905 BracketPair {
12906 start: "(".to_string(),
12907 end: ")".to_string(),
12908 close: true,
12909 surround: true,
12910 newline: true,
12911 },
12912 BracketPair {
12913 start: "/*".to_string(),
12914 end: " */".to_string(),
12915 close: true,
12916 surround: true,
12917 newline: true,
12918 },
12919 BracketPair {
12920 start: "[".to_string(),
12921 end: "]".to_string(),
12922 close: false,
12923 surround: false,
12924 newline: true,
12925 },
12926 BracketPair {
12927 start: "\"".to_string(),
12928 end: "\"".to_string(),
12929 close: true,
12930 surround: true,
12931 newline: false,
12932 },
12933 BracketPair {
12934 start: "<".to_string(),
12935 end: ">".to_string(),
12936 close: false,
12937 surround: true,
12938 newline: true,
12939 },
12940 ],
12941 ..Default::default()
12942 },
12943 autoclose_before: "})]".to_string(),
12944 ..Default::default()
12945 },
12946 Some(tree_sitter_rust::LANGUAGE.into()),
12947 );
12948 let language = Arc::new(language);
12949
12950 cx.language_registry().add(language.clone());
12951 cx.update_buffer(|buffer, cx| {
12952 buffer.set_language(Some(language), cx);
12953 });
12954
12955 cx.set_state(
12956 &r#"
12957 fn main() {
12958 sampleˇ
12959 }
12960 "#
12961 .unindent(),
12962 );
12963
12964 cx.update_editor(|editor, window, cx| {
12965 editor.handle_input("(", window, cx);
12966 });
12967 cx.assert_editor_state(
12968 &"
12969 fn main() {
12970 sample(ˇ)
12971 }
12972 "
12973 .unindent(),
12974 );
12975
12976 let mocked_response = lsp::SignatureHelp {
12977 signatures: vec![lsp::SignatureInformation {
12978 label: "fn sample(param1: u8, param2: u8)".to_string(),
12979 documentation: None,
12980 parameters: Some(vec![
12981 lsp::ParameterInformation {
12982 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
12983 documentation: None,
12984 },
12985 lsp::ParameterInformation {
12986 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
12987 documentation: None,
12988 },
12989 ]),
12990 active_parameter: None,
12991 }],
12992 active_signature: Some(0),
12993 active_parameter: Some(0),
12994 };
12995 handle_signature_help_request(&mut cx, mocked_response).await;
12996
12997 cx.condition(|editor, _| editor.signature_help_state.is_shown())
12998 .await;
12999
13000 cx.editor(|editor, _, _| {
13001 let signature_help_state = editor.signature_help_state.popover().cloned();
13002 let signature = signature_help_state.unwrap();
13003 assert_eq!(
13004 signature.signatures[signature.current_signature].label,
13005 "fn sample(param1: u8, param2: u8)"
13006 );
13007 });
13008}
13009
13010#[gpui::test]
13011async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
13012 init_test(cx, |_| {});
13013
13014 cx.update(|cx| {
13015 cx.update_global::<SettingsStore, _>(|settings, cx| {
13016 settings.update_user_settings(cx, |settings| {
13017 settings.editor.auto_signature_help = Some(false);
13018 settings.editor.show_signature_help_after_edits = Some(false);
13019 });
13020 });
13021 });
13022
13023 let mut cx = EditorLspTestContext::new_rust(
13024 lsp::ServerCapabilities {
13025 signature_help_provider: Some(lsp::SignatureHelpOptions {
13026 ..Default::default()
13027 }),
13028 ..Default::default()
13029 },
13030 cx,
13031 )
13032 .await;
13033
13034 let language = Language::new(
13035 LanguageConfig {
13036 name: "Rust".into(),
13037 brackets: BracketPairConfig {
13038 pairs: vec![
13039 BracketPair {
13040 start: "{".to_string(),
13041 end: "}".to_string(),
13042 close: true,
13043 surround: true,
13044 newline: true,
13045 },
13046 BracketPair {
13047 start: "(".to_string(),
13048 end: ")".to_string(),
13049 close: true,
13050 surround: true,
13051 newline: true,
13052 },
13053 BracketPair {
13054 start: "/*".to_string(),
13055 end: " */".to_string(),
13056 close: true,
13057 surround: true,
13058 newline: true,
13059 },
13060 BracketPair {
13061 start: "[".to_string(),
13062 end: "]".to_string(),
13063 close: false,
13064 surround: false,
13065 newline: true,
13066 },
13067 BracketPair {
13068 start: "\"".to_string(),
13069 end: "\"".to_string(),
13070 close: true,
13071 surround: true,
13072 newline: false,
13073 },
13074 BracketPair {
13075 start: "<".to_string(),
13076 end: ">".to_string(),
13077 close: false,
13078 surround: true,
13079 newline: true,
13080 },
13081 ],
13082 ..Default::default()
13083 },
13084 autoclose_before: "})]".to_string(),
13085 ..Default::default()
13086 },
13087 Some(tree_sitter_rust::LANGUAGE.into()),
13088 );
13089 let language = Arc::new(language);
13090
13091 cx.language_registry().add(language.clone());
13092 cx.update_buffer(|buffer, cx| {
13093 buffer.set_language(Some(language), cx);
13094 });
13095
13096 // Ensure that signature_help is not called when no signature help is enabled.
13097 cx.set_state(
13098 &r#"
13099 fn main() {
13100 sampleˇ
13101 }
13102 "#
13103 .unindent(),
13104 );
13105 cx.update_editor(|editor, window, cx| {
13106 editor.handle_input("(", window, cx);
13107 });
13108 cx.assert_editor_state(
13109 &"
13110 fn main() {
13111 sample(ˇ)
13112 }
13113 "
13114 .unindent(),
13115 );
13116 cx.editor(|editor, _, _| {
13117 assert!(editor.signature_help_state.task().is_none());
13118 });
13119
13120 let mocked_response = lsp::SignatureHelp {
13121 signatures: vec![lsp::SignatureInformation {
13122 label: "fn sample(param1: u8, param2: u8)".to_string(),
13123 documentation: None,
13124 parameters: Some(vec![
13125 lsp::ParameterInformation {
13126 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13127 documentation: None,
13128 },
13129 lsp::ParameterInformation {
13130 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13131 documentation: None,
13132 },
13133 ]),
13134 active_parameter: None,
13135 }],
13136 active_signature: Some(0),
13137 active_parameter: Some(0),
13138 };
13139
13140 // Ensure that signature_help is called when enabled afte edits
13141 cx.update(|_, cx| {
13142 cx.update_global::<SettingsStore, _>(|settings, cx| {
13143 settings.update_user_settings(cx, |settings| {
13144 settings.editor.auto_signature_help = Some(false);
13145 settings.editor.show_signature_help_after_edits = Some(true);
13146 });
13147 });
13148 });
13149 cx.set_state(
13150 &r#"
13151 fn main() {
13152 sampleˇ
13153 }
13154 "#
13155 .unindent(),
13156 );
13157 cx.update_editor(|editor, window, cx| {
13158 editor.handle_input("(", window, cx);
13159 });
13160 cx.assert_editor_state(
13161 &"
13162 fn main() {
13163 sample(ˇ)
13164 }
13165 "
13166 .unindent(),
13167 );
13168 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13169 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13170 .await;
13171 cx.update_editor(|editor, _, _| {
13172 let signature_help_state = editor.signature_help_state.popover().cloned();
13173 assert!(signature_help_state.is_some());
13174 let signature = signature_help_state.unwrap();
13175 assert_eq!(
13176 signature.signatures[signature.current_signature].label,
13177 "fn sample(param1: u8, param2: u8)"
13178 );
13179 editor.signature_help_state = SignatureHelpState::default();
13180 });
13181
13182 // Ensure that signature_help is called when auto signature help override is enabled
13183 cx.update(|_, cx| {
13184 cx.update_global::<SettingsStore, _>(|settings, cx| {
13185 settings.update_user_settings(cx, |settings| {
13186 settings.editor.auto_signature_help = Some(true);
13187 settings.editor.show_signature_help_after_edits = Some(false);
13188 });
13189 });
13190 });
13191 cx.set_state(
13192 &r#"
13193 fn main() {
13194 sampleˇ
13195 }
13196 "#
13197 .unindent(),
13198 );
13199 cx.update_editor(|editor, window, cx| {
13200 editor.handle_input("(", window, cx);
13201 });
13202 cx.assert_editor_state(
13203 &"
13204 fn main() {
13205 sample(ˇ)
13206 }
13207 "
13208 .unindent(),
13209 );
13210 handle_signature_help_request(&mut cx, mocked_response).await;
13211 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13212 .await;
13213 cx.editor(|editor, _, _| {
13214 let signature_help_state = editor.signature_help_state.popover().cloned();
13215 assert!(signature_help_state.is_some());
13216 let signature = signature_help_state.unwrap();
13217 assert_eq!(
13218 signature.signatures[signature.current_signature].label,
13219 "fn sample(param1: u8, param2: u8)"
13220 );
13221 });
13222}
13223
13224#[gpui::test]
13225async fn test_signature_help(cx: &mut TestAppContext) {
13226 init_test(cx, |_| {});
13227 cx.update(|cx| {
13228 cx.update_global::<SettingsStore, _>(|settings, cx| {
13229 settings.update_user_settings(cx, |settings| {
13230 settings.editor.auto_signature_help = Some(true);
13231 });
13232 });
13233 });
13234
13235 let mut cx = EditorLspTestContext::new_rust(
13236 lsp::ServerCapabilities {
13237 signature_help_provider: Some(lsp::SignatureHelpOptions {
13238 ..Default::default()
13239 }),
13240 ..Default::default()
13241 },
13242 cx,
13243 )
13244 .await;
13245
13246 // A test that directly calls `show_signature_help`
13247 cx.update_editor(|editor, window, cx| {
13248 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13249 });
13250
13251 let mocked_response = lsp::SignatureHelp {
13252 signatures: vec![lsp::SignatureInformation {
13253 label: "fn sample(param1: u8, param2: u8)".to_string(),
13254 documentation: None,
13255 parameters: Some(vec![
13256 lsp::ParameterInformation {
13257 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13258 documentation: None,
13259 },
13260 lsp::ParameterInformation {
13261 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13262 documentation: None,
13263 },
13264 ]),
13265 active_parameter: None,
13266 }],
13267 active_signature: Some(0),
13268 active_parameter: Some(0),
13269 };
13270 handle_signature_help_request(&mut cx, mocked_response).await;
13271
13272 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13273 .await;
13274
13275 cx.editor(|editor, _, _| {
13276 let signature_help_state = editor.signature_help_state.popover().cloned();
13277 assert!(signature_help_state.is_some());
13278 let signature = signature_help_state.unwrap();
13279 assert_eq!(
13280 signature.signatures[signature.current_signature].label,
13281 "fn sample(param1: u8, param2: u8)"
13282 );
13283 });
13284
13285 // When exiting outside from inside the brackets, `signature_help` is closed.
13286 cx.set_state(indoc! {"
13287 fn main() {
13288 sample(ˇ);
13289 }
13290
13291 fn sample(param1: u8, param2: u8) {}
13292 "});
13293
13294 cx.update_editor(|editor, window, cx| {
13295 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13296 s.select_ranges([0..0])
13297 });
13298 });
13299
13300 let mocked_response = lsp::SignatureHelp {
13301 signatures: Vec::new(),
13302 active_signature: None,
13303 active_parameter: None,
13304 };
13305 handle_signature_help_request(&mut cx, mocked_response).await;
13306
13307 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13308 .await;
13309
13310 cx.editor(|editor, _, _| {
13311 assert!(!editor.signature_help_state.is_shown());
13312 });
13313
13314 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
13315 cx.set_state(indoc! {"
13316 fn main() {
13317 sample(ˇ);
13318 }
13319
13320 fn sample(param1: u8, param2: u8) {}
13321 "});
13322
13323 let mocked_response = lsp::SignatureHelp {
13324 signatures: vec![lsp::SignatureInformation {
13325 label: "fn sample(param1: u8, param2: u8)".to_string(),
13326 documentation: None,
13327 parameters: Some(vec![
13328 lsp::ParameterInformation {
13329 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13330 documentation: None,
13331 },
13332 lsp::ParameterInformation {
13333 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13334 documentation: None,
13335 },
13336 ]),
13337 active_parameter: None,
13338 }],
13339 active_signature: Some(0),
13340 active_parameter: Some(0),
13341 };
13342 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13343 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13344 .await;
13345 cx.editor(|editor, _, _| {
13346 assert!(editor.signature_help_state.is_shown());
13347 });
13348
13349 // Restore the popover with more parameter input
13350 cx.set_state(indoc! {"
13351 fn main() {
13352 sample(param1, param2ˇ);
13353 }
13354
13355 fn sample(param1: u8, param2: u8) {}
13356 "});
13357
13358 let mocked_response = lsp::SignatureHelp {
13359 signatures: vec![lsp::SignatureInformation {
13360 label: "fn sample(param1: u8, param2: u8)".to_string(),
13361 documentation: None,
13362 parameters: Some(vec![
13363 lsp::ParameterInformation {
13364 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13365 documentation: None,
13366 },
13367 lsp::ParameterInformation {
13368 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13369 documentation: None,
13370 },
13371 ]),
13372 active_parameter: None,
13373 }],
13374 active_signature: Some(0),
13375 active_parameter: Some(1),
13376 };
13377 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13378 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13379 .await;
13380
13381 // When selecting a range, the popover is gone.
13382 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
13383 cx.update_editor(|editor, window, cx| {
13384 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13385 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13386 })
13387 });
13388 cx.assert_editor_state(indoc! {"
13389 fn main() {
13390 sample(param1, «ˇparam2»);
13391 }
13392
13393 fn sample(param1: u8, param2: u8) {}
13394 "});
13395 cx.editor(|editor, _, _| {
13396 assert!(!editor.signature_help_state.is_shown());
13397 });
13398
13399 // When unselecting again, the popover is back if within the brackets.
13400 cx.update_editor(|editor, window, cx| {
13401 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13402 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13403 })
13404 });
13405 cx.assert_editor_state(indoc! {"
13406 fn main() {
13407 sample(param1, ˇparam2);
13408 }
13409
13410 fn sample(param1: u8, param2: u8) {}
13411 "});
13412 handle_signature_help_request(&mut cx, mocked_response).await;
13413 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13414 .await;
13415 cx.editor(|editor, _, _| {
13416 assert!(editor.signature_help_state.is_shown());
13417 });
13418
13419 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
13420 cx.update_editor(|editor, window, cx| {
13421 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13422 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
13423 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13424 })
13425 });
13426 cx.assert_editor_state(indoc! {"
13427 fn main() {
13428 sample(param1, ˇparam2);
13429 }
13430
13431 fn sample(param1: u8, param2: u8) {}
13432 "});
13433
13434 let mocked_response = lsp::SignatureHelp {
13435 signatures: vec![lsp::SignatureInformation {
13436 label: "fn sample(param1: u8, param2: u8)".to_string(),
13437 documentation: None,
13438 parameters: Some(vec![
13439 lsp::ParameterInformation {
13440 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
13441 documentation: None,
13442 },
13443 lsp::ParameterInformation {
13444 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
13445 documentation: None,
13446 },
13447 ]),
13448 active_parameter: None,
13449 }],
13450 active_signature: Some(0),
13451 active_parameter: Some(1),
13452 };
13453 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
13454 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13455 .await;
13456 cx.update_editor(|editor, _, cx| {
13457 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
13458 });
13459 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
13460 .await;
13461 cx.update_editor(|editor, window, cx| {
13462 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13463 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
13464 })
13465 });
13466 cx.assert_editor_state(indoc! {"
13467 fn main() {
13468 sample(param1, «ˇparam2»);
13469 }
13470
13471 fn sample(param1: u8, param2: u8) {}
13472 "});
13473 cx.update_editor(|editor, window, cx| {
13474 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
13475 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
13476 })
13477 });
13478 cx.assert_editor_state(indoc! {"
13479 fn main() {
13480 sample(param1, ˇparam2);
13481 }
13482
13483 fn sample(param1: u8, param2: u8) {}
13484 "});
13485 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
13486 .await;
13487}
13488
13489#[gpui::test]
13490async fn test_signature_help_multiple_signatures(cx: &mut TestAppContext) {
13491 init_test(cx, |_| {});
13492
13493 let mut cx = EditorLspTestContext::new_rust(
13494 lsp::ServerCapabilities {
13495 signature_help_provider: Some(lsp::SignatureHelpOptions {
13496 ..Default::default()
13497 }),
13498 ..Default::default()
13499 },
13500 cx,
13501 )
13502 .await;
13503
13504 cx.set_state(indoc! {"
13505 fn main() {
13506 overloadedˇ
13507 }
13508 "});
13509
13510 cx.update_editor(|editor, window, cx| {
13511 editor.handle_input("(", window, cx);
13512 editor.show_signature_help(&ShowSignatureHelp, window, cx);
13513 });
13514
13515 // Mock response with 3 signatures
13516 let mocked_response = lsp::SignatureHelp {
13517 signatures: vec![
13518 lsp::SignatureInformation {
13519 label: "fn overloaded(x: i32)".to_string(),
13520 documentation: None,
13521 parameters: Some(vec![lsp::ParameterInformation {
13522 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13523 documentation: None,
13524 }]),
13525 active_parameter: None,
13526 },
13527 lsp::SignatureInformation {
13528 label: "fn overloaded(x: i32, y: i32)".to_string(),
13529 documentation: None,
13530 parameters: Some(vec![
13531 lsp::ParameterInformation {
13532 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13533 documentation: None,
13534 },
13535 lsp::ParameterInformation {
13536 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13537 documentation: None,
13538 },
13539 ]),
13540 active_parameter: None,
13541 },
13542 lsp::SignatureInformation {
13543 label: "fn overloaded(x: i32, y: i32, z: i32)".to_string(),
13544 documentation: None,
13545 parameters: Some(vec![
13546 lsp::ParameterInformation {
13547 label: lsp::ParameterLabel::Simple("x: i32".to_string()),
13548 documentation: None,
13549 },
13550 lsp::ParameterInformation {
13551 label: lsp::ParameterLabel::Simple("y: i32".to_string()),
13552 documentation: None,
13553 },
13554 lsp::ParameterInformation {
13555 label: lsp::ParameterLabel::Simple("z: i32".to_string()),
13556 documentation: None,
13557 },
13558 ]),
13559 active_parameter: None,
13560 },
13561 ],
13562 active_signature: Some(1),
13563 active_parameter: Some(0),
13564 };
13565 handle_signature_help_request(&mut cx, mocked_response).await;
13566
13567 cx.condition(|editor, _| editor.signature_help_state.is_shown())
13568 .await;
13569
13570 // Verify we have multiple signatures and the right one is selected
13571 cx.editor(|editor, _, _| {
13572 let popover = editor.signature_help_state.popover().cloned().unwrap();
13573 assert_eq!(popover.signatures.len(), 3);
13574 // active_signature was 1, so that should be the current
13575 assert_eq!(popover.current_signature, 1);
13576 assert_eq!(popover.signatures[0].label, "fn overloaded(x: i32)");
13577 assert_eq!(popover.signatures[1].label, "fn overloaded(x: i32, y: i32)");
13578 assert_eq!(
13579 popover.signatures[2].label,
13580 "fn overloaded(x: i32, y: i32, z: i32)"
13581 );
13582 });
13583
13584 // Test navigation functionality
13585 cx.update_editor(|editor, window, cx| {
13586 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13587 });
13588
13589 cx.editor(|editor, _, _| {
13590 let popover = editor.signature_help_state.popover().cloned().unwrap();
13591 assert_eq!(popover.current_signature, 2);
13592 });
13593
13594 // Test wrap around
13595 cx.update_editor(|editor, window, cx| {
13596 editor.signature_help_next(&crate::SignatureHelpNext, window, cx);
13597 });
13598
13599 cx.editor(|editor, _, _| {
13600 let popover = editor.signature_help_state.popover().cloned().unwrap();
13601 assert_eq!(popover.current_signature, 0);
13602 });
13603
13604 // Test previous navigation
13605 cx.update_editor(|editor, window, cx| {
13606 editor.signature_help_prev(&crate::SignatureHelpPrevious, window, cx);
13607 });
13608
13609 cx.editor(|editor, _, _| {
13610 let popover = editor.signature_help_state.popover().cloned().unwrap();
13611 assert_eq!(popover.current_signature, 2);
13612 });
13613}
13614
13615#[gpui::test]
13616async fn test_completion_mode(cx: &mut TestAppContext) {
13617 init_test(cx, |_| {});
13618 let mut cx = EditorLspTestContext::new_rust(
13619 lsp::ServerCapabilities {
13620 completion_provider: Some(lsp::CompletionOptions {
13621 resolve_provider: Some(true),
13622 ..Default::default()
13623 }),
13624 ..Default::default()
13625 },
13626 cx,
13627 )
13628 .await;
13629
13630 struct Run {
13631 run_description: &'static str,
13632 initial_state: String,
13633 buffer_marked_text: String,
13634 completion_label: &'static str,
13635 completion_text: &'static str,
13636 expected_with_insert_mode: String,
13637 expected_with_replace_mode: String,
13638 expected_with_replace_subsequence_mode: String,
13639 expected_with_replace_suffix_mode: String,
13640 }
13641
13642 let runs = [
13643 Run {
13644 run_description: "Start of word matches completion text",
13645 initial_state: "before ediˇ after".into(),
13646 buffer_marked_text: "before <edi|> after".into(),
13647 completion_label: "editor",
13648 completion_text: "editor",
13649 expected_with_insert_mode: "before editorˇ after".into(),
13650 expected_with_replace_mode: "before editorˇ after".into(),
13651 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13652 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13653 },
13654 Run {
13655 run_description: "Accept same text at the middle of the word",
13656 initial_state: "before ediˇtor after".into(),
13657 buffer_marked_text: "before <edi|tor> after".into(),
13658 completion_label: "editor",
13659 completion_text: "editor",
13660 expected_with_insert_mode: "before editorˇtor after".into(),
13661 expected_with_replace_mode: "before editorˇ after".into(),
13662 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13663 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13664 },
13665 Run {
13666 run_description: "End of word matches completion text -- cursor at end",
13667 initial_state: "before torˇ after".into(),
13668 buffer_marked_text: "before <tor|> after".into(),
13669 completion_label: "editor",
13670 completion_text: "editor",
13671 expected_with_insert_mode: "before editorˇ after".into(),
13672 expected_with_replace_mode: "before editorˇ after".into(),
13673 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13674 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13675 },
13676 Run {
13677 run_description: "End of word matches completion text -- cursor at start",
13678 initial_state: "before ˇtor after".into(),
13679 buffer_marked_text: "before <|tor> after".into(),
13680 completion_label: "editor",
13681 completion_text: "editor",
13682 expected_with_insert_mode: "before editorˇtor after".into(),
13683 expected_with_replace_mode: "before editorˇ after".into(),
13684 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13685 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13686 },
13687 Run {
13688 run_description: "Prepend text containing whitespace",
13689 initial_state: "pˇfield: bool".into(),
13690 buffer_marked_text: "<p|field>: bool".into(),
13691 completion_label: "pub ",
13692 completion_text: "pub ",
13693 expected_with_insert_mode: "pub ˇfield: bool".into(),
13694 expected_with_replace_mode: "pub ˇ: bool".into(),
13695 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
13696 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
13697 },
13698 Run {
13699 run_description: "Add element to start of list",
13700 initial_state: "[element_ˇelement_2]".into(),
13701 buffer_marked_text: "[<element_|element_2>]".into(),
13702 completion_label: "element_1",
13703 completion_text: "element_1",
13704 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
13705 expected_with_replace_mode: "[element_1ˇ]".into(),
13706 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
13707 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
13708 },
13709 Run {
13710 run_description: "Add element to start of list -- first and second elements are equal",
13711 initial_state: "[elˇelement]".into(),
13712 buffer_marked_text: "[<el|element>]".into(),
13713 completion_label: "element",
13714 completion_text: "element",
13715 expected_with_insert_mode: "[elementˇelement]".into(),
13716 expected_with_replace_mode: "[elementˇ]".into(),
13717 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
13718 expected_with_replace_suffix_mode: "[elementˇ]".into(),
13719 },
13720 Run {
13721 run_description: "Ends with matching suffix",
13722 initial_state: "SubˇError".into(),
13723 buffer_marked_text: "<Sub|Error>".into(),
13724 completion_label: "SubscriptionError",
13725 completion_text: "SubscriptionError",
13726 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
13727 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13728 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13729 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
13730 },
13731 Run {
13732 run_description: "Suffix is a subsequence -- contiguous",
13733 initial_state: "SubˇErr".into(),
13734 buffer_marked_text: "<Sub|Err>".into(),
13735 completion_label: "SubscriptionError",
13736 completion_text: "SubscriptionError",
13737 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
13738 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13739 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13740 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
13741 },
13742 Run {
13743 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
13744 initial_state: "Suˇscrirr".into(),
13745 buffer_marked_text: "<Su|scrirr>".into(),
13746 completion_label: "SubscriptionError",
13747 completion_text: "SubscriptionError",
13748 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
13749 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
13750 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
13751 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
13752 },
13753 Run {
13754 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
13755 initial_state: "foo(indˇix)".into(),
13756 buffer_marked_text: "foo(<ind|ix>)".into(),
13757 completion_label: "node_index",
13758 completion_text: "node_index",
13759 expected_with_insert_mode: "foo(node_indexˇix)".into(),
13760 expected_with_replace_mode: "foo(node_indexˇ)".into(),
13761 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
13762 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
13763 },
13764 Run {
13765 run_description: "Replace range ends before cursor - should extend to cursor",
13766 initial_state: "before editˇo after".into(),
13767 buffer_marked_text: "before <{ed}>it|o after".into(),
13768 completion_label: "editor",
13769 completion_text: "editor",
13770 expected_with_insert_mode: "before editorˇo after".into(),
13771 expected_with_replace_mode: "before editorˇo after".into(),
13772 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
13773 expected_with_replace_suffix_mode: "before editorˇo after".into(),
13774 },
13775 Run {
13776 run_description: "Uses label for suffix matching",
13777 initial_state: "before ediˇtor after".into(),
13778 buffer_marked_text: "before <edi|tor> after".into(),
13779 completion_label: "editor",
13780 completion_text: "editor()",
13781 expected_with_insert_mode: "before editor()ˇtor after".into(),
13782 expected_with_replace_mode: "before editor()ˇ after".into(),
13783 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
13784 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
13785 },
13786 Run {
13787 run_description: "Case insensitive subsequence and suffix matching",
13788 initial_state: "before EDiˇtoR after".into(),
13789 buffer_marked_text: "before <EDi|toR> after".into(),
13790 completion_label: "editor",
13791 completion_text: "editor",
13792 expected_with_insert_mode: "before editorˇtoR after".into(),
13793 expected_with_replace_mode: "before editorˇ after".into(),
13794 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
13795 expected_with_replace_suffix_mode: "before editorˇ after".into(),
13796 },
13797 ];
13798
13799 for run in runs {
13800 let run_variations = [
13801 (LspInsertMode::Insert, run.expected_with_insert_mode),
13802 (LspInsertMode::Replace, run.expected_with_replace_mode),
13803 (
13804 LspInsertMode::ReplaceSubsequence,
13805 run.expected_with_replace_subsequence_mode,
13806 ),
13807 (
13808 LspInsertMode::ReplaceSuffix,
13809 run.expected_with_replace_suffix_mode,
13810 ),
13811 ];
13812
13813 for (lsp_insert_mode, expected_text) in run_variations {
13814 eprintln!(
13815 "run = {:?}, mode = {lsp_insert_mode:.?}",
13816 run.run_description,
13817 );
13818
13819 update_test_language_settings(&mut cx, |settings| {
13820 settings.defaults.completions = Some(CompletionSettingsContent {
13821 lsp_insert_mode: Some(lsp_insert_mode),
13822 words: Some(WordsCompletionMode::Disabled),
13823 words_min_length: Some(0),
13824 ..Default::default()
13825 });
13826 });
13827
13828 cx.set_state(&run.initial_state);
13829 cx.update_editor(|editor, window, cx| {
13830 editor.show_completions(&ShowCompletions, window, cx);
13831 });
13832
13833 let counter = Arc::new(AtomicUsize::new(0));
13834 handle_completion_request_with_insert_and_replace(
13835 &mut cx,
13836 &run.buffer_marked_text,
13837 vec![(run.completion_label, run.completion_text)],
13838 counter.clone(),
13839 )
13840 .await;
13841 cx.condition(|editor, _| editor.context_menu_visible())
13842 .await;
13843 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13844
13845 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13846 editor
13847 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13848 .unwrap()
13849 });
13850 cx.assert_editor_state(&expected_text);
13851 handle_resolve_completion_request(&mut cx, None).await;
13852 apply_additional_edits.await.unwrap();
13853 }
13854 }
13855}
13856
13857#[gpui::test]
13858async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
13859 init_test(cx, |_| {});
13860 let mut cx = EditorLspTestContext::new_rust(
13861 lsp::ServerCapabilities {
13862 completion_provider: Some(lsp::CompletionOptions {
13863 resolve_provider: Some(true),
13864 ..Default::default()
13865 }),
13866 ..Default::default()
13867 },
13868 cx,
13869 )
13870 .await;
13871
13872 let initial_state = "SubˇError";
13873 let buffer_marked_text = "<Sub|Error>";
13874 let completion_text = "SubscriptionError";
13875 let expected_with_insert_mode = "SubscriptionErrorˇError";
13876 let expected_with_replace_mode = "SubscriptionErrorˇ";
13877
13878 update_test_language_settings(&mut cx, |settings| {
13879 settings.defaults.completions = Some(CompletionSettingsContent {
13880 words: Some(WordsCompletionMode::Disabled),
13881 words_min_length: Some(0),
13882 // set the opposite here to ensure that the action is overriding the default behavior
13883 lsp_insert_mode: Some(LspInsertMode::Insert),
13884 ..Default::default()
13885 });
13886 });
13887
13888 cx.set_state(initial_state);
13889 cx.update_editor(|editor, window, cx| {
13890 editor.show_completions(&ShowCompletions, window, cx);
13891 });
13892
13893 let counter = Arc::new(AtomicUsize::new(0));
13894 handle_completion_request_with_insert_and_replace(
13895 &mut cx,
13896 buffer_marked_text,
13897 vec![(completion_text, completion_text)],
13898 counter.clone(),
13899 )
13900 .await;
13901 cx.condition(|editor, _| editor.context_menu_visible())
13902 .await;
13903 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
13904
13905 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13906 editor
13907 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
13908 .unwrap()
13909 });
13910 cx.assert_editor_state(expected_with_replace_mode);
13911 handle_resolve_completion_request(&mut cx, None).await;
13912 apply_additional_edits.await.unwrap();
13913
13914 update_test_language_settings(&mut cx, |settings| {
13915 settings.defaults.completions = Some(CompletionSettingsContent {
13916 words: Some(WordsCompletionMode::Disabled),
13917 words_min_length: Some(0),
13918 // set the opposite here to ensure that the action is overriding the default behavior
13919 lsp_insert_mode: Some(LspInsertMode::Replace),
13920 ..Default::default()
13921 });
13922 });
13923
13924 cx.set_state(initial_state);
13925 cx.update_editor(|editor, window, cx| {
13926 editor.show_completions(&ShowCompletions, window, cx);
13927 });
13928 handle_completion_request_with_insert_and_replace(
13929 &mut cx,
13930 buffer_marked_text,
13931 vec![(completion_text, completion_text)],
13932 counter.clone(),
13933 )
13934 .await;
13935 cx.condition(|editor, _| editor.context_menu_visible())
13936 .await;
13937 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
13938
13939 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13940 editor
13941 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
13942 .unwrap()
13943 });
13944 cx.assert_editor_state(expected_with_insert_mode);
13945 handle_resolve_completion_request(&mut cx, None).await;
13946 apply_additional_edits.await.unwrap();
13947}
13948
13949#[gpui::test]
13950async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
13951 init_test(cx, |_| {});
13952 let mut cx = EditorLspTestContext::new_rust(
13953 lsp::ServerCapabilities {
13954 completion_provider: Some(lsp::CompletionOptions {
13955 resolve_provider: Some(true),
13956 ..Default::default()
13957 }),
13958 ..Default::default()
13959 },
13960 cx,
13961 )
13962 .await;
13963
13964 // scenario: surrounding text matches completion text
13965 let completion_text = "to_offset";
13966 let initial_state = indoc! {"
13967 1. buf.to_offˇsuffix
13968 2. buf.to_offˇsuf
13969 3. buf.to_offˇfix
13970 4. buf.to_offˇ
13971 5. into_offˇensive
13972 6. ˇsuffix
13973 7. let ˇ //
13974 8. aaˇzz
13975 9. buf.to_off«zzzzzˇ»suffix
13976 10. buf.«ˇzzzzz»suffix
13977 11. to_off«ˇzzzzz»
13978
13979 buf.to_offˇsuffix // newest cursor
13980 "};
13981 let completion_marked_buffer = indoc! {"
13982 1. buf.to_offsuffix
13983 2. buf.to_offsuf
13984 3. buf.to_offfix
13985 4. buf.to_off
13986 5. into_offensive
13987 6. suffix
13988 7. let //
13989 8. aazz
13990 9. buf.to_offzzzzzsuffix
13991 10. buf.zzzzzsuffix
13992 11. to_offzzzzz
13993
13994 buf.<to_off|suffix> // newest cursor
13995 "};
13996 let expected = indoc! {"
13997 1. buf.to_offsetˇ
13998 2. buf.to_offsetˇsuf
13999 3. buf.to_offsetˇfix
14000 4. buf.to_offsetˇ
14001 5. into_offsetˇensive
14002 6. to_offsetˇsuffix
14003 7. let to_offsetˇ //
14004 8. aato_offsetˇzz
14005 9. buf.to_offsetˇ
14006 10. buf.to_offsetˇsuffix
14007 11. to_offsetˇ
14008
14009 buf.to_offsetˇ // newest cursor
14010 "};
14011 cx.set_state(initial_state);
14012 cx.update_editor(|editor, window, cx| {
14013 editor.show_completions(&ShowCompletions, window, cx);
14014 });
14015 handle_completion_request_with_insert_and_replace(
14016 &mut cx,
14017 completion_marked_buffer,
14018 vec![(completion_text, completion_text)],
14019 Arc::new(AtomicUsize::new(0)),
14020 )
14021 .await;
14022 cx.condition(|editor, _| editor.context_menu_visible())
14023 .await;
14024 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14025 editor
14026 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14027 .unwrap()
14028 });
14029 cx.assert_editor_state(expected);
14030 handle_resolve_completion_request(&mut cx, None).await;
14031 apply_additional_edits.await.unwrap();
14032
14033 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
14034 let completion_text = "foo_and_bar";
14035 let initial_state = indoc! {"
14036 1. ooanbˇ
14037 2. zooanbˇ
14038 3. ooanbˇz
14039 4. zooanbˇz
14040 5. ooanˇ
14041 6. oanbˇ
14042
14043 ooanbˇ
14044 "};
14045 let completion_marked_buffer = indoc! {"
14046 1. ooanb
14047 2. zooanb
14048 3. ooanbz
14049 4. zooanbz
14050 5. ooan
14051 6. oanb
14052
14053 <ooanb|>
14054 "};
14055 let expected = indoc! {"
14056 1. foo_and_barˇ
14057 2. zfoo_and_barˇ
14058 3. foo_and_barˇz
14059 4. zfoo_and_barˇz
14060 5. ooanfoo_and_barˇ
14061 6. oanbfoo_and_barˇ
14062
14063 foo_and_barˇ
14064 "};
14065 cx.set_state(initial_state);
14066 cx.update_editor(|editor, window, cx| {
14067 editor.show_completions(&ShowCompletions, window, cx);
14068 });
14069 handle_completion_request_with_insert_and_replace(
14070 &mut cx,
14071 completion_marked_buffer,
14072 vec![(completion_text, completion_text)],
14073 Arc::new(AtomicUsize::new(0)),
14074 )
14075 .await;
14076 cx.condition(|editor, _| editor.context_menu_visible())
14077 .await;
14078 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14079 editor
14080 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14081 .unwrap()
14082 });
14083 cx.assert_editor_state(expected);
14084 handle_resolve_completion_request(&mut cx, None).await;
14085 apply_additional_edits.await.unwrap();
14086
14087 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
14088 // (expects the same as if it was inserted at the end)
14089 let completion_text = "foo_and_bar";
14090 let initial_state = indoc! {"
14091 1. ooˇanb
14092 2. zooˇanb
14093 3. ooˇanbz
14094 4. zooˇanbz
14095
14096 ooˇanb
14097 "};
14098 let completion_marked_buffer = indoc! {"
14099 1. ooanb
14100 2. zooanb
14101 3. ooanbz
14102 4. zooanbz
14103
14104 <oo|anb>
14105 "};
14106 let expected = indoc! {"
14107 1. foo_and_barˇ
14108 2. zfoo_and_barˇ
14109 3. foo_and_barˇz
14110 4. zfoo_and_barˇz
14111
14112 foo_and_barˇ
14113 "};
14114 cx.set_state(initial_state);
14115 cx.update_editor(|editor, window, cx| {
14116 editor.show_completions(&ShowCompletions, window, cx);
14117 });
14118 handle_completion_request_with_insert_and_replace(
14119 &mut cx,
14120 completion_marked_buffer,
14121 vec![(completion_text, completion_text)],
14122 Arc::new(AtomicUsize::new(0)),
14123 )
14124 .await;
14125 cx.condition(|editor, _| editor.context_menu_visible())
14126 .await;
14127 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14128 editor
14129 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14130 .unwrap()
14131 });
14132 cx.assert_editor_state(expected);
14133 handle_resolve_completion_request(&mut cx, None).await;
14134 apply_additional_edits.await.unwrap();
14135}
14136
14137// This used to crash
14138#[gpui::test]
14139async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
14140 init_test(cx, |_| {});
14141
14142 let buffer_text = indoc! {"
14143 fn main() {
14144 10.satu;
14145
14146 //
14147 // separate cursors so they open in different excerpts (manually reproducible)
14148 //
14149
14150 10.satu20;
14151 }
14152 "};
14153 let multibuffer_text_with_selections = indoc! {"
14154 fn main() {
14155 10.satuˇ;
14156
14157 //
14158
14159 //
14160
14161 10.satuˇ20;
14162 }
14163 "};
14164 let expected_multibuffer = indoc! {"
14165 fn main() {
14166 10.saturating_sub()ˇ;
14167
14168 //
14169
14170 //
14171
14172 10.saturating_sub()ˇ;
14173 }
14174 "};
14175
14176 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
14177 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
14178
14179 let fs = FakeFs::new(cx.executor());
14180 fs.insert_tree(
14181 path!("/a"),
14182 json!({
14183 "main.rs": buffer_text,
14184 }),
14185 )
14186 .await;
14187
14188 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
14189 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14190 language_registry.add(rust_lang());
14191 let mut fake_servers = language_registry.register_fake_lsp(
14192 "Rust",
14193 FakeLspAdapter {
14194 capabilities: lsp::ServerCapabilities {
14195 completion_provider: Some(lsp::CompletionOptions {
14196 resolve_provider: None,
14197 ..lsp::CompletionOptions::default()
14198 }),
14199 ..lsp::ServerCapabilities::default()
14200 },
14201 ..FakeLspAdapter::default()
14202 },
14203 );
14204 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14205 let cx = &mut VisualTestContext::from_window(*workspace, cx);
14206 let buffer = project
14207 .update(cx, |project, cx| {
14208 project.open_local_buffer(path!("/a/main.rs"), cx)
14209 })
14210 .await
14211 .unwrap();
14212
14213 let multi_buffer = cx.new(|cx| {
14214 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
14215 multi_buffer.push_excerpts(
14216 buffer.clone(),
14217 [ExcerptRange::new(0..first_excerpt_end)],
14218 cx,
14219 );
14220 multi_buffer.push_excerpts(
14221 buffer.clone(),
14222 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
14223 cx,
14224 );
14225 multi_buffer
14226 });
14227
14228 let editor = workspace
14229 .update(cx, |_, window, cx| {
14230 cx.new(|cx| {
14231 Editor::new(
14232 EditorMode::Full {
14233 scale_ui_elements_with_buffer_font_size: false,
14234 show_active_line_background: false,
14235 sizing_behavior: SizingBehavior::Default,
14236 },
14237 multi_buffer.clone(),
14238 Some(project.clone()),
14239 window,
14240 cx,
14241 )
14242 })
14243 })
14244 .unwrap();
14245
14246 let pane = workspace
14247 .update(cx, |workspace, _, _| workspace.active_pane().clone())
14248 .unwrap();
14249 pane.update_in(cx, |pane, window, cx| {
14250 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
14251 });
14252
14253 let fake_server = fake_servers.next().await.unwrap();
14254
14255 editor.update_in(cx, |editor, window, cx| {
14256 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
14257 s.select_ranges([
14258 Point::new(1, 11)..Point::new(1, 11),
14259 Point::new(7, 11)..Point::new(7, 11),
14260 ])
14261 });
14262
14263 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
14264 });
14265
14266 editor.update_in(cx, |editor, window, cx| {
14267 editor.show_completions(&ShowCompletions, window, cx);
14268 });
14269
14270 fake_server
14271 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14272 let completion_item = lsp::CompletionItem {
14273 label: "saturating_sub()".into(),
14274 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14275 lsp::InsertReplaceEdit {
14276 new_text: "saturating_sub()".to_owned(),
14277 insert: lsp::Range::new(
14278 lsp::Position::new(7, 7),
14279 lsp::Position::new(7, 11),
14280 ),
14281 replace: lsp::Range::new(
14282 lsp::Position::new(7, 7),
14283 lsp::Position::new(7, 13),
14284 ),
14285 },
14286 )),
14287 ..lsp::CompletionItem::default()
14288 };
14289
14290 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
14291 })
14292 .next()
14293 .await
14294 .unwrap();
14295
14296 cx.condition(&editor, |editor, _| editor.context_menu_visible())
14297 .await;
14298
14299 editor
14300 .update_in(cx, |editor, window, cx| {
14301 editor
14302 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
14303 .unwrap()
14304 })
14305 .await
14306 .unwrap();
14307
14308 editor.update(cx, |editor, cx| {
14309 assert_text_with_selections(editor, expected_multibuffer, cx);
14310 })
14311}
14312
14313#[gpui::test]
14314async fn test_completion(cx: &mut TestAppContext) {
14315 init_test(cx, |_| {});
14316
14317 let mut cx = EditorLspTestContext::new_rust(
14318 lsp::ServerCapabilities {
14319 completion_provider: Some(lsp::CompletionOptions {
14320 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14321 resolve_provider: Some(true),
14322 ..Default::default()
14323 }),
14324 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14325 ..Default::default()
14326 },
14327 cx,
14328 )
14329 .await;
14330 let counter = Arc::new(AtomicUsize::new(0));
14331
14332 cx.set_state(indoc! {"
14333 oneˇ
14334 two
14335 three
14336 "});
14337 cx.simulate_keystroke(".");
14338 handle_completion_request(
14339 indoc! {"
14340 one.|<>
14341 two
14342 three
14343 "},
14344 vec!["first_completion", "second_completion"],
14345 true,
14346 counter.clone(),
14347 &mut cx,
14348 )
14349 .await;
14350 cx.condition(|editor, _| editor.context_menu_visible())
14351 .await;
14352 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14353
14354 let _handler = handle_signature_help_request(
14355 &mut cx,
14356 lsp::SignatureHelp {
14357 signatures: vec![lsp::SignatureInformation {
14358 label: "test signature".to_string(),
14359 documentation: None,
14360 parameters: Some(vec![lsp::ParameterInformation {
14361 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
14362 documentation: None,
14363 }]),
14364 active_parameter: None,
14365 }],
14366 active_signature: None,
14367 active_parameter: None,
14368 },
14369 );
14370 cx.update_editor(|editor, window, cx| {
14371 assert!(
14372 !editor.signature_help_state.is_shown(),
14373 "No signature help was called for"
14374 );
14375 editor.show_signature_help(&ShowSignatureHelp, window, cx);
14376 });
14377 cx.run_until_parked();
14378 cx.update_editor(|editor, _, _| {
14379 assert!(
14380 !editor.signature_help_state.is_shown(),
14381 "No signature help should be shown when completions menu is open"
14382 );
14383 });
14384
14385 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14386 editor.context_menu_next(&Default::default(), window, cx);
14387 editor
14388 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14389 .unwrap()
14390 });
14391 cx.assert_editor_state(indoc! {"
14392 one.second_completionˇ
14393 two
14394 three
14395 "});
14396
14397 handle_resolve_completion_request(
14398 &mut cx,
14399 Some(vec![
14400 (
14401 //This overlaps with the primary completion edit which is
14402 //misbehavior from the LSP spec, test that we filter it out
14403 indoc! {"
14404 one.second_ˇcompletion
14405 two
14406 threeˇ
14407 "},
14408 "overlapping additional edit",
14409 ),
14410 (
14411 indoc! {"
14412 one.second_completion
14413 two
14414 threeˇ
14415 "},
14416 "\nadditional edit",
14417 ),
14418 ]),
14419 )
14420 .await;
14421 apply_additional_edits.await.unwrap();
14422 cx.assert_editor_state(indoc! {"
14423 one.second_completionˇ
14424 two
14425 three
14426 additional edit
14427 "});
14428
14429 cx.set_state(indoc! {"
14430 one.second_completion
14431 twoˇ
14432 threeˇ
14433 additional edit
14434 "});
14435 cx.simulate_keystroke(" ");
14436 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14437 cx.simulate_keystroke("s");
14438 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14439
14440 cx.assert_editor_state(indoc! {"
14441 one.second_completion
14442 two sˇ
14443 three sˇ
14444 additional edit
14445 "});
14446 handle_completion_request(
14447 indoc! {"
14448 one.second_completion
14449 two s
14450 three <s|>
14451 additional edit
14452 "},
14453 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14454 true,
14455 counter.clone(),
14456 &mut cx,
14457 )
14458 .await;
14459 cx.condition(|editor, _| editor.context_menu_visible())
14460 .await;
14461 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14462
14463 cx.simulate_keystroke("i");
14464
14465 handle_completion_request(
14466 indoc! {"
14467 one.second_completion
14468 two si
14469 three <si|>
14470 additional edit
14471 "},
14472 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
14473 true,
14474 counter.clone(),
14475 &mut cx,
14476 )
14477 .await;
14478 cx.condition(|editor, _| editor.context_menu_visible())
14479 .await;
14480 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14481
14482 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14483 editor
14484 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14485 .unwrap()
14486 });
14487 cx.assert_editor_state(indoc! {"
14488 one.second_completion
14489 two sixth_completionˇ
14490 three sixth_completionˇ
14491 additional edit
14492 "});
14493
14494 apply_additional_edits.await.unwrap();
14495
14496 update_test_language_settings(&mut cx, |settings| {
14497 settings.defaults.show_completions_on_input = Some(false);
14498 });
14499 cx.set_state("editorˇ");
14500 cx.simulate_keystroke(".");
14501 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14502 cx.simulate_keystrokes("c l o");
14503 cx.assert_editor_state("editor.cloˇ");
14504 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
14505 cx.update_editor(|editor, window, cx| {
14506 editor.show_completions(&ShowCompletions, window, cx);
14507 });
14508 handle_completion_request(
14509 "editor.<clo|>",
14510 vec!["close", "clobber"],
14511 true,
14512 counter.clone(),
14513 &mut cx,
14514 )
14515 .await;
14516 cx.condition(|editor, _| editor.context_menu_visible())
14517 .await;
14518 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
14519
14520 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14521 editor
14522 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14523 .unwrap()
14524 });
14525 cx.assert_editor_state("editor.clobberˇ");
14526 handle_resolve_completion_request(&mut cx, None).await;
14527 apply_additional_edits.await.unwrap();
14528}
14529
14530#[gpui::test]
14531async fn test_completion_reuse(cx: &mut TestAppContext) {
14532 init_test(cx, |_| {});
14533
14534 let mut cx = EditorLspTestContext::new_rust(
14535 lsp::ServerCapabilities {
14536 completion_provider: Some(lsp::CompletionOptions {
14537 trigger_characters: Some(vec![".".to_string()]),
14538 ..Default::default()
14539 }),
14540 ..Default::default()
14541 },
14542 cx,
14543 )
14544 .await;
14545
14546 let counter = Arc::new(AtomicUsize::new(0));
14547 cx.set_state("objˇ");
14548 cx.simulate_keystroke(".");
14549
14550 // Initial completion request returns complete results
14551 let is_incomplete = false;
14552 handle_completion_request(
14553 "obj.|<>",
14554 vec!["a", "ab", "abc"],
14555 is_incomplete,
14556 counter.clone(),
14557 &mut cx,
14558 )
14559 .await;
14560 cx.run_until_parked();
14561 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14562 cx.assert_editor_state("obj.ˇ");
14563 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14564
14565 // Type "a" - filters existing completions
14566 cx.simulate_keystroke("a");
14567 cx.run_until_parked();
14568 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14569 cx.assert_editor_state("obj.aˇ");
14570 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14571
14572 // Type "b" - filters existing completions
14573 cx.simulate_keystroke("b");
14574 cx.run_until_parked();
14575 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14576 cx.assert_editor_state("obj.abˇ");
14577 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14578
14579 // Type "c" - filters existing completions
14580 cx.simulate_keystroke("c");
14581 cx.run_until_parked();
14582 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14583 cx.assert_editor_state("obj.abcˇ");
14584 check_displayed_completions(vec!["abc"], &mut cx);
14585
14586 // Backspace to delete "c" - filters existing completions
14587 cx.update_editor(|editor, window, cx| {
14588 editor.backspace(&Backspace, window, cx);
14589 });
14590 cx.run_until_parked();
14591 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14592 cx.assert_editor_state("obj.abˇ");
14593 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14594
14595 // Moving cursor to the left dismisses menu.
14596 cx.update_editor(|editor, window, cx| {
14597 editor.move_left(&MoveLeft, window, cx);
14598 });
14599 cx.run_until_parked();
14600 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
14601 cx.assert_editor_state("obj.aˇb");
14602 cx.update_editor(|editor, _, _| {
14603 assert_eq!(editor.context_menu_visible(), false);
14604 });
14605
14606 // Type "b" - new request
14607 cx.simulate_keystroke("b");
14608 let is_incomplete = false;
14609 handle_completion_request(
14610 "obj.<ab|>a",
14611 vec!["ab", "abc"],
14612 is_incomplete,
14613 counter.clone(),
14614 &mut cx,
14615 )
14616 .await;
14617 cx.run_until_parked();
14618 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
14619 cx.assert_editor_state("obj.abˇb");
14620 check_displayed_completions(vec!["ab", "abc"], &mut cx);
14621
14622 // Backspace to delete "b" - since query was "ab" and is now "a", new request is made.
14623 cx.update_editor(|editor, window, cx| {
14624 editor.backspace(&Backspace, window, cx);
14625 });
14626 let is_incomplete = false;
14627 handle_completion_request(
14628 "obj.<a|>b",
14629 vec!["a", "ab", "abc"],
14630 is_incomplete,
14631 counter.clone(),
14632 &mut cx,
14633 )
14634 .await;
14635 cx.run_until_parked();
14636 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14637 cx.assert_editor_state("obj.aˇb");
14638 check_displayed_completions(vec!["a", "ab", "abc"], &mut cx);
14639
14640 // Backspace to delete "a" - dismisses menu.
14641 cx.update_editor(|editor, window, cx| {
14642 editor.backspace(&Backspace, window, cx);
14643 });
14644 cx.run_until_parked();
14645 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
14646 cx.assert_editor_state("obj.ˇb");
14647 cx.update_editor(|editor, _, _| {
14648 assert_eq!(editor.context_menu_visible(), false);
14649 });
14650}
14651
14652#[gpui::test]
14653async fn test_word_completion(cx: &mut TestAppContext) {
14654 let lsp_fetch_timeout_ms = 10;
14655 init_test(cx, |language_settings| {
14656 language_settings.defaults.completions = Some(CompletionSettingsContent {
14657 words_min_length: Some(0),
14658 lsp_fetch_timeout_ms: Some(10),
14659 lsp_insert_mode: Some(LspInsertMode::Insert),
14660 ..Default::default()
14661 });
14662 });
14663
14664 let mut cx = EditorLspTestContext::new_rust(
14665 lsp::ServerCapabilities {
14666 completion_provider: Some(lsp::CompletionOptions {
14667 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14668 ..lsp::CompletionOptions::default()
14669 }),
14670 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14671 ..lsp::ServerCapabilities::default()
14672 },
14673 cx,
14674 )
14675 .await;
14676
14677 let throttle_completions = Arc::new(AtomicBool::new(false));
14678
14679 let lsp_throttle_completions = throttle_completions.clone();
14680 let _completion_requests_handler =
14681 cx.lsp
14682 .server
14683 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
14684 let lsp_throttle_completions = lsp_throttle_completions.clone();
14685 let cx = cx.clone();
14686 async move {
14687 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
14688 cx.background_executor()
14689 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
14690 .await;
14691 }
14692 Ok(Some(lsp::CompletionResponse::Array(vec![
14693 lsp::CompletionItem {
14694 label: "first".into(),
14695 ..lsp::CompletionItem::default()
14696 },
14697 lsp::CompletionItem {
14698 label: "last".into(),
14699 ..lsp::CompletionItem::default()
14700 },
14701 ])))
14702 }
14703 });
14704
14705 cx.set_state(indoc! {"
14706 oneˇ
14707 two
14708 three
14709 "});
14710 cx.simulate_keystroke(".");
14711 cx.executor().run_until_parked();
14712 cx.condition(|editor, _| editor.context_menu_visible())
14713 .await;
14714 cx.update_editor(|editor, window, cx| {
14715 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14716 {
14717 assert_eq!(
14718 completion_menu_entries(menu),
14719 &["first", "last"],
14720 "When LSP server is fast to reply, no fallback word completions are used"
14721 );
14722 } else {
14723 panic!("expected completion menu to be open");
14724 }
14725 editor.cancel(&Cancel, window, cx);
14726 });
14727 cx.executor().run_until_parked();
14728 cx.condition(|editor, _| !editor.context_menu_visible())
14729 .await;
14730
14731 throttle_completions.store(true, atomic::Ordering::Release);
14732 cx.simulate_keystroke(".");
14733 cx.executor()
14734 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
14735 cx.executor().run_until_parked();
14736 cx.condition(|editor, _| editor.context_menu_visible())
14737 .await;
14738 cx.update_editor(|editor, _, _| {
14739 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14740 {
14741 assert_eq!(completion_menu_entries(menu), &["one", "three", "two"],
14742 "When LSP server is slow, document words can be shown instead, if configured accordingly");
14743 } else {
14744 panic!("expected completion menu to be open");
14745 }
14746 });
14747}
14748
14749#[gpui::test]
14750async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
14751 init_test(cx, |language_settings| {
14752 language_settings.defaults.completions = Some(CompletionSettingsContent {
14753 words: Some(WordsCompletionMode::Enabled),
14754 words_min_length: Some(0),
14755 lsp_insert_mode: Some(LspInsertMode::Insert),
14756 ..Default::default()
14757 });
14758 });
14759
14760 let mut cx = EditorLspTestContext::new_rust(
14761 lsp::ServerCapabilities {
14762 completion_provider: Some(lsp::CompletionOptions {
14763 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14764 ..lsp::CompletionOptions::default()
14765 }),
14766 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14767 ..lsp::ServerCapabilities::default()
14768 },
14769 cx,
14770 )
14771 .await;
14772
14773 let _completion_requests_handler =
14774 cx.lsp
14775 .server
14776 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14777 Ok(Some(lsp::CompletionResponse::Array(vec![
14778 lsp::CompletionItem {
14779 label: "first".into(),
14780 ..lsp::CompletionItem::default()
14781 },
14782 lsp::CompletionItem {
14783 label: "last".into(),
14784 ..lsp::CompletionItem::default()
14785 },
14786 ])))
14787 });
14788
14789 cx.set_state(indoc! {"ˇ
14790 first
14791 last
14792 second
14793 "});
14794 cx.simulate_keystroke(".");
14795 cx.executor().run_until_parked();
14796 cx.condition(|editor, _| editor.context_menu_visible())
14797 .await;
14798 cx.update_editor(|editor, _, _| {
14799 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14800 {
14801 assert_eq!(
14802 completion_menu_entries(menu),
14803 &["first", "last", "second"],
14804 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
14805 );
14806 } else {
14807 panic!("expected completion menu to be open");
14808 }
14809 });
14810}
14811
14812#[gpui::test]
14813async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
14814 init_test(cx, |language_settings| {
14815 language_settings.defaults.completions = Some(CompletionSettingsContent {
14816 words: Some(WordsCompletionMode::Disabled),
14817 words_min_length: Some(0),
14818 lsp_insert_mode: Some(LspInsertMode::Insert),
14819 ..Default::default()
14820 });
14821 });
14822
14823 let mut cx = EditorLspTestContext::new_rust(
14824 lsp::ServerCapabilities {
14825 completion_provider: Some(lsp::CompletionOptions {
14826 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
14827 ..lsp::CompletionOptions::default()
14828 }),
14829 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
14830 ..lsp::ServerCapabilities::default()
14831 },
14832 cx,
14833 )
14834 .await;
14835
14836 let _completion_requests_handler =
14837 cx.lsp
14838 .server
14839 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
14840 panic!("LSP completions should not be queried when dealing with word completions")
14841 });
14842
14843 cx.set_state(indoc! {"ˇ
14844 first
14845 last
14846 second
14847 "});
14848 cx.update_editor(|editor, window, cx| {
14849 editor.show_word_completions(&ShowWordCompletions, window, cx);
14850 });
14851 cx.executor().run_until_parked();
14852 cx.condition(|editor, _| editor.context_menu_visible())
14853 .await;
14854 cx.update_editor(|editor, _, _| {
14855 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14856 {
14857 assert_eq!(
14858 completion_menu_entries(menu),
14859 &["first", "last", "second"],
14860 "`ShowWordCompletions` action should show word completions"
14861 );
14862 } else {
14863 panic!("expected completion menu to be open");
14864 }
14865 });
14866
14867 cx.simulate_keystroke("l");
14868 cx.executor().run_until_parked();
14869 cx.condition(|editor, _| editor.context_menu_visible())
14870 .await;
14871 cx.update_editor(|editor, _, _| {
14872 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14873 {
14874 assert_eq!(
14875 completion_menu_entries(menu),
14876 &["last"],
14877 "After showing word completions, further editing should filter them and not query the LSP"
14878 );
14879 } else {
14880 panic!("expected completion menu to be open");
14881 }
14882 });
14883}
14884
14885#[gpui::test]
14886async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
14887 init_test(cx, |language_settings| {
14888 language_settings.defaults.completions = Some(CompletionSettingsContent {
14889 words_min_length: Some(0),
14890 lsp: Some(false),
14891 lsp_insert_mode: Some(LspInsertMode::Insert),
14892 ..Default::default()
14893 });
14894 });
14895
14896 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14897
14898 cx.set_state(indoc! {"ˇ
14899 0_usize
14900 let
14901 33
14902 4.5f32
14903 "});
14904 cx.update_editor(|editor, window, cx| {
14905 editor.show_completions(&ShowCompletions, window, cx);
14906 });
14907 cx.executor().run_until_parked();
14908 cx.condition(|editor, _| editor.context_menu_visible())
14909 .await;
14910 cx.update_editor(|editor, window, cx| {
14911 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14912 {
14913 assert_eq!(
14914 completion_menu_entries(menu),
14915 &["let"],
14916 "With no digits in the completion query, no digits should be in the word completions"
14917 );
14918 } else {
14919 panic!("expected completion menu to be open");
14920 }
14921 editor.cancel(&Cancel, window, cx);
14922 });
14923
14924 cx.set_state(indoc! {"3ˇ
14925 0_usize
14926 let
14927 3
14928 33.35f32
14929 "});
14930 cx.update_editor(|editor, window, cx| {
14931 editor.show_completions(&ShowCompletions, window, cx);
14932 });
14933 cx.executor().run_until_parked();
14934 cx.condition(|editor, _| editor.context_menu_visible())
14935 .await;
14936 cx.update_editor(|editor, _, _| {
14937 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14938 {
14939 assert_eq!(completion_menu_entries(menu), &["33", "35f32"], "The digit is in the completion query, \
14940 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
14941 } else {
14942 panic!("expected completion menu to be open");
14943 }
14944 });
14945}
14946
14947#[gpui::test]
14948async fn test_word_completions_do_not_show_before_threshold(cx: &mut TestAppContext) {
14949 init_test(cx, |language_settings| {
14950 language_settings.defaults.completions = Some(CompletionSettingsContent {
14951 words: Some(WordsCompletionMode::Enabled),
14952 words_min_length: Some(3),
14953 lsp_insert_mode: Some(LspInsertMode::Insert),
14954 ..Default::default()
14955 });
14956 });
14957
14958 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14959 cx.set_state(indoc! {"ˇ
14960 wow
14961 wowen
14962 wowser
14963 "});
14964 cx.simulate_keystroke("w");
14965 cx.executor().run_until_parked();
14966 cx.update_editor(|editor, _, _| {
14967 if editor.context_menu.borrow_mut().is_some() {
14968 panic!(
14969 "expected completion menu to be hidden, as words completion threshold is not met"
14970 );
14971 }
14972 });
14973
14974 cx.update_editor(|editor, window, cx| {
14975 editor.show_word_completions(&ShowWordCompletions, window, cx);
14976 });
14977 cx.executor().run_until_parked();
14978 cx.update_editor(|editor, window, cx| {
14979 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14980 {
14981 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");
14982 } else {
14983 panic!("expected completion menu to be open after the word completions are called with an action");
14984 }
14985
14986 editor.cancel(&Cancel, window, cx);
14987 });
14988 cx.update_editor(|editor, _, _| {
14989 if editor.context_menu.borrow_mut().is_some() {
14990 panic!("expected completion menu to be hidden after canceling");
14991 }
14992 });
14993
14994 cx.simulate_keystroke("o");
14995 cx.executor().run_until_parked();
14996 cx.update_editor(|editor, _, _| {
14997 if editor.context_menu.borrow_mut().is_some() {
14998 panic!(
14999 "expected completion menu to be hidden, as words completion threshold is not met still"
15000 );
15001 }
15002 });
15003
15004 cx.simulate_keystroke("w");
15005 cx.executor().run_until_parked();
15006 cx.update_editor(|editor, _, _| {
15007 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15008 {
15009 assert_eq!(completion_menu_entries(menu), &["wowen", "wowser"], "After word completion threshold is met, matching words should be shown, excluding the already typed word");
15010 } else {
15011 panic!("expected completion menu to be open after the word completions threshold is met");
15012 }
15013 });
15014}
15015
15016#[gpui::test]
15017async fn test_word_completions_disabled(cx: &mut TestAppContext) {
15018 init_test(cx, |language_settings| {
15019 language_settings.defaults.completions = Some(CompletionSettingsContent {
15020 words: Some(WordsCompletionMode::Enabled),
15021 words_min_length: Some(0),
15022 lsp_insert_mode: Some(LspInsertMode::Insert),
15023 ..Default::default()
15024 });
15025 });
15026
15027 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15028 cx.update_editor(|editor, _, _| {
15029 editor.disable_word_completions();
15030 });
15031 cx.set_state(indoc! {"ˇ
15032 wow
15033 wowen
15034 wowser
15035 "});
15036 cx.simulate_keystroke("w");
15037 cx.executor().run_until_parked();
15038 cx.update_editor(|editor, _, _| {
15039 if editor.context_menu.borrow_mut().is_some() {
15040 panic!(
15041 "expected completion menu to be hidden, as words completion are disabled for this editor"
15042 );
15043 }
15044 });
15045
15046 cx.update_editor(|editor, window, cx| {
15047 editor.show_word_completions(&ShowWordCompletions, window, cx);
15048 });
15049 cx.executor().run_until_parked();
15050 cx.update_editor(|editor, _, _| {
15051 if editor.context_menu.borrow_mut().is_some() {
15052 panic!(
15053 "expected completion menu to be hidden even if called for explicitly, as words completion are disabled for this editor"
15054 );
15055 }
15056 });
15057}
15058
15059#[gpui::test]
15060async fn test_word_completions_disabled_with_no_provider(cx: &mut TestAppContext) {
15061 init_test(cx, |language_settings| {
15062 language_settings.defaults.completions = Some(CompletionSettingsContent {
15063 words: Some(WordsCompletionMode::Disabled),
15064 words_min_length: Some(0),
15065 lsp_insert_mode: Some(LspInsertMode::Insert),
15066 ..Default::default()
15067 });
15068 });
15069
15070 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15071 cx.update_editor(|editor, _, _| {
15072 editor.set_completion_provider(None);
15073 });
15074 cx.set_state(indoc! {"ˇ
15075 wow
15076 wowen
15077 wowser
15078 "});
15079 cx.simulate_keystroke("w");
15080 cx.executor().run_until_parked();
15081 cx.update_editor(|editor, _, _| {
15082 if editor.context_menu.borrow_mut().is_some() {
15083 panic!("expected completion menu to be hidden, as disabled in settings");
15084 }
15085 });
15086}
15087
15088fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
15089 let position = || lsp::Position {
15090 line: params.text_document_position.position.line,
15091 character: params.text_document_position.position.character,
15092 };
15093 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15094 range: lsp::Range {
15095 start: position(),
15096 end: position(),
15097 },
15098 new_text: text.to_string(),
15099 }))
15100}
15101
15102#[gpui::test]
15103async fn test_multiline_completion(cx: &mut TestAppContext) {
15104 init_test(cx, |_| {});
15105
15106 let fs = FakeFs::new(cx.executor());
15107 fs.insert_tree(
15108 path!("/a"),
15109 json!({
15110 "main.ts": "a",
15111 }),
15112 )
15113 .await;
15114
15115 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
15116 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
15117 let typescript_language = Arc::new(Language::new(
15118 LanguageConfig {
15119 name: "TypeScript".into(),
15120 matcher: LanguageMatcher {
15121 path_suffixes: vec!["ts".to_string()],
15122 ..LanguageMatcher::default()
15123 },
15124 line_comments: vec!["// ".into()],
15125 ..LanguageConfig::default()
15126 },
15127 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
15128 ));
15129 language_registry.add(typescript_language.clone());
15130 let mut fake_servers = language_registry.register_fake_lsp(
15131 "TypeScript",
15132 FakeLspAdapter {
15133 capabilities: lsp::ServerCapabilities {
15134 completion_provider: Some(lsp::CompletionOptions {
15135 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
15136 ..lsp::CompletionOptions::default()
15137 }),
15138 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
15139 ..lsp::ServerCapabilities::default()
15140 },
15141 // Emulate vtsls label generation
15142 label_for_completion: Some(Box::new(|item, _| {
15143 let text = if let Some(description) = item
15144 .label_details
15145 .as_ref()
15146 .and_then(|label_details| label_details.description.as_ref())
15147 {
15148 format!("{} {}", item.label, description)
15149 } else if let Some(detail) = &item.detail {
15150 format!("{} {}", item.label, detail)
15151 } else {
15152 item.label.clone()
15153 };
15154 Some(language::CodeLabel::plain(text, None))
15155 })),
15156 ..FakeLspAdapter::default()
15157 },
15158 );
15159 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15160 let cx = &mut VisualTestContext::from_window(*workspace, cx);
15161 let worktree_id = workspace
15162 .update(cx, |workspace, _window, cx| {
15163 workspace.project().update(cx, |project, cx| {
15164 project.worktrees(cx).next().unwrap().read(cx).id()
15165 })
15166 })
15167 .unwrap();
15168 let _buffer = project
15169 .update(cx, |project, cx| {
15170 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
15171 })
15172 .await
15173 .unwrap();
15174 let editor = workspace
15175 .update(cx, |workspace, window, cx| {
15176 workspace.open_path((worktree_id, rel_path("main.ts")), None, true, window, cx)
15177 })
15178 .unwrap()
15179 .await
15180 .unwrap()
15181 .downcast::<Editor>()
15182 .unwrap();
15183 let fake_server = fake_servers.next().await.unwrap();
15184
15185 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
15186 let multiline_label_2 = "a\nb\nc\n";
15187 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
15188 let multiline_description = "d\ne\nf\n";
15189 let multiline_detail_2 = "g\nh\ni\n";
15190
15191 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
15192 move |params, _| async move {
15193 Ok(Some(lsp::CompletionResponse::Array(vec![
15194 lsp::CompletionItem {
15195 label: multiline_label.to_string(),
15196 text_edit: gen_text_edit(¶ms, "new_text_1"),
15197 ..lsp::CompletionItem::default()
15198 },
15199 lsp::CompletionItem {
15200 label: "single line label 1".to_string(),
15201 detail: Some(multiline_detail.to_string()),
15202 text_edit: gen_text_edit(¶ms, "new_text_2"),
15203 ..lsp::CompletionItem::default()
15204 },
15205 lsp::CompletionItem {
15206 label: "single line label 2".to_string(),
15207 label_details: Some(lsp::CompletionItemLabelDetails {
15208 description: Some(multiline_description.to_string()),
15209 detail: None,
15210 }),
15211 text_edit: gen_text_edit(¶ms, "new_text_2"),
15212 ..lsp::CompletionItem::default()
15213 },
15214 lsp::CompletionItem {
15215 label: multiline_label_2.to_string(),
15216 detail: Some(multiline_detail_2.to_string()),
15217 text_edit: gen_text_edit(¶ms, "new_text_3"),
15218 ..lsp::CompletionItem::default()
15219 },
15220 lsp::CompletionItem {
15221 label: "Label with many spaces and \t but without newlines".to_string(),
15222 detail: Some(
15223 "Details with many spaces and \t but without newlines".to_string(),
15224 ),
15225 text_edit: gen_text_edit(¶ms, "new_text_4"),
15226 ..lsp::CompletionItem::default()
15227 },
15228 ])))
15229 },
15230 );
15231
15232 editor.update_in(cx, |editor, window, cx| {
15233 cx.focus_self(window);
15234 editor.move_to_end(&MoveToEnd, window, cx);
15235 editor.handle_input(".", window, cx);
15236 });
15237 cx.run_until_parked();
15238 completion_handle.next().await.unwrap();
15239
15240 editor.update(cx, |editor, _| {
15241 assert!(editor.context_menu_visible());
15242 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15243 {
15244 let completion_labels = menu
15245 .completions
15246 .borrow()
15247 .iter()
15248 .map(|c| c.label.text.clone())
15249 .collect::<Vec<_>>();
15250 assert_eq!(
15251 completion_labels,
15252 &[
15253 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
15254 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
15255 "single line label 2 d e f ",
15256 "a b c g h i ",
15257 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
15258 ],
15259 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
15260 );
15261
15262 for completion in menu
15263 .completions
15264 .borrow()
15265 .iter() {
15266 assert_eq!(
15267 completion.label.filter_range,
15268 0..completion.label.text.len(),
15269 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
15270 );
15271 }
15272 } else {
15273 panic!("expected completion menu to be open");
15274 }
15275 });
15276}
15277
15278#[gpui::test]
15279async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
15280 init_test(cx, |_| {});
15281 let mut cx = EditorLspTestContext::new_rust(
15282 lsp::ServerCapabilities {
15283 completion_provider: Some(lsp::CompletionOptions {
15284 trigger_characters: Some(vec![".".to_string()]),
15285 ..Default::default()
15286 }),
15287 ..Default::default()
15288 },
15289 cx,
15290 )
15291 .await;
15292 cx.lsp
15293 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15294 Ok(Some(lsp::CompletionResponse::Array(vec![
15295 lsp::CompletionItem {
15296 label: "first".into(),
15297 ..Default::default()
15298 },
15299 lsp::CompletionItem {
15300 label: "last".into(),
15301 ..Default::default()
15302 },
15303 ])))
15304 });
15305 cx.set_state("variableˇ");
15306 cx.simulate_keystroke(".");
15307 cx.executor().run_until_parked();
15308
15309 cx.update_editor(|editor, _, _| {
15310 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15311 {
15312 assert_eq!(completion_menu_entries(menu), &["first", "last"]);
15313 } else {
15314 panic!("expected completion menu to be open");
15315 }
15316 });
15317
15318 cx.update_editor(|editor, window, cx| {
15319 editor.move_page_down(&MovePageDown::default(), window, cx);
15320 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15321 {
15322 assert!(
15323 menu.selected_item == 1,
15324 "expected PageDown to select the last item from the context menu"
15325 );
15326 } else {
15327 panic!("expected completion menu to stay open after PageDown");
15328 }
15329 });
15330
15331 cx.update_editor(|editor, window, cx| {
15332 editor.move_page_up(&MovePageUp::default(), window, cx);
15333 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
15334 {
15335 assert!(
15336 menu.selected_item == 0,
15337 "expected PageUp to select the first item from the context menu"
15338 );
15339 } else {
15340 panic!("expected completion menu to stay open after PageUp");
15341 }
15342 });
15343}
15344
15345#[gpui::test]
15346async fn test_as_is_completions(cx: &mut TestAppContext) {
15347 init_test(cx, |_| {});
15348 let mut cx = EditorLspTestContext::new_rust(
15349 lsp::ServerCapabilities {
15350 completion_provider: Some(lsp::CompletionOptions {
15351 ..Default::default()
15352 }),
15353 ..Default::default()
15354 },
15355 cx,
15356 )
15357 .await;
15358 cx.lsp
15359 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15360 Ok(Some(lsp::CompletionResponse::Array(vec![
15361 lsp::CompletionItem {
15362 label: "unsafe".into(),
15363 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15364 range: lsp::Range {
15365 start: lsp::Position {
15366 line: 1,
15367 character: 2,
15368 },
15369 end: lsp::Position {
15370 line: 1,
15371 character: 3,
15372 },
15373 },
15374 new_text: "unsafe".to_string(),
15375 })),
15376 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
15377 ..Default::default()
15378 },
15379 ])))
15380 });
15381 cx.set_state("fn a() {}\n nˇ");
15382 cx.executor().run_until_parked();
15383 cx.update_editor(|editor, window, cx| {
15384 editor.trigger_completion_on_input("n", true, window, cx)
15385 });
15386 cx.executor().run_until_parked();
15387
15388 cx.update_editor(|editor, window, cx| {
15389 editor.confirm_completion(&Default::default(), window, cx)
15390 });
15391 cx.executor().run_until_parked();
15392 cx.assert_editor_state("fn a() {}\n unsafeˇ");
15393}
15394
15395#[gpui::test]
15396async fn test_panic_during_c_completions(cx: &mut TestAppContext) {
15397 init_test(cx, |_| {});
15398 let language =
15399 Arc::try_unwrap(languages::language("c", tree_sitter_c::LANGUAGE.into())).unwrap();
15400 let mut cx = EditorLspTestContext::new(
15401 language,
15402 lsp::ServerCapabilities {
15403 completion_provider: Some(lsp::CompletionOptions {
15404 ..lsp::CompletionOptions::default()
15405 }),
15406 ..lsp::ServerCapabilities::default()
15407 },
15408 cx,
15409 )
15410 .await;
15411
15412 cx.set_state(
15413 "#ifndef BAR_H
15414#define BAR_H
15415
15416#include <stdbool.h>
15417
15418int fn_branch(bool do_branch1, bool do_branch2);
15419
15420#endif // BAR_H
15421ˇ",
15422 );
15423 cx.executor().run_until_parked();
15424 cx.update_editor(|editor, window, cx| {
15425 editor.handle_input("#", window, cx);
15426 });
15427 cx.executor().run_until_parked();
15428 cx.update_editor(|editor, window, cx| {
15429 editor.handle_input("i", window, cx);
15430 });
15431 cx.executor().run_until_parked();
15432 cx.update_editor(|editor, window, cx| {
15433 editor.handle_input("n", window, cx);
15434 });
15435 cx.executor().run_until_parked();
15436 cx.assert_editor_state(
15437 "#ifndef BAR_H
15438#define BAR_H
15439
15440#include <stdbool.h>
15441
15442int fn_branch(bool do_branch1, bool do_branch2);
15443
15444#endif // BAR_H
15445#inˇ",
15446 );
15447
15448 cx.lsp
15449 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15450 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15451 is_incomplete: false,
15452 item_defaults: None,
15453 items: vec![lsp::CompletionItem {
15454 kind: Some(lsp::CompletionItemKind::SNIPPET),
15455 label_details: Some(lsp::CompletionItemLabelDetails {
15456 detail: Some("header".to_string()),
15457 description: None,
15458 }),
15459 label: " include".to_string(),
15460 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15461 range: lsp::Range {
15462 start: lsp::Position {
15463 line: 8,
15464 character: 1,
15465 },
15466 end: lsp::Position {
15467 line: 8,
15468 character: 1,
15469 },
15470 },
15471 new_text: "include \"$0\"".to_string(),
15472 })),
15473 sort_text: Some("40b67681include".to_string()),
15474 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15475 filter_text: Some("include".to_string()),
15476 insert_text: Some("include \"$0\"".to_string()),
15477 ..lsp::CompletionItem::default()
15478 }],
15479 })))
15480 });
15481 cx.update_editor(|editor, window, cx| {
15482 editor.show_completions(&ShowCompletions, window, cx);
15483 });
15484 cx.executor().run_until_parked();
15485 cx.update_editor(|editor, window, cx| {
15486 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15487 });
15488 cx.executor().run_until_parked();
15489 cx.assert_editor_state(
15490 "#ifndef BAR_H
15491#define BAR_H
15492
15493#include <stdbool.h>
15494
15495int fn_branch(bool do_branch1, bool do_branch2);
15496
15497#endif // BAR_H
15498#include \"ˇ\"",
15499 );
15500
15501 cx.lsp
15502 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
15503 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15504 is_incomplete: true,
15505 item_defaults: None,
15506 items: vec![lsp::CompletionItem {
15507 kind: Some(lsp::CompletionItemKind::FILE),
15508 label: "AGL/".to_string(),
15509 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15510 range: lsp::Range {
15511 start: lsp::Position {
15512 line: 8,
15513 character: 10,
15514 },
15515 end: lsp::Position {
15516 line: 8,
15517 character: 11,
15518 },
15519 },
15520 new_text: "AGL/".to_string(),
15521 })),
15522 sort_text: Some("40b67681AGL/".to_string()),
15523 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
15524 filter_text: Some("AGL/".to_string()),
15525 insert_text: Some("AGL/".to_string()),
15526 ..lsp::CompletionItem::default()
15527 }],
15528 })))
15529 });
15530 cx.update_editor(|editor, window, cx| {
15531 editor.show_completions(&ShowCompletions, window, cx);
15532 });
15533 cx.executor().run_until_parked();
15534 cx.update_editor(|editor, window, cx| {
15535 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
15536 });
15537 cx.executor().run_until_parked();
15538 cx.assert_editor_state(
15539 r##"#ifndef BAR_H
15540#define BAR_H
15541
15542#include <stdbool.h>
15543
15544int fn_branch(bool do_branch1, bool do_branch2);
15545
15546#endif // BAR_H
15547#include "AGL/ˇ"##,
15548 );
15549
15550 cx.update_editor(|editor, window, cx| {
15551 editor.handle_input("\"", window, cx);
15552 });
15553 cx.executor().run_until_parked();
15554 cx.assert_editor_state(
15555 r##"#ifndef BAR_H
15556#define BAR_H
15557
15558#include <stdbool.h>
15559
15560int fn_branch(bool do_branch1, bool do_branch2);
15561
15562#endif // BAR_H
15563#include "AGL/"ˇ"##,
15564 );
15565}
15566
15567#[gpui::test]
15568async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
15569 init_test(cx, |_| {});
15570
15571 let mut cx = EditorLspTestContext::new_rust(
15572 lsp::ServerCapabilities {
15573 completion_provider: Some(lsp::CompletionOptions {
15574 trigger_characters: Some(vec![".".to_string()]),
15575 resolve_provider: Some(true),
15576 ..Default::default()
15577 }),
15578 ..Default::default()
15579 },
15580 cx,
15581 )
15582 .await;
15583
15584 cx.set_state("fn main() { let a = 2ˇ; }");
15585 cx.simulate_keystroke(".");
15586 let completion_item = lsp::CompletionItem {
15587 label: "Some".into(),
15588 kind: Some(lsp::CompletionItemKind::SNIPPET),
15589 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
15590 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
15591 kind: lsp::MarkupKind::Markdown,
15592 value: "```rust\nSome(2)\n```".to_string(),
15593 })),
15594 deprecated: Some(false),
15595 sort_text: Some("Some".to_string()),
15596 filter_text: Some("Some".to_string()),
15597 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
15598 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
15599 range: lsp::Range {
15600 start: lsp::Position {
15601 line: 0,
15602 character: 22,
15603 },
15604 end: lsp::Position {
15605 line: 0,
15606 character: 22,
15607 },
15608 },
15609 new_text: "Some(2)".to_string(),
15610 })),
15611 additional_text_edits: Some(vec![lsp::TextEdit {
15612 range: lsp::Range {
15613 start: lsp::Position {
15614 line: 0,
15615 character: 20,
15616 },
15617 end: lsp::Position {
15618 line: 0,
15619 character: 22,
15620 },
15621 },
15622 new_text: "".to_string(),
15623 }]),
15624 ..Default::default()
15625 };
15626
15627 let closure_completion_item = completion_item.clone();
15628 let counter = Arc::new(AtomicUsize::new(0));
15629 let counter_clone = counter.clone();
15630 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
15631 let task_completion_item = closure_completion_item.clone();
15632 counter_clone.fetch_add(1, atomic::Ordering::Release);
15633 async move {
15634 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
15635 is_incomplete: true,
15636 item_defaults: None,
15637 items: vec![task_completion_item],
15638 })))
15639 }
15640 });
15641
15642 cx.condition(|editor, _| editor.context_menu_visible())
15643 .await;
15644 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
15645 assert!(request.next().await.is_some());
15646 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
15647
15648 cx.simulate_keystrokes("S o m");
15649 cx.condition(|editor, _| editor.context_menu_visible())
15650 .await;
15651 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
15652 assert!(request.next().await.is_some());
15653 assert!(request.next().await.is_some());
15654 assert!(request.next().await.is_some());
15655 request.close();
15656 assert!(request.next().await.is_none());
15657 assert_eq!(
15658 counter.load(atomic::Ordering::Acquire),
15659 4,
15660 "With the completions menu open, only one LSP request should happen per input"
15661 );
15662}
15663
15664#[gpui::test]
15665async fn test_toggle_comment(cx: &mut TestAppContext) {
15666 init_test(cx, |_| {});
15667 let mut cx = EditorTestContext::new(cx).await;
15668 let language = Arc::new(Language::new(
15669 LanguageConfig {
15670 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15671 ..Default::default()
15672 },
15673 Some(tree_sitter_rust::LANGUAGE.into()),
15674 ));
15675 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15676
15677 // If multiple selections intersect a line, the line is only toggled once.
15678 cx.set_state(indoc! {"
15679 fn a() {
15680 «//b();
15681 ˇ»// «c();
15682 //ˇ» d();
15683 }
15684 "});
15685
15686 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15687
15688 cx.assert_editor_state(indoc! {"
15689 fn a() {
15690 «b();
15691 c();
15692 ˇ» d();
15693 }
15694 "});
15695
15696 // The comment prefix is inserted at the same column for every line in a
15697 // selection.
15698 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15699
15700 cx.assert_editor_state(indoc! {"
15701 fn a() {
15702 // «b();
15703 // c();
15704 ˇ»// d();
15705 }
15706 "});
15707
15708 // If a selection ends at the beginning of a line, that line is not toggled.
15709 cx.set_selections_state(indoc! {"
15710 fn a() {
15711 // b();
15712 «// c();
15713 ˇ» // d();
15714 }
15715 "});
15716
15717 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15718
15719 cx.assert_editor_state(indoc! {"
15720 fn a() {
15721 // b();
15722 «c();
15723 ˇ» // d();
15724 }
15725 "});
15726
15727 // If a selection span a single line and is empty, the line is toggled.
15728 cx.set_state(indoc! {"
15729 fn a() {
15730 a();
15731 b();
15732 ˇ
15733 }
15734 "});
15735
15736 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15737
15738 cx.assert_editor_state(indoc! {"
15739 fn a() {
15740 a();
15741 b();
15742 //•ˇ
15743 }
15744 "});
15745
15746 // If a selection span multiple lines, empty lines are not toggled.
15747 cx.set_state(indoc! {"
15748 fn a() {
15749 «a();
15750
15751 c();ˇ»
15752 }
15753 "});
15754
15755 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15756
15757 cx.assert_editor_state(indoc! {"
15758 fn a() {
15759 // «a();
15760
15761 // c();ˇ»
15762 }
15763 "});
15764
15765 // If a selection includes multiple comment prefixes, all lines are uncommented.
15766 cx.set_state(indoc! {"
15767 fn a() {
15768 «// a();
15769 /// b();
15770 //! c();ˇ»
15771 }
15772 "});
15773
15774 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
15775
15776 cx.assert_editor_state(indoc! {"
15777 fn a() {
15778 «a();
15779 b();
15780 c();ˇ»
15781 }
15782 "});
15783}
15784
15785#[gpui::test]
15786async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
15787 init_test(cx, |_| {});
15788 let mut cx = EditorTestContext::new(cx).await;
15789 let language = Arc::new(Language::new(
15790 LanguageConfig {
15791 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
15792 ..Default::default()
15793 },
15794 Some(tree_sitter_rust::LANGUAGE.into()),
15795 ));
15796 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
15797
15798 let toggle_comments = &ToggleComments {
15799 advance_downwards: false,
15800 ignore_indent: true,
15801 };
15802
15803 // If multiple selections intersect a line, the line is only toggled once.
15804 cx.set_state(indoc! {"
15805 fn a() {
15806 // «b();
15807 // c();
15808 // ˇ» d();
15809 }
15810 "});
15811
15812 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15813
15814 cx.assert_editor_state(indoc! {"
15815 fn a() {
15816 «b();
15817 c();
15818 ˇ» d();
15819 }
15820 "});
15821
15822 // The comment prefix is inserted at the beginning of each line
15823 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15824
15825 cx.assert_editor_state(indoc! {"
15826 fn a() {
15827 // «b();
15828 // c();
15829 // ˇ» d();
15830 }
15831 "});
15832
15833 // If a selection ends at the beginning of a line, that line is not toggled.
15834 cx.set_selections_state(indoc! {"
15835 fn a() {
15836 // b();
15837 // «c();
15838 ˇ»// d();
15839 }
15840 "});
15841
15842 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15843
15844 cx.assert_editor_state(indoc! {"
15845 fn a() {
15846 // b();
15847 «c();
15848 ˇ»// d();
15849 }
15850 "});
15851
15852 // If a selection span a single line and is empty, the line is toggled.
15853 cx.set_state(indoc! {"
15854 fn a() {
15855 a();
15856 b();
15857 ˇ
15858 }
15859 "});
15860
15861 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15862
15863 cx.assert_editor_state(indoc! {"
15864 fn a() {
15865 a();
15866 b();
15867 //ˇ
15868 }
15869 "});
15870
15871 // If a selection span multiple lines, empty lines are not toggled.
15872 cx.set_state(indoc! {"
15873 fn a() {
15874 «a();
15875
15876 c();ˇ»
15877 }
15878 "});
15879
15880 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15881
15882 cx.assert_editor_state(indoc! {"
15883 fn a() {
15884 // «a();
15885
15886 // c();ˇ»
15887 }
15888 "});
15889
15890 // If a selection includes multiple comment prefixes, all lines are uncommented.
15891 cx.set_state(indoc! {"
15892 fn a() {
15893 // «a();
15894 /// b();
15895 //! c();ˇ»
15896 }
15897 "});
15898
15899 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
15900
15901 cx.assert_editor_state(indoc! {"
15902 fn a() {
15903 «a();
15904 b();
15905 c();ˇ»
15906 }
15907 "});
15908}
15909
15910#[gpui::test]
15911async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
15912 init_test(cx, |_| {});
15913
15914 let language = Arc::new(Language::new(
15915 LanguageConfig {
15916 line_comments: vec!["// ".into()],
15917 ..Default::default()
15918 },
15919 Some(tree_sitter_rust::LANGUAGE.into()),
15920 ));
15921
15922 let mut cx = EditorTestContext::new(cx).await;
15923
15924 cx.language_registry().add(language.clone());
15925 cx.update_buffer(|buffer, cx| {
15926 buffer.set_language(Some(language), cx);
15927 });
15928
15929 let toggle_comments = &ToggleComments {
15930 advance_downwards: true,
15931 ignore_indent: false,
15932 };
15933
15934 // Single cursor on one line -> advance
15935 // Cursor moves horizontally 3 characters as well on non-blank line
15936 cx.set_state(indoc!(
15937 "fn a() {
15938 ˇdog();
15939 cat();
15940 }"
15941 ));
15942 cx.update_editor(|editor, window, cx| {
15943 editor.toggle_comments(toggle_comments, window, cx);
15944 });
15945 cx.assert_editor_state(indoc!(
15946 "fn a() {
15947 // dog();
15948 catˇ();
15949 }"
15950 ));
15951
15952 // Single selection on one line -> don't advance
15953 cx.set_state(indoc!(
15954 "fn a() {
15955 «dog()ˇ»;
15956 cat();
15957 }"
15958 ));
15959 cx.update_editor(|editor, window, cx| {
15960 editor.toggle_comments(toggle_comments, window, cx);
15961 });
15962 cx.assert_editor_state(indoc!(
15963 "fn a() {
15964 // «dog()ˇ»;
15965 cat();
15966 }"
15967 ));
15968
15969 // Multiple cursors on one line -> advance
15970 cx.set_state(indoc!(
15971 "fn a() {
15972 ˇdˇog();
15973 cat();
15974 }"
15975 ));
15976 cx.update_editor(|editor, window, cx| {
15977 editor.toggle_comments(toggle_comments, window, cx);
15978 });
15979 cx.assert_editor_state(indoc!(
15980 "fn a() {
15981 // dog();
15982 catˇ(ˇ);
15983 }"
15984 ));
15985
15986 // Multiple cursors on one line, with selection -> don't advance
15987 cx.set_state(indoc!(
15988 "fn a() {
15989 ˇdˇog«()ˇ»;
15990 cat();
15991 }"
15992 ));
15993 cx.update_editor(|editor, window, cx| {
15994 editor.toggle_comments(toggle_comments, window, cx);
15995 });
15996 cx.assert_editor_state(indoc!(
15997 "fn a() {
15998 // ˇdˇog«()ˇ»;
15999 cat();
16000 }"
16001 ));
16002
16003 // Single cursor on one line -> advance
16004 // Cursor moves to column 0 on blank line
16005 cx.set_state(indoc!(
16006 "fn a() {
16007 ˇdog();
16008
16009 cat();
16010 }"
16011 ));
16012 cx.update_editor(|editor, window, cx| {
16013 editor.toggle_comments(toggle_comments, window, cx);
16014 });
16015 cx.assert_editor_state(indoc!(
16016 "fn a() {
16017 // dog();
16018 ˇ
16019 cat();
16020 }"
16021 ));
16022
16023 // Single cursor on one line -> advance
16024 // Cursor starts and ends at column 0
16025 cx.set_state(indoc!(
16026 "fn a() {
16027 ˇ dog();
16028 cat();
16029 }"
16030 ));
16031 cx.update_editor(|editor, window, cx| {
16032 editor.toggle_comments(toggle_comments, window, cx);
16033 });
16034 cx.assert_editor_state(indoc!(
16035 "fn a() {
16036 // dog();
16037 ˇ cat();
16038 }"
16039 ));
16040}
16041
16042#[gpui::test]
16043async fn test_toggle_block_comment(cx: &mut TestAppContext) {
16044 init_test(cx, |_| {});
16045
16046 let mut cx = EditorTestContext::new(cx).await;
16047
16048 let html_language = Arc::new(
16049 Language::new(
16050 LanguageConfig {
16051 name: "HTML".into(),
16052 block_comment: Some(BlockCommentConfig {
16053 start: "<!-- ".into(),
16054 prefix: "".into(),
16055 end: " -->".into(),
16056 tab_size: 0,
16057 }),
16058 ..Default::default()
16059 },
16060 Some(tree_sitter_html::LANGUAGE.into()),
16061 )
16062 .with_injection_query(
16063 r#"
16064 (script_element
16065 (raw_text) @injection.content
16066 (#set! injection.language "javascript"))
16067 "#,
16068 )
16069 .unwrap(),
16070 );
16071
16072 let javascript_language = Arc::new(Language::new(
16073 LanguageConfig {
16074 name: "JavaScript".into(),
16075 line_comments: vec!["// ".into()],
16076 ..Default::default()
16077 },
16078 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
16079 ));
16080
16081 cx.language_registry().add(html_language.clone());
16082 cx.language_registry().add(javascript_language);
16083 cx.update_buffer(|buffer, cx| {
16084 buffer.set_language(Some(html_language), cx);
16085 });
16086
16087 // Toggle comments for empty selections
16088 cx.set_state(
16089 &r#"
16090 <p>A</p>ˇ
16091 <p>B</p>ˇ
16092 <p>C</p>ˇ
16093 "#
16094 .unindent(),
16095 );
16096 cx.update_editor(|editor, window, cx| {
16097 editor.toggle_comments(&ToggleComments::default(), window, cx)
16098 });
16099 cx.assert_editor_state(
16100 &r#"
16101 <!-- <p>A</p>ˇ -->
16102 <!-- <p>B</p>ˇ -->
16103 <!-- <p>C</p>ˇ -->
16104 "#
16105 .unindent(),
16106 );
16107 cx.update_editor(|editor, window, cx| {
16108 editor.toggle_comments(&ToggleComments::default(), window, cx)
16109 });
16110 cx.assert_editor_state(
16111 &r#"
16112 <p>A</p>ˇ
16113 <p>B</p>ˇ
16114 <p>C</p>ˇ
16115 "#
16116 .unindent(),
16117 );
16118
16119 // Toggle comments for mixture of empty and non-empty selections, where
16120 // multiple selections occupy a given line.
16121 cx.set_state(
16122 &r#"
16123 <p>A«</p>
16124 <p>ˇ»B</p>ˇ
16125 <p>C«</p>
16126 <p>ˇ»D</p>ˇ
16127 "#
16128 .unindent(),
16129 );
16130
16131 cx.update_editor(|editor, window, cx| {
16132 editor.toggle_comments(&ToggleComments::default(), window, cx)
16133 });
16134 cx.assert_editor_state(
16135 &r#"
16136 <!-- <p>A«</p>
16137 <p>ˇ»B</p>ˇ -->
16138 <!-- <p>C«</p>
16139 <p>ˇ»D</p>ˇ -->
16140 "#
16141 .unindent(),
16142 );
16143 cx.update_editor(|editor, window, cx| {
16144 editor.toggle_comments(&ToggleComments::default(), window, cx)
16145 });
16146 cx.assert_editor_state(
16147 &r#"
16148 <p>A«</p>
16149 <p>ˇ»B</p>ˇ
16150 <p>C«</p>
16151 <p>ˇ»D</p>ˇ
16152 "#
16153 .unindent(),
16154 );
16155
16156 // Toggle comments when different languages are active for different
16157 // selections.
16158 cx.set_state(
16159 &r#"
16160 ˇ<script>
16161 ˇvar x = new Y();
16162 ˇ</script>
16163 "#
16164 .unindent(),
16165 );
16166 cx.executor().run_until_parked();
16167 cx.update_editor(|editor, window, cx| {
16168 editor.toggle_comments(&ToggleComments::default(), window, cx)
16169 });
16170 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
16171 // Uncommenting and commenting from this position brings in even more wrong artifacts.
16172 cx.assert_editor_state(
16173 &r#"
16174 <!-- ˇ<script> -->
16175 // ˇvar x = new Y();
16176 <!-- ˇ</script> -->
16177 "#
16178 .unindent(),
16179 );
16180}
16181
16182#[gpui::test]
16183fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
16184 init_test(cx, |_| {});
16185
16186 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16187 let multibuffer = cx.new(|cx| {
16188 let mut multibuffer = MultiBuffer::new(ReadWrite);
16189 multibuffer.push_excerpts(
16190 buffer.clone(),
16191 [
16192 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
16193 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
16194 ],
16195 cx,
16196 );
16197 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
16198 multibuffer
16199 });
16200
16201 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16202 editor.update_in(cx, |editor, window, cx| {
16203 assert_eq!(editor.text(cx), "aaaa\nbbbb");
16204 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16205 s.select_ranges([
16206 Point::new(0, 0)..Point::new(0, 0),
16207 Point::new(1, 0)..Point::new(1, 0),
16208 ])
16209 });
16210
16211 editor.handle_input("X", window, cx);
16212 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
16213 assert_eq!(
16214 editor.selections.ranges(&editor.display_snapshot(cx)),
16215 [
16216 Point::new(0, 1)..Point::new(0, 1),
16217 Point::new(1, 1)..Point::new(1, 1),
16218 ]
16219 );
16220
16221 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
16222 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16223 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
16224 });
16225 editor.backspace(&Default::default(), window, cx);
16226 assert_eq!(editor.text(cx), "Xa\nbbb");
16227 assert_eq!(
16228 editor.selections.ranges(&editor.display_snapshot(cx)),
16229 [Point::new(1, 0)..Point::new(1, 0)]
16230 );
16231
16232 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16233 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
16234 });
16235 editor.backspace(&Default::default(), window, cx);
16236 assert_eq!(editor.text(cx), "X\nbb");
16237 assert_eq!(
16238 editor.selections.ranges(&editor.display_snapshot(cx)),
16239 [Point::new(0, 1)..Point::new(0, 1)]
16240 );
16241 });
16242}
16243
16244#[gpui::test]
16245fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
16246 init_test(cx, |_| {});
16247
16248 let markers = vec![('[', ']').into(), ('(', ')').into()];
16249 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
16250 indoc! {"
16251 [aaaa
16252 (bbbb]
16253 cccc)",
16254 },
16255 markers.clone(),
16256 );
16257 let excerpt_ranges = markers.into_iter().map(|marker| {
16258 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
16259 ExcerptRange::new(context)
16260 });
16261 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
16262 let multibuffer = cx.new(|cx| {
16263 let mut multibuffer = MultiBuffer::new(ReadWrite);
16264 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
16265 multibuffer
16266 });
16267
16268 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
16269 editor.update_in(cx, |editor, window, cx| {
16270 let (expected_text, selection_ranges) = marked_text_ranges(
16271 indoc! {"
16272 aaaa
16273 bˇbbb
16274 bˇbbˇb
16275 cccc"
16276 },
16277 true,
16278 );
16279 assert_eq!(editor.text(cx), expected_text);
16280 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16281 s.select_ranges(selection_ranges)
16282 });
16283
16284 editor.handle_input("X", window, cx);
16285
16286 let (expected_text, expected_selections) = marked_text_ranges(
16287 indoc! {"
16288 aaaa
16289 bXˇbbXb
16290 bXˇbbXˇb
16291 cccc"
16292 },
16293 false,
16294 );
16295 assert_eq!(editor.text(cx), expected_text);
16296 assert_eq!(
16297 editor.selections.ranges(&editor.display_snapshot(cx)),
16298 expected_selections
16299 );
16300
16301 editor.newline(&Newline, window, cx);
16302 let (expected_text, expected_selections) = marked_text_ranges(
16303 indoc! {"
16304 aaaa
16305 bX
16306 ˇbbX
16307 b
16308 bX
16309 ˇbbX
16310 ˇb
16311 cccc"
16312 },
16313 false,
16314 );
16315 assert_eq!(editor.text(cx), expected_text);
16316 assert_eq!(
16317 editor.selections.ranges(&editor.display_snapshot(cx)),
16318 expected_selections
16319 );
16320 });
16321}
16322
16323#[gpui::test]
16324fn test_refresh_selections(cx: &mut TestAppContext) {
16325 init_test(cx, |_| {});
16326
16327 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16328 let mut excerpt1_id = None;
16329 let multibuffer = cx.new(|cx| {
16330 let mut multibuffer = MultiBuffer::new(ReadWrite);
16331 excerpt1_id = multibuffer
16332 .push_excerpts(
16333 buffer.clone(),
16334 [
16335 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16336 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16337 ],
16338 cx,
16339 )
16340 .into_iter()
16341 .next();
16342 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16343 multibuffer
16344 });
16345
16346 let editor = cx.add_window(|window, cx| {
16347 let mut editor = build_editor(multibuffer.clone(), window, cx);
16348 let snapshot = editor.snapshot(window, cx);
16349 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16350 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
16351 });
16352 editor.begin_selection(
16353 Point::new(2, 1).to_display_point(&snapshot),
16354 true,
16355 1,
16356 window,
16357 cx,
16358 );
16359 assert_eq!(
16360 editor.selections.ranges(&editor.display_snapshot(cx)),
16361 [
16362 Point::new(1, 3)..Point::new(1, 3),
16363 Point::new(2, 1)..Point::new(2, 1),
16364 ]
16365 );
16366 editor
16367 });
16368
16369 // Refreshing selections is a no-op when excerpts haven't changed.
16370 _ = editor.update(cx, |editor, window, cx| {
16371 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16372 assert_eq!(
16373 editor.selections.ranges(&editor.display_snapshot(cx)),
16374 [
16375 Point::new(1, 3)..Point::new(1, 3),
16376 Point::new(2, 1)..Point::new(2, 1),
16377 ]
16378 );
16379 });
16380
16381 multibuffer.update(cx, |multibuffer, cx| {
16382 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16383 });
16384 _ = editor.update(cx, |editor, window, cx| {
16385 // Removing an excerpt causes the first selection to become degenerate.
16386 assert_eq!(
16387 editor.selections.ranges(&editor.display_snapshot(cx)),
16388 [
16389 Point::new(0, 0)..Point::new(0, 0),
16390 Point::new(0, 1)..Point::new(0, 1)
16391 ]
16392 );
16393
16394 // Refreshing selections will relocate the first selection to the original buffer
16395 // location.
16396 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16397 assert_eq!(
16398 editor.selections.ranges(&editor.display_snapshot(cx)),
16399 [
16400 Point::new(0, 1)..Point::new(0, 1),
16401 Point::new(0, 3)..Point::new(0, 3)
16402 ]
16403 );
16404 assert!(editor.selections.pending_anchor().is_some());
16405 });
16406}
16407
16408#[gpui::test]
16409fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
16410 init_test(cx, |_| {});
16411
16412 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
16413 let mut excerpt1_id = None;
16414 let multibuffer = cx.new(|cx| {
16415 let mut multibuffer = MultiBuffer::new(ReadWrite);
16416 excerpt1_id = multibuffer
16417 .push_excerpts(
16418 buffer.clone(),
16419 [
16420 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
16421 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
16422 ],
16423 cx,
16424 )
16425 .into_iter()
16426 .next();
16427 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
16428 multibuffer
16429 });
16430
16431 let editor = cx.add_window(|window, cx| {
16432 let mut editor = build_editor(multibuffer.clone(), window, cx);
16433 let snapshot = editor.snapshot(window, cx);
16434 editor.begin_selection(
16435 Point::new(1, 3).to_display_point(&snapshot),
16436 false,
16437 1,
16438 window,
16439 cx,
16440 );
16441 assert_eq!(
16442 editor.selections.ranges(&editor.display_snapshot(cx)),
16443 [Point::new(1, 3)..Point::new(1, 3)]
16444 );
16445 editor
16446 });
16447
16448 multibuffer.update(cx, |multibuffer, cx| {
16449 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
16450 });
16451 _ = editor.update(cx, |editor, window, cx| {
16452 assert_eq!(
16453 editor.selections.ranges(&editor.display_snapshot(cx)),
16454 [Point::new(0, 0)..Point::new(0, 0)]
16455 );
16456
16457 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
16458 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| s.refresh());
16459 assert_eq!(
16460 editor.selections.ranges(&editor.display_snapshot(cx)),
16461 [Point::new(0, 3)..Point::new(0, 3)]
16462 );
16463 assert!(editor.selections.pending_anchor().is_some());
16464 });
16465}
16466
16467#[gpui::test]
16468async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
16469 init_test(cx, |_| {});
16470
16471 let language = Arc::new(
16472 Language::new(
16473 LanguageConfig {
16474 brackets: BracketPairConfig {
16475 pairs: vec![
16476 BracketPair {
16477 start: "{".to_string(),
16478 end: "}".to_string(),
16479 close: true,
16480 surround: true,
16481 newline: true,
16482 },
16483 BracketPair {
16484 start: "/* ".to_string(),
16485 end: " */".to_string(),
16486 close: true,
16487 surround: true,
16488 newline: true,
16489 },
16490 ],
16491 ..Default::default()
16492 },
16493 ..Default::default()
16494 },
16495 Some(tree_sitter_rust::LANGUAGE.into()),
16496 )
16497 .with_indents_query("")
16498 .unwrap(),
16499 );
16500
16501 let text = concat!(
16502 "{ }\n", //
16503 " x\n", //
16504 " /* */\n", //
16505 "x\n", //
16506 "{{} }\n", //
16507 );
16508
16509 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
16510 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
16511 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
16512 editor
16513 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
16514 .await;
16515
16516 editor.update_in(cx, |editor, window, cx| {
16517 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16518 s.select_display_ranges([
16519 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
16520 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
16521 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
16522 ])
16523 });
16524 editor.newline(&Newline, window, cx);
16525
16526 assert_eq!(
16527 editor.buffer().read(cx).read(cx).text(),
16528 concat!(
16529 "{ \n", // Suppress rustfmt
16530 "\n", //
16531 "}\n", //
16532 " x\n", //
16533 " /* \n", //
16534 " \n", //
16535 " */\n", //
16536 "x\n", //
16537 "{{} \n", //
16538 "}\n", //
16539 )
16540 );
16541 });
16542}
16543
16544#[gpui::test]
16545fn test_highlighted_ranges(cx: &mut TestAppContext) {
16546 init_test(cx, |_| {});
16547
16548 let editor = cx.add_window(|window, cx| {
16549 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
16550 build_editor(buffer, window, cx)
16551 });
16552
16553 _ = editor.update(cx, |editor, window, cx| {
16554 struct Type1;
16555 struct Type2;
16556
16557 let buffer = editor.buffer.read(cx).snapshot(cx);
16558
16559 let anchor_range =
16560 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
16561
16562 editor.highlight_background::<Type1>(
16563 &[
16564 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
16565 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
16566 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
16567 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
16568 ],
16569 |_| Hsla::red(),
16570 cx,
16571 );
16572 editor.highlight_background::<Type2>(
16573 &[
16574 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
16575 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
16576 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
16577 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
16578 ],
16579 |_| Hsla::green(),
16580 cx,
16581 );
16582
16583 let snapshot = editor.snapshot(window, cx);
16584 let highlighted_ranges = editor.sorted_background_highlights_in_range(
16585 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
16586 &snapshot,
16587 cx.theme(),
16588 );
16589 assert_eq!(
16590 highlighted_ranges,
16591 &[
16592 (
16593 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
16594 Hsla::green(),
16595 ),
16596 (
16597 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
16598 Hsla::red(),
16599 ),
16600 (
16601 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
16602 Hsla::green(),
16603 ),
16604 (
16605 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16606 Hsla::red(),
16607 ),
16608 ]
16609 );
16610 assert_eq!(
16611 editor.sorted_background_highlights_in_range(
16612 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
16613 &snapshot,
16614 cx.theme(),
16615 ),
16616 &[(
16617 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
16618 Hsla::red(),
16619 )]
16620 );
16621 });
16622}
16623
16624#[gpui::test]
16625async fn test_following(cx: &mut TestAppContext) {
16626 init_test(cx, |_| {});
16627
16628 let fs = FakeFs::new(cx.executor());
16629 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16630
16631 let buffer = project.update(cx, |project, cx| {
16632 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, false, cx);
16633 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
16634 });
16635 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
16636 let follower = cx.update(|cx| {
16637 cx.open_window(
16638 WindowOptions {
16639 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
16640 gpui::Point::new(px(0.), px(0.)),
16641 gpui::Point::new(px(10.), px(80.)),
16642 ))),
16643 ..Default::default()
16644 },
16645 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
16646 )
16647 .unwrap()
16648 });
16649
16650 let is_still_following = Rc::new(RefCell::new(true));
16651 let follower_edit_event_count = Rc::new(RefCell::new(0));
16652 let pending_update = Rc::new(RefCell::new(None));
16653 let leader_entity = leader.root(cx).unwrap();
16654 let follower_entity = follower.root(cx).unwrap();
16655 _ = follower.update(cx, {
16656 let update = pending_update.clone();
16657 let is_still_following = is_still_following.clone();
16658 let follower_edit_event_count = follower_edit_event_count.clone();
16659 |_, window, cx| {
16660 cx.subscribe_in(
16661 &leader_entity,
16662 window,
16663 move |_, leader, event, window, cx| {
16664 leader.read(cx).add_event_to_update_proto(
16665 event,
16666 &mut update.borrow_mut(),
16667 window,
16668 cx,
16669 );
16670 },
16671 )
16672 .detach();
16673
16674 cx.subscribe_in(
16675 &follower_entity,
16676 window,
16677 move |_, _, event: &EditorEvent, _window, _cx| {
16678 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
16679 *is_still_following.borrow_mut() = false;
16680 }
16681
16682 if let EditorEvent::BufferEdited = event {
16683 *follower_edit_event_count.borrow_mut() += 1;
16684 }
16685 },
16686 )
16687 .detach();
16688 }
16689 });
16690
16691 // Update the selections only
16692 _ = leader.update(cx, |leader, window, cx| {
16693 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16694 s.select_ranges([1..1])
16695 });
16696 });
16697 follower
16698 .update(cx, |follower, window, cx| {
16699 follower.apply_update_proto(
16700 &project,
16701 pending_update.borrow_mut().take().unwrap(),
16702 window,
16703 cx,
16704 )
16705 })
16706 .unwrap()
16707 .await
16708 .unwrap();
16709 _ = follower.update(cx, |follower, _, cx| {
16710 assert_eq!(
16711 follower.selections.ranges(&follower.display_snapshot(cx)),
16712 vec![1..1]
16713 );
16714 });
16715 assert!(*is_still_following.borrow());
16716 assert_eq!(*follower_edit_event_count.borrow(), 0);
16717
16718 // Update the scroll position only
16719 _ = leader.update(cx, |leader, window, cx| {
16720 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16721 });
16722 follower
16723 .update(cx, |follower, window, cx| {
16724 follower.apply_update_proto(
16725 &project,
16726 pending_update.borrow_mut().take().unwrap(),
16727 window,
16728 cx,
16729 )
16730 })
16731 .unwrap()
16732 .await
16733 .unwrap();
16734 assert_eq!(
16735 follower
16736 .update(cx, |follower, _, cx| follower.scroll_position(cx))
16737 .unwrap(),
16738 gpui::Point::new(1.5, 3.5)
16739 );
16740 assert!(*is_still_following.borrow());
16741 assert_eq!(*follower_edit_event_count.borrow(), 0);
16742
16743 // Update the selections and scroll position. The follower's scroll position is updated
16744 // via autoscroll, not via the leader's exact scroll position.
16745 _ = leader.update(cx, |leader, window, cx| {
16746 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16747 s.select_ranges([0..0])
16748 });
16749 leader.request_autoscroll(Autoscroll::newest(), cx);
16750 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
16751 });
16752 follower
16753 .update(cx, |follower, window, cx| {
16754 follower.apply_update_proto(
16755 &project,
16756 pending_update.borrow_mut().take().unwrap(),
16757 window,
16758 cx,
16759 )
16760 })
16761 .unwrap()
16762 .await
16763 .unwrap();
16764 _ = follower.update(cx, |follower, _, cx| {
16765 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
16766 assert_eq!(
16767 follower.selections.ranges(&follower.display_snapshot(cx)),
16768 vec![0..0]
16769 );
16770 });
16771 assert!(*is_still_following.borrow());
16772
16773 // Creating a pending selection that precedes another selection
16774 _ = leader.update(cx, |leader, window, cx| {
16775 leader.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
16776 s.select_ranges([1..1])
16777 });
16778 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
16779 });
16780 follower
16781 .update(cx, |follower, window, cx| {
16782 follower.apply_update_proto(
16783 &project,
16784 pending_update.borrow_mut().take().unwrap(),
16785 window,
16786 cx,
16787 )
16788 })
16789 .unwrap()
16790 .await
16791 .unwrap();
16792 _ = follower.update(cx, |follower, _, cx| {
16793 assert_eq!(
16794 follower.selections.ranges(&follower.display_snapshot(cx)),
16795 vec![0..0, 1..1]
16796 );
16797 });
16798 assert!(*is_still_following.borrow());
16799
16800 // Extend the pending selection so that it surrounds another selection
16801 _ = leader.update(cx, |leader, window, cx| {
16802 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
16803 });
16804 follower
16805 .update(cx, |follower, window, cx| {
16806 follower.apply_update_proto(
16807 &project,
16808 pending_update.borrow_mut().take().unwrap(),
16809 window,
16810 cx,
16811 )
16812 })
16813 .unwrap()
16814 .await
16815 .unwrap();
16816 _ = follower.update(cx, |follower, _, cx| {
16817 assert_eq!(
16818 follower.selections.ranges(&follower.display_snapshot(cx)),
16819 vec![0..2]
16820 );
16821 });
16822
16823 // Scrolling locally breaks the follow
16824 _ = follower.update(cx, |follower, window, cx| {
16825 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
16826 follower.set_scroll_anchor(
16827 ScrollAnchor {
16828 anchor: top_anchor,
16829 offset: gpui::Point::new(0.0, 0.5),
16830 },
16831 window,
16832 cx,
16833 );
16834 });
16835 assert!(!(*is_still_following.borrow()));
16836}
16837
16838#[gpui::test]
16839async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
16840 init_test(cx, |_| {});
16841
16842 let fs = FakeFs::new(cx.executor());
16843 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
16844 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
16845 let pane = workspace
16846 .update(cx, |workspace, _, _| workspace.active_pane().clone())
16847 .unwrap();
16848
16849 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
16850
16851 let leader = pane.update_in(cx, |_, window, cx| {
16852 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
16853 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
16854 });
16855
16856 // Start following the editor when it has no excerpts.
16857 let mut state_message =
16858 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16859 let workspace_entity = workspace.root(cx).unwrap();
16860 let follower_1 = cx
16861 .update_window(*workspace.deref(), |_, window, cx| {
16862 Editor::from_state_proto(
16863 workspace_entity,
16864 ViewId {
16865 creator: CollaboratorId::PeerId(PeerId::default()),
16866 id: 0,
16867 },
16868 &mut state_message,
16869 window,
16870 cx,
16871 )
16872 })
16873 .unwrap()
16874 .unwrap()
16875 .await
16876 .unwrap();
16877
16878 let update_message = Rc::new(RefCell::new(None));
16879 follower_1.update_in(cx, {
16880 let update = update_message.clone();
16881 |_, window, cx| {
16882 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
16883 leader.read(cx).add_event_to_update_proto(
16884 event,
16885 &mut update.borrow_mut(),
16886 window,
16887 cx,
16888 );
16889 })
16890 .detach();
16891 }
16892 });
16893
16894 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
16895 (
16896 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, false, cx),
16897 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, false, cx),
16898 )
16899 });
16900
16901 // Insert some excerpts.
16902 leader.update(cx, |leader, cx| {
16903 leader.buffer.update(cx, |multibuffer, cx| {
16904 multibuffer.set_excerpts_for_path(
16905 PathKey::with_sort_prefix(1, rel_path("b.txt").into_arc()),
16906 buffer_1.clone(),
16907 vec![
16908 Point::row_range(0..3),
16909 Point::row_range(1..6),
16910 Point::row_range(12..15),
16911 ],
16912 0,
16913 cx,
16914 );
16915 multibuffer.set_excerpts_for_path(
16916 PathKey::with_sort_prefix(1, rel_path("a.txt").into_arc()),
16917 buffer_2.clone(),
16918 vec![Point::row_range(0..6), Point::row_range(8..12)],
16919 0,
16920 cx,
16921 );
16922 });
16923 });
16924
16925 // Apply the update of adding the excerpts.
16926 follower_1
16927 .update_in(cx, |follower, window, cx| {
16928 follower.apply_update_proto(
16929 &project,
16930 update_message.borrow().clone().unwrap(),
16931 window,
16932 cx,
16933 )
16934 })
16935 .await
16936 .unwrap();
16937 assert_eq!(
16938 follower_1.update(cx, |editor, cx| editor.text(cx)),
16939 leader.update(cx, |editor, cx| editor.text(cx))
16940 );
16941 update_message.borrow_mut().take();
16942
16943 // Start following separately after it already has excerpts.
16944 let mut state_message =
16945 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
16946 let workspace_entity = workspace.root(cx).unwrap();
16947 let follower_2 = cx
16948 .update_window(*workspace.deref(), |_, window, cx| {
16949 Editor::from_state_proto(
16950 workspace_entity,
16951 ViewId {
16952 creator: CollaboratorId::PeerId(PeerId::default()),
16953 id: 0,
16954 },
16955 &mut state_message,
16956 window,
16957 cx,
16958 )
16959 })
16960 .unwrap()
16961 .unwrap()
16962 .await
16963 .unwrap();
16964 assert_eq!(
16965 follower_2.update(cx, |editor, cx| editor.text(cx)),
16966 leader.update(cx, |editor, cx| editor.text(cx))
16967 );
16968
16969 // Remove some excerpts.
16970 leader.update(cx, |leader, cx| {
16971 leader.buffer.update(cx, |multibuffer, cx| {
16972 let excerpt_ids = multibuffer.excerpt_ids();
16973 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
16974 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
16975 });
16976 });
16977
16978 // Apply the update of removing the excerpts.
16979 follower_1
16980 .update_in(cx, |follower, window, cx| {
16981 follower.apply_update_proto(
16982 &project,
16983 update_message.borrow().clone().unwrap(),
16984 window,
16985 cx,
16986 )
16987 })
16988 .await
16989 .unwrap();
16990 follower_2
16991 .update_in(cx, |follower, window, cx| {
16992 follower.apply_update_proto(
16993 &project,
16994 update_message.borrow().clone().unwrap(),
16995 window,
16996 cx,
16997 )
16998 })
16999 .await
17000 .unwrap();
17001 update_message.borrow_mut().take();
17002 assert_eq!(
17003 follower_1.update(cx, |editor, cx| editor.text(cx)),
17004 leader.update(cx, |editor, cx| editor.text(cx))
17005 );
17006}
17007
17008#[gpui::test]
17009async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17010 init_test(cx, |_| {});
17011
17012 let mut cx = EditorTestContext::new(cx).await;
17013 let lsp_store =
17014 cx.update_editor(|editor, _, cx| editor.project().unwrap().read(cx).lsp_store());
17015
17016 cx.set_state(indoc! {"
17017 ˇfn func(abc def: i32) -> u32 {
17018 }
17019 "});
17020
17021 cx.update(|_, cx| {
17022 lsp_store.update(cx, |lsp_store, cx| {
17023 lsp_store
17024 .update_diagnostics(
17025 LanguageServerId(0),
17026 lsp::PublishDiagnosticsParams {
17027 uri: lsp::Uri::from_file_path(path!("/root/file")).unwrap(),
17028 version: None,
17029 diagnostics: vec![
17030 lsp::Diagnostic {
17031 range: lsp::Range::new(
17032 lsp::Position::new(0, 11),
17033 lsp::Position::new(0, 12),
17034 ),
17035 severity: Some(lsp::DiagnosticSeverity::ERROR),
17036 ..Default::default()
17037 },
17038 lsp::Diagnostic {
17039 range: lsp::Range::new(
17040 lsp::Position::new(0, 12),
17041 lsp::Position::new(0, 15),
17042 ),
17043 severity: Some(lsp::DiagnosticSeverity::ERROR),
17044 ..Default::default()
17045 },
17046 lsp::Diagnostic {
17047 range: lsp::Range::new(
17048 lsp::Position::new(0, 25),
17049 lsp::Position::new(0, 28),
17050 ),
17051 severity: Some(lsp::DiagnosticSeverity::ERROR),
17052 ..Default::default()
17053 },
17054 ],
17055 },
17056 None,
17057 DiagnosticSourceKind::Pushed,
17058 &[],
17059 cx,
17060 )
17061 .unwrap()
17062 });
17063 });
17064
17065 executor.run_until_parked();
17066
17067 cx.update_editor(|editor, window, cx| {
17068 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17069 });
17070
17071 cx.assert_editor_state(indoc! {"
17072 fn func(abc def: i32) -> ˇu32 {
17073 }
17074 "});
17075
17076 cx.update_editor(|editor, window, cx| {
17077 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17078 });
17079
17080 cx.assert_editor_state(indoc! {"
17081 fn func(abc ˇdef: i32) -> u32 {
17082 }
17083 "});
17084
17085 cx.update_editor(|editor, window, cx| {
17086 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17087 });
17088
17089 cx.assert_editor_state(indoc! {"
17090 fn func(abcˇ def: i32) -> u32 {
17091 }
17092 "});
17093
17094 cx.update_editor(|editor, window, cx| {
17095 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic::default(), window, cx);
17096 });
17097
17098 cx.assert_editor_state(indoc! {"
17099 fn func(abc def: i32) -> ˇu32 {
17100 }
17101 "});
17102}
17103
17104#[gpui::test]
17105async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17106 init_test(cx, |_| {});
17107
17108 let mut cx = EditorTestContext::new(cx).await;
17109
17110 let diff_base = r#"
17111 use some::mod;
17112
17113 const A: u32 = 42;
17114
17115 fn main() {
17116 println!("hello");
17117
17118 println!("world");
17119 }
17120 "#
17121 .unindent();
17122
17123 // Edits are modified, removed, modified, added
17124 cx.set_state(
17125 &r#"
17126 use some::modified;
17127
17128 ˇ
17129 fn main() {
17130 println!("hello there");
17131
17132 println!("around the");
17133 println!("world");
17134 }
17135 "#
17136 .unindent(),
17137 );
17138
17139 cx.set_head_text(&diff_base);
17140 executor.run_until_parked();
17141
17142 cx.update_editor(|editor, window, cx| {
17143 //Wrap around the bottom of the buffer
17144 for _ in 0..3 {
17145 editor.go_to_next_hunk(&GoToHunk, window, cx);
17146 }
17147 });
17148
17149 cx.assert_editor_state(
17150 &r#"
17151 ˇuse some::modified;
17152
17153
17154 fn main() {
17155 println!("hello there");
17156
17157 println!("around the");
17158 println!("world");
17159 }
17160 "#
17161 .unindent(),
17162 );
17163
17164 cx.update_editor(|editor, window, cx| {
17165 //Wrap around the top of the buffer
17166 for _ in 0..2 {
17167 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17168 }
17169 });
17170
17171 cx.assert_editor_state(
17172 &r#"
17173 use some::modified;
17174
17175
17176 fn main() {
17177 ˇ println!("hello there");
17178
17179 println!("around the");
17180 println!("world");
17181 }
17182 "#
17183 .unindent(),
17184 );
17185
17186 cx.update_editor(|editor, window, cx| {
17187 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17188 });
17189
17190 cx.assert_editor_state(
17191 &r#"
17192 use some::modified;
17193
17194 ˇ
17195 fn main() {
17196 println!("hello there");
17197
17198 println!("around the");
17199 println!("world");
17200 }
17201 "#
17202 .unindent(),
17203 );
17204
17205 cx.update_editor(|editor, window, cx| {
17206 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17207 });
17208
17209 cx.assert_editor_state(
17210 &r#"
17211 ˇuse some::modified;
17212
17213
17214 fn main() {
17215 println!("hello there");
17216
17217 println!("around the");
17218 println!("world");
17219 }
17220 "#
17221 .unindent(),
17222 );
17223
17224 cx.update_editor(|editor, window, cx| {
17225 for _ in 0..2 {
17226 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
17227 }
17228 });
17229
17230 cx.assert_editor_state(
17231 &r#"
17232 use some::modified;
17233
17234
17235 fn main() {
17236 ˇ println!("hello there");
17237
17238 println!("around the");
17239 println!("world");
17240 }
17241 "#
17242 .unindent(),
17243 );
17244
17245 cx.update_editor(|editor, window, cx| {
17246 editor.fold(&Fold, window, cx);
17247 });
17248
17249 cx.update_editor(|editor, window, cx| {
17250 editor.go_to_next_hunk(&GoToHunk, window, cx);
17251 });
17252
17253 cx.assert_editor_state(
17254 &r#"
17255 ˇuse some::modified;
17256
17257
17258 fn main() {
17259 println!("hello there");
17260
17261 println!("around the");
17262 println!("world");
17263 }
17264 "#
17265 .unindent(),
17266 );
17267}
17268
17269#[test]
17270fn test_split_words() {
17271 fn split(text: &str) -> Vec<&str> {
17272 split_words(text).collect()
17273 }
17274
17275 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
17276 assert_eq!(split("hello_world"), &["hello_", "world"]);
17277 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
17278 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
17279 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
17280 assert_eq!(split("helloworld"), &["helloworld"]);
17281
17282 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
17283}
17284
17285#[gpui::test]
17286async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
17287 init_test(cx, |_| {});
17288
17289 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
17290 let mut assert = |before, after| {
17291 let _state_context = cx.set_state(before);
17292 cx.run_until_parked();
17293 cx.update_editor(|editor, window, cx| {
17294 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
17295 });
17296 cx.run_until_parked();
17297 cx.assert_editor_state(after);
17298 };
17299
17300 // Outside bracket jumps to outside of matching bracket
17301 assert("console.logˇ(var);", "console.log(var)ˇ;");
17302 assert("console.log(var)ˇ;", "console.logˇ(var);");
17303
17304 // Inside bracket jumps to inside of matching bracket
17305 assert("console.log(ˇvar);", "console.log(varˇ);");
17306 assert("console.log(varˇ);", "console.log(ˇvar);");
17307
17308 // When outside a bracket and inside, favor jumping to the inside bracket
17309 assert(
17310 "console.log('foo', [1, 2, 3]ˇ);",
17311 "console.log(ˇ'foo', [1, 2, 3]);",
17312 );
17313 assert(
17314 "console.log(ˇ'foo', [1, 2, 3]);",
17315 "console.log('foo', [1, 2, 3]ˇ);",
17316 );
17317
17318 // Bias forward if two options are equally likely
17319 assert(
17320 "let result = curried_fun()ˇ();",
17321 "let result = curried_fun()()ˇ;",
17322 );
17323
17324 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
17325 assert(
17326 indoc! {"
17327 function test() {
17328 console.log('test')ˇ
17329 }"},
17330 indoc! {"
17331 function test() {
17332 console.logˇ('test')
17333 }"},
17334 );
17335}
17336
17337#[gpui::test]
17338async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
17339 init_test(cx, |_| {});
17340
17341 let fs = FakeFs::new(cx.executor());
17342 fs.insert_tree(
17343 path!("/a"),
17344 json!({
17345 "main.rs": "fn main() { let a = 5; }",
17346 "other.rs": "// Test file",
17347 }),
17348 )
17349 .await;
17350 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17351
17352 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17353 language_registry.add(Arc::new(Language::new(
17354 LanguageConfig {
17355 name: "Rust".into(),
17356 matcher: LanguageMatcher {
17357 path_suffixes: vec!["rs".to_string()],
17358 ..Default::default()
17359 },
17360 brackets: BracketPairConfig {
17361 pairs: vec![BracketPair {
17362 start: "{".to_string(),
17363 end: "}".to_string(),
17364 close: true,
17365 surround: true,
17366 newline: true,
17367 }],
17368 disabled_scopes_by_bracket_ix: Vec::new(),
17369 },
17370 ..Default::default()
17371 },
17372 Some(tree_sitter_rust::LANGUAGE.into()),
17373 )));
17374 let mut fake_servers = language_registry.register_fake_lsp(
17375 "Rust",
17376 FakeLspAdapter {
17377 capabilities: lsp::ServerCapabilities {
17378 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17379 first_trigger_character: "{".to_string(),
17380 more_trigger_character: None,
17381 }),
17382 ..Default::default()
17383 },
17384 ..Default::default()
17385 },
17386 );
17387
17388 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17389
17390 let cx = &mut VisualTestContext::from_window(*workspace, cx);
17391
17392 let worktree_id = workspace
17393 .update(cx, |workspace, _, cx| {
17394 workspace.project().update(cx, |project, cx| {
17395 project.worktrees(cx).next().unwrap().read(cx).id()
17396 })
17397 })
17398 .unwrap();
17399
17400 let buffer = project
17401 .update(cx, |project, cx| {
17402 project.open_local_buffer(path!("/a/main.rs"), cx)
17403 })
17404 .await
17405 .unwrap();
17406 let editor_handle = workspace
17407 .update(cx, |workspace, window, cx| {
17408 workspace.open_path((worktree_id, rel_path("main.rs")), None, true, window, cx)
17409 })
17410 .unwrap()
17411 .await
17412 .unwrap()
17413 .downcast::<Editor>()
17414 .unwrap();
17415
17416 cx.executor().start_waiting();
17417 let fake_server = fake_servers.next().await.unwrap();
17418
17419 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
17420 |params, _| async move {
17421 assert_eq!(
17422 params.text_document_position.text_document.uri,
17423 lsp::Uri::from_file_path(path!("/a/main.rs")).unwrap(),
17424 );
17425 assert_eq!(
17426 params.text_document_position.position,
17427 lsp::Position::new(0, 21),
17428 );
17429
17430 Ok(Some(vec![lsp::TextEdit {
17431 new_text: "]".to_string(),
17432 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17433 }]))
17434 },
17435 );
17436
17437 editor_handle.update_in(cx, |editor, window, cx| {
17438 window.focus(&editor.focus_handle(cx));
17439 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
17440 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
17441 });
17442 editor.handle_input("{", window, cx);
17443 });
17444
17445 cx.executor().run_until_parked();
17446
17447 buffer.update(cx, |buffer, _| {
17448 assert_eq!(
17449 buffer.text(),
17450 "fn main() { let a = {5}; }",
17451 "No extra braces from on type formatting should appear in the buffer"
17452 )
17453 });
17454}
17455
17456#[gpui::test(iterations = 20, seeds(31))]
17457async fn test_on_type_formatting_is_applied_after_autoindent(cx: &mut TestAppContext) {
17458 init_test(cx, |_| {});
17459
17460 let mut cx = EditorLspTestContext::new_rust(
17461 lsp::ServerCapabilities {
17462 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
17463 first_trigger_character: ".".to_string(),
17464 more_trigger_character: None,
17465 }),
17466 ..Default::default()
17467 },
17468 cx,
17469 )
17470 .await;
17471
17472 cx.update_buffer(|buffer, _| {
17473 // This causes autoindent to be async.
17474 buffer.set_sync_parse_timeout(Duration::ZERO)
17475 });
17476
17477 cx.set_state("fn c() {\n d()ˇ\n}\n");
17478 cx.simulate_keystroke("\n");
17479 cx.run_until_parked();
17480
17481 let buffer_cloned = cx.multibuffer(|multi_buffer, _| multi_buffer.as_singleton().unwrap());
17482 let mut request =
17483 cx.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(move |_, _, mut cx| {
17484 let buffer_cloned = buffer_cloned.clone();
17485 async move {
17486 buffer_cloned.update(&mut cx, |buffer, _| {
17487 assert_eq!(
17488 buffer.text(),
17489 "fn c() {\n d()\n .\n}\n",
17490 "OnTypeFormatting should triggered after autoindent applied"
17491 )
17492 })?;
17493
17494 Ok(Some(vec![]))
17495 }
17496 });
17497
17498 cx.simulate_keystroke(".");
17499 cx.run_until_parked();
17500
17501 cx.assert_editor_state("fn c() {\n d()\n .ˇ\n}\n");
17502 assert!(request.next().await.is_some());
17503 request.close();
17504 assert!(request.next().await.is_none());
17505}
17506
17507#[gpui::test]
17508async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
17509 init_test(cx, |_| {});
17510
17511 let fs = FakeFs::new(cx.executor());
17512 fs.insert_tree(
17513 path!("/a"),
17514 json!({
17515 "main.rs": "fn main() { let a = 5; }",
17516 "other.rs": "// Test file",
17517 }),
17518 )
17519 .await;
17520
17521 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17522
17523 let server_restarts = Arc::new(AtomicUsize::new(0));
17524 let closure_restarts = Arc::clone(&server_restarts);
17525 let language_server_name = "test language server";
17526 let language_name: LanguageName = "Rust".into();
17527
17528 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
17529 language_registry.add(Arc::new(Language::new(
17530 LanguageConfig {
17531 name: language_name.clone(),
17532 matcher: LanguageMatcher {
17533 path_suffixes: vec!["rs".to_string()],
17534 ..Default::default()
17535 },
17536 ..Default::default()
17537 },
17538 Some(tree_sitter_rust::LANGUAGE.into()),
17539 )));
17540 let mut fake_servers = language_registry.register_fake_lsp(
17541 "Rust",
17542 FakeLspAdapter {
17543 name: language_server_name,
17544 initialization_options: Some(json!({
17545 "testOptionValue": true
17546 })),
17547 initializer: Some(Box::new(move |fake_server| {
17548 let task_restarts = Arc::clone(&closure_restarts);
17549 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
17550 task_restarts.fetch_add(1, atomic::Ordering::Release);
17551 futures::future::ready(Ok(()))
17552 });
17553 })),
17554 ..Default::default()
17555 },
17556 );
17557
17558 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17559 let _buffer = project
17560 .update(cx, |project, cx| {
17561 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
17562 })
17563 .await
17564 .unwrap();
17565 let _fake_server = fake_servers.next().await.unwrap();
17566 update_test_language_settings(cx, |language_settings| {
17567 language_settings.languages.0.insert(
17568 language_name.clone().0,
17569 LanguageSettingsContent {
17570 tab_size: NonZeroU32::new(8),
17571 ..Default::default()
17572 },
17573 );
17574 });
17575 cx.executor().run_until_parked();
17576 assert_eq!(
17577 server_restarts.load(atomic::Ordering::Acquire),
17578 0,
17579 "Should not restart LSP server on an unrelated change"
17580 );
17581
17582 update_test_project_settings(cx, |project_settings| {
17583 project_settings.lsp.insert(
17584 "Some other server name".into(),
17585 LspSettings {
17586 binary: None,
17587 settings: None,
17588 initialization_options: Some(json!({
17589 "some other init value": false
17590 })),
17591 enable_lsp_tasks: false,
17592 fetch: None,
17593 },
17594 );
17595 });
17596 cx.executor().run_until_parked();
17597 assert_eq!(
17598 server_restarts.load(atomic::Ordering::Acquire),
17599 0,
17600 "Should not restart LSP server on an unrelated LSP settings change"
17601 );
17602
17603 update_test_project_settings(cx, |project_settings| {
17604 project_settings.lsp.insert(
17605 language_server_name.into(),
17606 LspSettings {
17607 binary: None,
17608 settings: None,
17609 initialization_options: Some(json!({
17610 "anotherInitValue": false
17611 })),
17612 enable_lsp_tasks: false,
17613 fetch: None,
17614 },
17615 );
17616 });
17617 cx.executor().run_until_parked();
17618 assert_eq!(
17619 server_restarts.load(atomic::Ordering::Acquire),
17620 1,
17621 "Should restart LSP server on a related LSP settings change"
17622 );
17623
17624 update_test_project_settings(cx, |project_settings| {
17625 project_settings.lsp.insert(
17626 language_server_name.into(),
17627 LspSettings {
17628 binary: None,
17629 settings: None,
17630 initialization_options: Some(json!({
17631 "anotherInitValue": false
17632 })),
17633 enable_lsp_tasks: false,
17634 fetch: None,
17635 },
17636 );
17637 });
17638 cx.executor().run_until_parked();
17639 assert_eq!(
17640 server_restarts.load(atomic::Ordering::Acquire),
17641 1,
17642 "Should not restart LSP server on a related LSP settings change that is the same"
17643 );
17644
17645 update_test_project_settings(cx, |project_settings| {
17646 project_settings.lsp.insert(
17647 language_server_name.into(),
17648 LspSettings {
17649 binary: None,
17650 settings: None,
17651 initialization_options: None,
17652 enable_lsp_tasks: false,
17653 fetch: None,
17654 },
17655 );
17656 });
17657 cx.executor().run_until_parked();
17658 assert_eq!(
17659 server_restarts.load(atomic::Ordering::Acquire),
17660 2,
17661 "Should restart LSP server on another related LSP settings change"
17662 );
17663}
17664
17665#[gpui::test]
17666async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
17667 init_test(cx, |_| {});
17668
17669 let mut cx = EditorLspTestContext::new_rust(
17670 lsp::ServerCapabilities {
17671 completion_provider: Some(lsp::CompletionOptions {
17672 trigger_characters: Some(vec![".".to_string()]),
17673 resolve_provider: Some(true),
17674 ..Default::default()
17675 }),
17676 ..Default::default()
17677 },
17678 cx,
17679 )
17680 .await;
17681
17682 cx.set_state("fn main() { let a = 2ˇ; }");
17683 cx.simulate_keystroke(".");
17684 let completion_item = lsp::CompletionItem {
17685 label: "some".into(),
17686 kind: Some(lsp::CompletionItemKind::SNIPPET),
17687 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
17688 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
17689 kind: lsp::MarkupKind::Markdown,
17690 value: "```rust\nSome(2)\n```".to_string(),
17691 })),
17692 deprecated: Some(false),
17693 sort_text: Some("fffffff2".to_string()),
17694 filter_text: Some("some".to_string()),
17695 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
17696 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17697 range: lsp::Range {
17698 start: lsp::Position {
17699 line: 0,
17700 character: 22,
17701 },
17702 end: lsp::Position {
17703 line: 0,
17704 character: 22,
17705 },
17706 },
17707 new_text: "Some(2)".to_string(),
17708 })),
17709 additional_text_edits: Some(vec![lsp::TextEdit {
17710 range: lsp::Range {
17711 start: lsp::Position {
17712 line: 0,
17713 character: 20,
17714 },
17715 end: lsp::Position {
17716 line: 0,
17717 character: 22,
17718 },
17719 },
17720 new_text: "".to_string(),
17721 }]),
17722 ..Default::default()
17723 };
17724
17725 let closure_completion_item = completion_item.clone();
17726 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
17727 let task_completion_item = closure_completion_item.clone();
17728 async move {
17729 Ok(Some(lsp::CompletionResponse::Array(vec![
17730 task_completion_item,
17731 ])))
17732 }
17733 });
17734
17735 request.next().await;
17736
17737 cx.condition(|editor, _| editor.context_menu_visible())
17738 .await;
17739 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
17740 editor
17741 .confirm_completion(&ConfirmCompletion::default(), window, cx)
17742 .unwrap()
17743 });
17744 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
17745
17746 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
17747 let task_completion_item = completion_item.clone();
17748 async move { Ok(task_completion_item) }
17749 })
17750 .next()
17751 .await
17752 .unwrap();
17753 apply_additional_edits.await.unwrap();
17754 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
17755}
17756
17757#[gpui::test]
17758async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
17759 init_test(cx, |_| {});
17760
17761 let mut cx = EditorLspTestContext::new_rust(
17762 lsp::ServerCapabilities {
17763 completion_provider: Some(lsp::CompletionOptions {
17764 trigger_characters: Some(vec![".".to_string()]),
17765 resolve_provider: Some(true),
17766 ..Default::default()
17767 }),
17768 ..Default::default()
17769 },
17770 cx,
17771 )
17772 .await;
17773
17774 cx.set_state("fn main() { let a = 2ˇ; }");
17775 cx.simulate_keystroke(".");
17776
17777 let item1 = lsp::CompletionItem {
17778 label: "method id()".to_string(),
17779 filter_text: Some("id".to_string()),
17780 detail: None,
17781 documentation: None,
17782 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17783 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17784 new_text: ".id".to_string(),
17785 })),
17786 ..lsp::CompletionItem::default()
17787 };
17788
17789 let item2 = lsp::CompletionItem {
17790 label: "other".to_string(),
17791 filter_text: Some("other".to_string()),
17792 detail: None,
17793 documentation: None,
17794 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17795 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
17796 new_text: ".other".to_string(),
17797 })),
17798 ..lsp::CompletionItem::default()
17799 };
17800
17801 let item1 = item1.clone();
17802 cx.set_request_handler::<lsp::request::Completion, _, _>({
17803 let item1 = item1.clone();
17804 move |_, _, _| {
17805 let item1 = item1.clone();
17806 let item2 = item2.clone();
17807 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
17808 }
17809 })
17810 .next()
17811 .await;
17812
17813 cx.condition(|editor, _| editor.context_menu_visible())
17814 .await;
17815 cx.update_editor(|editor, _, _| {
17816 let context_menu = editor.context_menu.borrow_mut();
17817 let context_menu = context_menu
17818 .as_ref()
17819 .expect("Should have the context menu deployed");
17820 match context_menu {
17821 CodeContextMenu::Completions(completions_menu) => {
17822 let completions = completions_menu.completions.borrow_mut();
17823 assert_eq!(
17824 completions
17825 .iter()
17826 .map(|completion| &completion.label.text)
17827 .collect::<Vec<_>>(),
17828 vec!["method id()", "other"]
17829 )
17830 }
17831 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17832 }
17833 });
17834
17835 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
17836 let item1 = item1.clone();
17837 move |_, item_to_resolve, _| {
17838 let item1 = item1.clone();
17839 async move {
17840 if item1 == item_to_resolve {
17841 Ok(lsp::CompletionItem {
17842 label: "method id()".to_string(),
17843 filter_text: Some("id".to_string()),
17844 detail: Some("Now resolved!".to_string()),
17845 documentation: Some(lsp::Documentation::String("Docs".to_string())),
17846 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
17847 range: lsp::Range::new(
17848 lsp::Position::new(0, 22),
17849 lsp::Position::new(0, 22),
17850 ),
17851 new_text: ".id".to_string(),
17852 })),
17853 ..lsp::CompletionItem::default()
17854 })
17855 } else {
17856 Ok(item_to_resolve)
17857 }
17858 }
17859 }
17860 })
17861 .next()
17862 .await
17863 .unwrap();
17864 cx.run_until_parked();
17865
17866 cx.update_editor(|editor, window, cx| {
17867 editor.context_menu_next(&Default::default(), window, cx);
17868 });
17869
17870 cx.update_editor(|editor, _, _| {
17871 let context_menu = editor.context_menu.borrow_mut();
17872 let context_menu = context_menu
17873 .as_ref()
17874 .expect("Should have the context menu deployed");
17875 match context_menu {
17876 CodeContextMenu::Completions(completions_menu) => {
17877 let completions = completions_menu.completions.borrow_mut();
17878 assert_eq!(
17879 completions
17880 .iter()
17881 .map(|completion| &completion.label.text)
17882 .collect::<Vec<_>>(),
17883 vec!["method id() Now resolved!", "other"],
17884 "Should update first completion label, but not second as the filter text did not match."
17885 );
17886 }
17887 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
17888 }
17889 });
17890}
17891
17892#[gpui::test]
17893async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
17894 init_test(cx, |_| {});
17895 let mut cx = EditorLspTestContext::new_rust(
17896 lsp::ServerCapabilities {
17897 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
17898 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
17899 completion_provider: Some(lsp::CompletionOptions {
17900 resolve_provider: Some(true),
17901 ..Default::default()
17902 }),
17903 ..Default::default()
17904 },
17905 cx,
17906 )
17907 .await;
17908 cx.set_state(indoc! {"
17909 struct TestStruct {
17910 field: i32
17911 }
17912
17913 fn mainˇ() {
17914 let unused_var = 42;
17915 let test_struct = TestStruct { field: 42 };
17916 }
17917 "});
17918 let symbol_range = cx.lsp_range(indoc! {"
17919 struct TestStruct {
17920 field: i32
17921 }
17922
17923 «fn main»() {
17924 let unused_var = 42;
17925 let test_struct = TestStruct { field: 42 };
17926 }
17927 "});
17928 let mut hover_requests =
17929 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
17930 Ok(Some(lsp::Hover {
17931 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
17932 kind: lsp::MarkupKind::Markdown,
17933 value: "Function documentation".to_string(),
17934 }),
17935 range: Some(symbol_range),
17936 }))
17937 });
17938
17939 // Case 1: Test that code action menu hide hover popover
17940 cx.dispatch_action(Hover);
17941 hover_requests.next().await;
17942 cx.condition(|editor, _| editor.hover_state.visible()).await;
17943 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
17944 move |_, _, _| async move {
17945 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
17946 lsp::CodeAction {
17947 title: "Remove unused variable".to_string(),
17948 kind: Some(CodeActionKind::QUICKFIX),
17949 edit: Some(lsp::WorkspaceEdit {
17950 changes: Some(
17951 [(
17952 lsp::Uri::from_file_path(path!("/file.rs")).unwrap(),
17953 vec![lsp::TextEdit {
17954 range: lsp::Range::new(
17955 lsp::Position::new(5, 4),
17956 lsp::Position::new(5, 27),
17957 ),
17958 new_text: "".to_string(),
17959 }],
17960 )]
17961 .into_iter()
17962 .collect(),
17963 ),
17964 ..Default::default()
17965 }),
17966 ..Default::default()
17967 },
17968 )]))
17969 },
17970 );
17971 cx.update_editor(|editor, window, cx| {
17972 editor.toggle_code_actions(
17973 &ToggleCodeActions {
17974 deployed_from: None,
17975 quick_launch: false,
17976 },
17977 window,
17978 cx,
17979 );
17980 });
17981 code_action_requests.next().await;
17982 cx.run_until_parked();
17983 cx.condition(|editor, _| editor.context_menu_visible())
17984 .await;
17985 cx.update_editor(|editor, _, _| {
17986 assert!(
17987 !editor.hover_state.visible(),
17988 "Hover popover should be hidden when code action menu is shown"
17989 );
17990 // Hide code actions
17991 editor.context_menu.take();
17992 });
17993
17994 // Case 2: Test that code completions hide hover popover
17995 cx.dispatch_action(Hover);
17996 hover_requests.next().await;
17997 cx.condition(|editor, _| editor.hover_state.visible()).await;
17998 let counter = Arc::new(AtomicUsize::new(0));
17999 let mut completion_requests =
18000 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18001 let counter = counter.clone();
18002 async move {
18003 counter.fetch_add(1, atomic::Ordering::Release);
18004 Ok(Some(lsp::CompletionResponse::Array(vec![
18005 lsp::CompletionItem {
18006 label: "main".into(),
18007 kind: Some(lsp::CompletionItemKind::FUNCTION),
18008 detail: Some("() -> ()".to_string()),
18009 ..Default::default()
18010 },
18011 lsp::CompletionItem {
18012 label: "TestStruct".into(),
18013 kind: Some(lsp::CompletionItemKind::STRUCT),
18014 detail: Some("struct TestStruct".to_string()),
18015 ..Default::default()
18016 },
18017 ])))
18018 }
18019 });
18020 cx.update_editor(|editor, window, cx| {
18021 editor.show_completions(&ShowCompletions, window, cx);
18022 });
18023 completion_requests.next().await;
18024 cx.condition(|editor, _| editor.context_menu_visible())
18025 .await;
18026 cx.update_editor(|editor, _, _| {
18027 assert!(
18028 !editor.hover_state.visible(),
18029 "Hover popover should be hidden when completion menu is shown"
18030 );
18031 });
18032}
18033
18034#[gpui::test]
18035async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
18036 init_test(cx, |_| {});
18037
18038 let mut cx = EditorLspTestContext::new_rust(
18039 lsp::ServerCapabilities {
18040 completion_provider: Some(lsp::CompletionOptions {
18041 trigger_characters: Some(vec![".".to_string()]),
18042 resolve_provider: Some(true),
18043 ..Default::default()
18044 }),
18045 ..Default::default()
18046 },
18047 cx,
18048 )
18049 .await;
18050
18051 cx.set_state("fn main() { let a = 2ˇ; }");
18052 cx.simulate_keystroke(".");
18053
18054 let unresolved_item_1 = lsp::CompletionItem {
18055 label: "id".to_string(),
18056 filter_text: Some("id".to_string()),
18057 detail: None,
18058 documentation: None,
18059 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18060 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18061 new_text: ".id".to_string(),
18062 })),
18063 ..lsp::CompletionItem::default()
18064 };
18065 let resolved_item_1 = lsp::CompletionItem {
18066 additional_text_edits: Some(vec![lsp::TextEdit {
18067 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18068 new_text: "!!".to_string(),
18069 }]),
18070 ..unresolved_item_1.clone()
18071 };
18072 let unresolved_item_2 = lsp::CompletionItem {
18073 label: "other".to_string(),
18074 filter_text: Some("other".to_string()),
18075 detail: None,
18076 documentation: None,
18077 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
18078 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
18079 new_text: ".other".to_string(),
18080 })),
18081 ..lsp::CompletionItem::default()
18082 };
18083 let resolved_item_2 = lsp::CompletionItem {
18084 additional_text_edits: Some(vec![lsp::TextEdit {
18085 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
18086 new_text: "??".to_string(),
18087 }]),
18088 ..unresolved_item_2.clone()
18089 };
18090
18091 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
18092 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
18093 cx.lsp
18094 .server
18095 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18096 let unresolved_item_1 = unresolved_item_1.clone();
18097 let resolved_item_1 = resolved_item_1.clone();
18098 let unresolved_item_2 = unresolved_item_2.clone();
18099 let resolved_item_2 = resolved_item_2.clone();
18100 let resolve_requests_1 = resolve_requests_1.clone();
18101 let resolve_requests_2 = resolve_requests_2.clone();
18102 move |unresolved_request, _| {
18103 let unresolved_item_1 = unresolved_item_1.clone();
18104 let resolved_item_1 = resolved_item_1.clone();
18105 let unresolved_item_2 = unresolved_item_2.clone();
18106 let resolved_item_2 = resolved_item_2.clone();
18107 let resolve_requests_1 = resolve_requests_1.clone();
18108 let resolve_requests_2 = resolve_requests_2.clone();
18109 async move {
18110 if unresolved_request == unresolved_item_1 {
18111 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
18112 Ok(resolved_item_1.clone())
18113 } else if unresolved_request == unresolved_item_2 {
18114 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
18115 Ok(resolved_item_2.clone())
18116 } else {
18117 panic!("Unexpected completion item {unresolved_request:?}")
18118 }
18119 }
18120 }
18121 })
18122 .detach();
18123
18124 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18125 let unresolved_item_1 = unresolved_item_1.clone();
18126 let unresolved_item_2 = unresolved_item_2.clone();
18127 async move {
18128 Ok(Some(lsp::CompletionResponse::Array(vec![
18129 unresolved_item_1,
18130 unresolved_item_2,
18131 ])))
18132 }
18133 })
18134 .next()
18135 .await;
18136
18137 cx.condition(|editor, _| editor.context_menu_visible())
18138 .await;
18139 cx.update_editor(|editor, _, _| {
18140 let context_menu = editor.context_menu.borrow_mut();
18141 let context_menu = context_menu
18142 .as_ref()
18143 .expect("Should have the context menu deployed");
18144 match context_menu {
18145 CodeContextMenu::Completions(completions_menu) => {
18146 let completions = completions_menu.completions.borrow_mut();
18147 assert_eq!(
18148 completions
18149 .iter()
18150 .map(|completion| &completion.label.text)
18151 .collect::<Vec<_>>(),
18152 vec!["id", "other"]
18153 )
18154 }
18155 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
18156 }
18157 });
18158 cx.run_until_parked();
18159
18160 cx.update_editor(|editor, window, cx| {
18161 editor.context_menu_next(&ContextMenuNext, window, cx);
18162 });
18163 cx.run_until_parked();
18164 cx.update_editor(|editor, window, cx| {
18165 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18166 });
18167 cx.run_until_parked();
18168 cx.update_editor(|editor, window, cx| {
18169 editor.context_menu_next(&ContextMenuNext, window, cx);
18170 });
18171 cx.run_until_parked();
18172 cx.update_editor(|editor, window, cx| {
18173 editor
18174 .compose_completion(&ComposeCompletion::default(), window, cx)
18175 .expect("No task returned")
18176 })
18177 .await
18178 .expect("Completion failed");
18179 cx.run_until_parked();
18180
18181 cx.update_editor(|editor, _, cx| {
18182 assert_eq!(
18183 resolve_requests_1.load(atomic::Ordering::Acquire),
18184 1,
18185 "Should always resolve once despite multiple selections"
18186 );
18187 assert_eq!(
18188 resolve_requests_2.load(atomic::Ordering::Acquire),
18189 1,
18190 "Should always resolve once after multiple selections and applying the completion"
18191 );
18192 assert_eq!(
18193 editor.text(cx),
18194 "fn main() { let a = ??.other; }",
18195 "Should use resolved data when applying the completion"
18196 );
18197 });
18198}
18199
18200#[gpui::test]
18201async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
18202 init_test(cx, |_| {});
18203
18204 let item_0 = lsp::CompletionItem {
18205 label: "abs".into(),
18206 insert_text: Some("abs".into()),
18207 data: Some(json!({ "very": "special"})),
18208 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
18209 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
18210 lsp::InsertReplaceEdit {
18211 new_text: "abs".to_string(),
18212 insert: lsp::Range::default(),
18213 replace: lsp::Range::default(),
18214 },
18215 )),
18216 ..lsp::CompletionItem::default()
18217 };
18218 let items = iter::once(item_0.clone())
18219 .chain((11..51).map(|i| lsp::CompletionItem {
18220 label: format!("item_{}", i),
18221 insert_text: Some(format!("item_{}", i)),
18222 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
18223 ..lsp::CompletionItem::default()
18224 }))
18225 .collect::<Vec<_>>();
18226
18227 let default_commit_characters = vec!["?".to_string()];
18228 let default_data = json!({ "default": "data"});
18229 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
18230 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
18231 let default_edit_range = lsp::Range {
18232 start: lsp::Position {
18233 line: 0,
18234 character: 5,
18235 },
18236 end: lsp::Position {
18237 line: 0,
18238 character: 5,
18239 },
18240 };
18241
18242 let mut cx = EditorLspTestContext::new_rust(
18243 lsp::ServerCapabilities {
18244 completion_provider: Some(lsp::CompletionOptions {
18245 trigger_characters: Some(vec![".".to_string()]),
18246 resolve_provider: Some(true),
18247 ..Default::default()
18248 }),
18249 ..Default::default()
18250 },
18251 cx,
18252 )
18253 .await;
18254
18255 cx.set_state("fn main() { let a = 2ˇ; }");
18256 cx.simulate_keystroke(".");
18257
18258 let completion_data = default_data.clone();
18259 let completion_characters = default_commit_characters.clone();
18260 let completion_items = items.clone();
18261 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
18262 let default_data = completion_data.clone();
18263 let default_commit_characters = completion_characters.clone();
18264 let items = completion_items.clone();
18265 async move {
18266 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
18267 items,
18268 item_defaults: Some(lsp::CompletionListItemDefaults {
18269 data: Some(default_data.clone()),
18270 commit_characters: Some(default_commit_characters.clone()),
18271 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
18272 default_edit_range,
18273 )),
18274 insert_text_format: Some(default_insert_text_format),
18275 insert_text_mode: Some(default_insert_text_mode),
18276 }),
18277 ..lsp::CompletionList::default()
18278 })))
18279 }
18280 })
18281 .next()
18282 .await;
18283
18284 let resolved_items = Arc::new(Mutex::new(Vec::new()));
18285 cx.lsp
18286 .server
18287 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
18288 let closure_resolved_items = resolved_items.clone();
18289 move |item_to_resolve, _| {
18290 let closure_resolved_items = closure_resolved_items.clone();
18291 async move {
18292 closure_resolved_items.lock().push(item_to_resolve.clone());
18293 Ok(item_to_resolve)
18294 }
18295 }
18296 })
18297 .detach();
18298
18299 cx.condition(|editor, _| editor.context_menu_visible())
18300 .await;
18301 cx.run_until_parked();
18302 cx.update_editor(|editor, _, _| {
18303 let menu = editor.context_menu.borrow_mut();
18304 match menu.as_ref().expect("should have the completions menu") {
18305 CodeContextMenu::Completions(completions_menu) => {
18306 assert_eq!(
18307 completions_menu
18308 .entries
18309 .borrow()
18310 .iter()
18311 .map(|mat| mat.string.clone())
18312 .collect::<Vec<String>>(),
18313 items
18314 .iter()
18315 .map(|completion| completion.label.clone())
18316 .collect::<Vec<String>>()
18317 );
18318 }
18319 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
18320 }
18321 });
18322 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
18323 // with 4 from the end.
18324 assert_eq!(
18325 *resolved_items.lock(),
18326 [&items[0..16], &items[items.len() - 4..items.len()]]
18327 .concat()
18328 .iter()
18329 .cloned()
18330 .map(|mut item| {
18331 if item.data.is_none() {
18332 item.data = Some(default_data.clone());
18333 }
18334 item
18335 })
18336 .collect::<Vec<lsp::CompletionItem>>(),
18337 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
18338 );
18339 resolved_items.lock().clear();
18340
18341 cx.update_editor(|editor, window, cx| {
18342 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
18343 });
18344 cx.run_until_parked();
18345 // Completions that have already been resolved are skipped.
18346 assert_eq!(
18347 *resolved_items.lock(),
18348 items[items.len() - 17..items.len() - 4]
18349 .iter()
18350 .cloned()
18351 .map(|mut item| {
18352 if item.data.is_none() {
18353 item.data = Some(default_data.clone());
18354 }
18355 item
18356 })
18357 .collect::<Vec<lsp::CompletionItem>>()
18358 );
18359 resolved_items.lock().clear();
18360}
18361
18362#[gpui::test]
18363async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
18364 init_test(cx, |_| {});
18365
18366 let mut cx = EditorLspTestContext::new(
18367 Language::new(
18368 LanguageConfig {
18369 matcher: LanguageMatcher {
18370 path_suffixes: vec!["jsx".into()],
18371 ..Default::default()
18372 },
18373 overrides: [(
18374 "element".into(),
18375 LanguageConfigOverride {
18376 completion_query_characters: Override::Set(['-'].into_iter().collect()),
18377 ..Default::default()
18378 },
18379 )]
18380 .into_iter()
18381 .collect(),
18382 ..Default::default()
18383 },
18384 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
18385 )
18386 .with_override_query("(jsx_self_closing_element) @element")
18387 .unwrap(),
18388 lsp::ServerCapabilities {
18389 completion_provider: Some(lsp::CompletionOptions {
18390 trigger_characters: Some(vec![":".to_string()]),
18391 ..Default::default()
18392 }),
18393 ..Default::default()
18394 },
18395 cx,
18396 )
18397 .await;
18398
18399 cx.lsp
18400 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
18401 Ok(Some(lsp::CompletionResponse::Array(vec![
18402 lsp::CompletionItem {
18403 label: "bg-blue".into(),
18404 ..Default::default()
18405 },
18406 lsp::CompletionItem {
18407 label: "bg-red".into(),
18408 ..Default::default()
18409 },
18410 lsp::CompletionItem {
18411 label: "bg-yellow".into(),
18412 ..Default::default()
18413 },
18414 ])))
18415 });
18416
18417 cx.set_state(r#"<p class="bgˇ" />"#);
18418
18419 // Trigger completion when typing a dash, because the dash is an extra
18420 // word character in the 'element' scope, which contains the cursor.
18421 cx.simulate_keystroke("-");
18422 cx.executor().run_until_parked();
18423 cx.update_editor(|editor, _, _| {
18424 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18425 {
18426 assert_eq!(
18427 completion_menu_entries(menu),
18428 &["bg-blue", "bg-red", "bg-yellow"]
18429 );
18430 } else {
18431 panic!("expected completion menu to be open");
18432 }
18433 });
18434
18435 cx.simulate_keystroke("l");
18436 cx.executor().run_until_parked();
18437 cx.update_editor(|editor, _, _| {
18438 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18439 {
18440 assert_eq!(completion_menu_entries(menu), &["bg-blue", "bg-yellow"]);
18441 } else {
18442 panic!("expected completion menu to be open");
18443 }
18444 });
18445
18446 // When filtering completions, consider the character after the '-' to
18447 // be the start of a subword.
18448 cx.set_state(r#"<p class="yelˇ" />"#);
18449 cx.simulate_keystroke("l");
18450 cx.executor().run_until_parked();
18451 cx.update_editor(|editor, _, _| {
18452 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
18453 {
18454 assert_eq!(completion_menu_entries(menu), &["bg-yellow"]);
18455 } else {
18456 panic!("expected completion menu to be open");
18457 }
18458 });
18459}
18460
18461fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
18462 let entries = menu.entries.borrow();
18463 entries.iter().map(|mat| mat.string.clone()).collect()
18464}
18465
18466#[gpui::test]
18467async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
18468 init_test(cx, |settings| {
18469 settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
18470 });
18471
18472 let fs = FakeFs::new(cx.executor());
18473 fs.insert_file(path!("/file.ts"), Default::default()).await;
18474
18475 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
18476 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
18477
18478 language_registry.add(Arc::new(Language::new(
18479 LanguageConfig {
18480 name: "TypeScript".into(),
18481 matcher: LanguageMatcher {
18482 path_suffixes: vec!["ts".to_string()],
18483 ..Default::default()
18484 },
18485 ..Default::default()
18486 },
18487 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
18488 )));
18489 update_test_language_settings(cx, |settings| {
18490 settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
18491 });
18492
18493 let test_plugin = "test_plugin";
18494 let _ = language_registry.register_fake_lsp(
18495 "TypeScript",
18496 FakeLspAdapter {
18497 prettier_plugins: vec![test_plugin],
18498 ..Default::default()
18499 },
18500 );
18501
18502 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
18503 let buffer = project
18504 .update(cx, |project, cx| {
18505 project.open_local_buffer(path!("/file.ts"), cx)
18506 })
18507 .await
18508 .unwrap();
18509
18510 let buffer_text = "one\ntwo\nthree\n";
18511 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
18512 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
18513 editor.update_in(cx, |editor, window, cx| {
18514 editor.set_text(buffer_text, window, cx)
18515 });
18516
18517 editor
18518 .update_in(cx, |editor, window, cx| {
18519 editor.perform_format(
18520 project.clone(),
18521 FormatTrigger::Manual,
18522 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18523 window,
18524 cx,
18525 )
18526 })
18527 .unwrap()
18528 .await;
18529 assert_eq!(
18530 editor.update(cx, |editor, cx| editor.text(cx)),
18531 buffer_text.to_string() + prettier_format_suffix,
18532 "Test prettier formatting was not applied to the original buffer text",
18533 );
18534
18535 update_test_language_settings(cx, |settings| {
18536 settings.defaults.formatter = Some(FormatterList::default())
18537 });
18538 let format = editor.update_in(cx, |editor, window, cx| {
18539 editor.perform_format(
18540 project.clone(),
18541 FormatTrigger::Manual,
18542 FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
18543 window,
18544 cx,
18545 )
18546 });
18547 format.await.unwrap();
18548 assert_eq!(
18549 editor.update(cx, |editor, cx| editor.text(cx)),
18550 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
18551 "Autoformatting (via test prettier) was not applied to the original buffer text",
18552 );
18553}
18554
18555#[gpui::test]
18556async fn test_addition_reverts(cx: &mut TestAppContext) {
18557 init_test(cx, |_| {});
18558 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18559 let base_text = indoc! {r#"
18560 struct Row;
18561 struct Row1;
18562 struct Row2;
18563
18564 struct Row4;
18565 struct Row5;
18566 struct Row6;
18567
18568 struct Row8;
18569 struct Row9;
18570 struct Row10;"#};
18571
18572 // When addition hunks are not adjacent to carets, no hunk revert is performed
18573 assert_hunk_revert(
18574 indoc! {r#"struct Row;
18575 struct Row1;
18576 struct Row1.1;
18577 struct Row1.2;
18578 struct Row2;ˇ
18579
18580 struct Row4;
18581 struct Row5;
18582 struct Row6;
18583
18584 struct Row8;
18585 ˇstruct Row9;
18586 struct Row9.1;
18587 struct Row9.2;
18588 struct Row9.3;
18589 struct Row10;"#},
18590 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18591 indoc! {r#"struct Row;
18592 struct Row1;
18593 struct Row1.1;
18594 struct Row1.2;
18595 struct Row2;ˇ
18596
18597 struct Row4;
18598 struct Row5;
18599 struct Row6;
18600
18601 struct Row8;
18602 ˇstruct Row9;
18603 struct Row9.1;
18604 struct Row9.2;
18605 struct Row9.3;
18606 struct Row10;"#},
18607 base_text,
18608 &mut cx,
18609 );
18610 // Same for selections
18611 assert_hunk_revert(
18612 indoc! {r#"struct Row;
18613 struct Row1;
18614 struct Row2;
18615 struct Row2.1;
18616 struct Row2.2;
18617 «ˇ
18618 struct Row4;
18619 struct» Row5;
18620 «struct Row6;
18621 ˇ»
18622 struct Row9.1;
18623 struct Row9.2;
18624 struct Row9.3;
18625 struct Row8;
18626 struct Row9;
18627 struct Row10;"#},
18628 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
18629 indoc! {r#"struct Row;
18630 struct Row1;
18631 struct Row2;
18632 struct Row2.1;
18633 struct Row2.2;
18634 «ˇ
18635 struct Row4;
18636 struct» Row5;
18637 «struct Row6;
18638 ˇ»
18639 struct Row9.1;
18640 struct Row9.2;
18641 struct Row9.3;
18642 struct Row8;
18643 struct Row9;
18644 struct Row10;"#},
18645 base_text,
18646 &mut cx,
18647 );
18648
18649 // When carets and selections intersect the addition hunks, those are reverted.
18650 // Adjacent carets got merged.
18651 assert_hunk_revert(
18652 indoc! {r#"struct Row;
18653 ˇ// something on the top
18654 struct Row1;
18655 struct Row2;
18656 struct Roˇw3.1;
18657 struct Row2.2;
18658 struct Row2.3;ˇ
18659
18660 struct Row4;
18661 struct ˇRow5.1;
18662 struct Row5.2;
18663 struct «Rowˇ»5.3;
18664 struct Row5;
18665 struct Row6;
18666 ˇ
18667 struct Row9.1;
18668 struct «Rowˇ»9.2;
18669 struct «ˇRow»9.3;
18670 struct Row8;
18671 struct Row9;
18672 «ˇ// something on bottom»
18673 struct Row10;"#},
18674 vec![
18675 DiffHunkStatusKind::Added,
18676 DiffHunkStatusKind::Added,
18677 DiffHunkStatusKind::Added,
18678 DiffHunkStatusKind::Added,
18679 DiffHunkStatusKind::Added,
18680 ],
18681 indoc! {r#"struct Row;
18682 ˇstruct Row1;
18683 struct Row2;
18684 ˇ
18685 struct Row4;
18686 ˇstruct Row5;
18687 struct Row6;
18688 ˇ
18689 ˇstruct Row8;
18690 struct Row9;
18691 ˇstruct Row10;"#},
18692 base_text,
18693 &mut cx,
18694 );
18695}
18696
18697#[gpui::test]
18698async fn test_modification_reverts(cx: &mut TestAppContext) {
18699 init_test(cx, |_| {});
18700 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18701 let base_text = indoc! {r#"
18702 struct Row;
18703 struct Row1;
18704 struct Row2;
18705
18706 struct Row4;
18707 struct Row5;
18708 struct Row6;
18709
18710 struct Row8;
18711 struct Row9;
18712 struct Row10;"#};
18713
18714 // Modification hunks behave the same as the addition ones.
18715 assert_hunk_revert(
18716 indoc! {r#"struct Row;
18717 struct Row1;
18718 struct Row33;
18719 ˇ
18720 struct Row4;
18721 struct Row5;
18722 struct Row6;
18723 ˇ
18724 struct Row99;
18725 struct Row9;
18726 struct Row10;"#},
18727 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18728 indoc! {r#"struct Row;
18729 struct Row1;
18730 struct Row33;
18731 ˇ
18732 struct Row4;
18733 struct Row5;
18734 struct Row6;
18735 ˇ
18736 struct Row99;
18737 struct Row9;
18738 struct Row10;"#},
18739 base_text,
18740 &mut cx,
18741 );
18742 assert_hunk_revert(
18743 indoc! {r#"struct Row;
18744 struct Row1;
18745 struct Row33;
18746 «ˇ
18747 struct Row4;
18748 struct» Row5;
18749 «struct Row6;
18750 ˇ»
18751 struct Row99;
18752 struct Row9;
18753 struct Row10;"#},
18754 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
18755 indoc! {r#"struct Row;
18756 struct Row1;
18757 struct Row33;
18758 «ˇ
18759 struct Row4;
18760 struct» Row5;
18761 «struct Row6;
18762 ˇ»
18763 struct Row99;
18764 struct Row9;
18765 struct Row10;"#},
18766 base_text,
18767 &mut cx,
18768 );
18769
18770 assert_hunk_revert(
18771 indoc! {r#"ˇstruct Row1.1;
18772 struct Row1;
18773 «ˇstr»uct Row22;
18774
18775 struct ˇRow44;
18776 struct Row5;
18777 struct «Rˇ»ow66;ˇ
18778
18779 «struˇ»ct Row88;
18780 struct Row9;
18781 struct Row1011;ˇ"#},
18782 vec![
18783 DiffHunkStatusKind::Modified,
18784 DiffHunkStatusKind::Modified,
18785 DiffHunkStatusKind::Modified,
18786 DiffHunkStatusKind::Modified,
18787 DiffHunkStatusKind::Modified,
18788 DiffHunkStatusKind::Modified,
18789 ],
18790 indoc! {r#"struct Row;
18791 ˇstruct Row1;
18792 struct Row2;
18793 ˇ
18794 struct Row4;
18795 ˇstruct Row5;
18796 struct Row6;
18797 ˇ
18798 struct Row8;
18799 ˇstruct Row9;
18800 struct Row10;ˇ"#},
18801 base_text,
18802 &mut cx,
18803 );
18804}
18805
18806#[gpui::test]
18807async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
18808 init_test(cx, |_| {});
18809 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18810 let base_text = indoc! {r#"
18811 one
18812
18813 two
18814 three
18815 "#};
18816
18817 cx.set_head_text(base_text);
18818 cx.set_state("\nˇ\n");
18819 cx.executor().run_until_parked();
18820 cx.update_editor(|editor, _window, cx| {
18821 editor.expand_selected_diff_hunks(cx);
18822 });
18823 cx.executor().run_until_parked();
18824 cx.update_editor(|editor, window, cx| {
18825 editor.backspace(&Default::default(), window, cx);
18826 });
18827 cx.run_until_parked();
18828 cx.assert_state_with_diff(
18829 indoc! {r#"
18830
18831 - two
18832 - threeˇ
18833 +
18834 "#}
18835 .to_string(),
18836 );
18837}
18838
18839#[gpui::test]
18840async fn test_deletion_reverts(cx: &mut TestAppContext) {
18841 init_test(cx, |_| {});
18842 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
18843 let base_text = indoc! {r#"struct Row;
18844struct Row1;
18845struct Row2;
18846
18847struct Row4;
18848struct Row5;
18849struct Row6;
18850
18851struct Row8;
18852struct Row9;
18853struct Row10;"#};
18854
18855 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
18856 assert_hunk_revert(
18857 indoc! {r#"struct Row;
18858 struct Row2;
18859
18860 ˇstruct Row4;
18861 struct Row5;
18862 struct Row6;
18863 ˇ
18864 struct Row8;
18865 struct Row10;"#},
18866 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18867 indoc! {r#"struct Row;
18868 struct Row2;
18869
18870 ˇstruct Row4;
18871 struct Row5;
18872 struct Row6;
18873 ˇ
18874 struct Row8;
18875 struct Row10;"#},
18876 base_text,
18877 &mut cx,
18878 );
18879 assert_hunk_revert(
18880 indoc! {r#"struct Row;
18881 struct Row2;
18882
18883 «ˇstruct Row4;
18884 struct» Row5;
18885 «struct Row6;
18886 ˇ»
18887 struct Row8;
18888 struct Row10;"#},
18889 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18890 indoc! {r#"struct Row;
18891 struct Row2;
18892
18893 «ˇstruct Row4;
18894 struct» Row5;
18895 «struct Row6;
18896 ˇ»
18897 struct Row8;
18898 struct Row10;"#},
18899 base_text,
18900 &mut cx,
18901 );
18902
18903 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
18904 assert_hunk_revert(
18905 indoc! {r#"struct Row;
18906 ˇstruct Row2;
18907
18908 struct Row4;
18909 struct Row5;
18910 struct Row6;
18911
18912 struct Row8;ˇ
18913 struct Row10;"#},
18914 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
18915 indoc! {r#"struct Row;
18916 struct Row1;
18917 ˇstruct Row2;
18918
18919 struct Row4;
18920 struct Row5;
18921 struct Row6;
18922
18923 struct Row8;ˇ
18924 struct Row9;
18925 struct Row10;"#},
18926 base_text,
18927 &mut cx,
18928 );
18929 assert_hunk_revert(
18930 indoc! {r#"struct Row;
18931 struct Row2«ˇ;
18932 struct Row4;
18933 struct» Row5;
18934 «struct Row6;
18935
18936 struct Row8;ˇ»
18937 struct Row10;"#},
18938 vec![
18939 DiffHunkStatusKind::Deleted,
18940 DiffHunkStatusKind::Deleted,
18941 DiffHunkStatusKind::Deleted,
18942 ],
18943 indoc! {r#"struct Row;
18944 struct Row1;
18945 struct Row2«ˇ;
18946
18947 struct Row4;
18948 struct» Row5;
18949 «struct Row6;
18950
18951 struct Row8;ˇ»
18952 struct Row9;
18953 struct Row10;"#},
18954 base_text,
18955 &mut cx,
18956 );
18957}
18958
18959#[gpui::test]
18960async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
18961 init_test(cx, |_| {});
18962
18963 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
18964 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
18965 let base_text_3 =
18966 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
18967
18968 let text_1 = edit_first_char_of_every_line(base_text_1);
18969 let text_2 = edit_first_char_of_every_line(base_text_2);
18970 let text_3 = edit_first_char_of_every_line(base_text_3);
18971
18972 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
18973 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
18974 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
18975
18976 let multibuffer = cx.new(|cx| {
18977 let mut multibuffer = MultiBuffer::new(ReadWrite);
18978 multibuffer.push_excerpts(
18979 buffer_1.clone(),
18980 [
18981 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18982 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18983 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18984 ],
18985 cx,
18986 );
18987 multibuffer.push_excerpts(
18988 buffer_2.clone(),
18989 [
18990 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18991 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18992 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18993 ],
18994 cx,
18995 );
18996 multibuffer.push_excerpts(
18997 buffer_3.clone(),
18998 [
18999 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19000 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19001 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19002 ],
19003 cx,
19004 );
19005 multibuffer
19006 });
19007
19008 let fs = FakeFs::new(cx.executor());
19009 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19010 let (editor, cx) = cx
19011 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
19012 editor.update_in(cx, |editor, _window, cx| {
19013 for (buffer, diff_base) in [
19014 (buffer_1.clone(), base_text_1),
19015 (buffer_2.clone(), base_text_2),
19016 (buffer_3.clone(), base_text_3),
19017 ] {
19018 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19019 editor
19020 .buffer
19021 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19022 }
19023 });
19024 cx.executor().run_until_parked();
19025
19026 editor.update_in(cx, |editor, window, cx| {
19027 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}");
19028 editor.select_all(&SelectAll, window, cx);
19029 editor.git_restore(&Default::default(), window, cx);
19030 });
19031 cx.executor().run_until_parked();
19032
19033 // When all ranges are selected, all buffer hunks are reverted.
19034 editor.update(cx, |editor, cx| {
19035 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");
19036 });
19037 buffer_1.update(cx, |buffer, _| {
19038 assert_eq!(buffer.text(), base_text_1);
19039 });
19040 buffer_2.update(cx, |buffer, _| {
19041 assert_eq!(buffer.text(), base_text_2);
19042 });
19043 buffer_3.update(cx, |buffer, _| {
19044 assert_eq!(buffer.text(), base_text_3);
19045 });
19046
19047 editor.update_in(cx, |editor, window, cx| {
19048 editor.undo(&Default::default(), window, cx);
19049 });
19050
19051 editor.update_in(cx, |editor, window, cx| {
19052 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
19053 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
19054 });
19055 editor.git_restore(&Default::default(), window, cx);
19056 });
19057
19058 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
19059 // but not affect buffer_2 and its related excerpts.
19060 editor.update(cx, |editor, cx| {
19061 assert_eq!(
19062 editor.text(cx),
19063 "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}"
19064 );
19065 });
19066 buffer_1.update(cx, |buffer, _| {
19067 assert_eq!(buffer.text(), base_text_1);
19068 });
19069 buffer_2.update(cx, |buffer, _| {
19070 assert_eq!(
19071 buffer.text(),
19072 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
19073 );
19074 });
19075 buffer_3.update(cx, |buffer, _| {
19076 assert_eq!(
19077 buffer.text(),
19078 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
19079 );
19080 });
19081
19082 fn edit_first_char_of_every_line(text: &str) -> String {
19083 text.split('\n')
19084 .map(|line| format!("X{}", &line[1..]))
19085 .collect::<Vec<_>>()
19086 .join("\n")
19087 }
19088}
19089
19090#[gpui::test]
19091async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
19092 init_test(cx, |_| {});
19093
19094 let cols = 4;
19095 let rows = 10;
19096 let sample_text_1 = sample_text(rows, cols, 'a');
19097 assert_eq!(
19098 sample_text_1,
19099 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
19100 );
19101 let sample_text_2 = sample_text(rows, cols, 'l');
19102 assert_eq!(
19103 sample_text_2,
19104 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
19105 );
19106 let sample_text_3 = sample_text(rows, cols, 'v');
19107 assert_eq!(
19108 sample_text_3,
19109 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
19110 );
19111
19112 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
19113 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
19114 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
19115
19116 let multi_buffer = cx.new(|cx| {
19117 let mut multibuffer = MultiBuffer::new(ReadWrite);
19118 multibuffer.push_excerpts(
19119 buffer_1.clone(),
19120 [
19121 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19122 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19123 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19124 ],
19125 cx,
19126 );
19127 multibuffer.push_excerpts(
19128 buffer_2.clone(),
19129 [
19130 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19131 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19132 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19133 ],
19134 cx,
19135 );
19136 multibuffer.push_excerpts(
19137 buffer_3.clone(),
19138 [
19139 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19140 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19141 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
19142 ],
19143 cx,
19144 );
19145 multibuffer
19146 });
19147
19148 let fs = FakeFs::new(cx.executor());
19149 fs.insert_tree(
19150 "/a",
19151 json!({
19152 "main.rs": sample_text_1,
19153 "other.rs": sample_text_2,
19154 "lib.rs": sample_text_3,
19155 }),
19156 )
19157 .await;
19158 let project = Project::test(fs, ["/a".as_ref()], cx).await;
19159 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19160 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19161 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
19162 Editor::new(
19163 EditorMode::full(),
19164 multi_buffer,
19165 Some(project.clone()),
19166 window,
19167 cx,
19168 )
19169 });
19170 let multibuffer_item_id = workspace
19171 .update(cx, |workspace, window, cx| {
19172 assert!(
19173 workspace.active_item(cx).is_none(),
19174 "active item should be None before the first item is added"
19175 );
19176 workspace.add_item_to_active_pane(
19177 Box::new(multi_buffer_editor.clone()),
19178 None,
19179 true,
19180 window,
19181 cx,
19182 );
19183 let active_item = workspace
19184 .active_item(cx)
19185 .expect("should have an active item after adding the multi buffer");
19186 assert_eq!(
19187 active_item.buffer_kind(cx),
19188 ItemBufferKind::Multibuffer,
19189 "A multi buffer was expected to active after adding"
19190 );
19191 active_item.item_id()
19192 })
19193 .unwrap();
19194 cx.executor().run_until_parked();
19195
19196 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19197 editor.change_selections(
19198 SelectionEffects::scroll(Autoscroll::Next),
19199 window,
19200 cx,
19201 |s| s.select_ranges(Some(1..2)),
19202 );
19203 editor.open_excerpts(&OpenExcerpts, window, cx);
19204 });
19205 cx.executor().run_until_parked();
19206 let first_item_id = workspace
19207 .update(cx, |workspace, window, cx| {
19208 let active_item = workspace
19209 .active_item(cx)
19210 .expect("should have an active item after navigating into the 1st buffer");
19211 let first_item_id = active_item.item_id();
19212 assert_ne!(
19213 first_item_id, multibuffer_item_id,
19214 "Should navigate into the 1st buffer and activate it"
19215 );
19216 assert_eq!(
19217 active_item.buffer_kind(cx),
19218 ItemBufferKind::Singleton,
19219 "New active item should be a singleton buffer"
19220 );
19221 assert_eq!(
19222 active_item
19223 .act_as::<Editor>(cx)
19224 .expect("should have navigated into an editor for the 1st buffer")
19225 .read(cx)
19226 .text(cx),
19227 sample_text_1
19228 );
19229
19230 workspace
19231 .go_back(workspace.active_pane().downgrade(), window, cx)
19232 .detach_and_log_err(cx);
19233
19234 first_item_id
19235 })
19236 .unwrap();
19237 cx.executor().run_until_parked();
19238 workspace
19239 .update(cx, |workspace, _, cx| {
19240 let active_item = workspace
19241 .active_item(cx)
19242 .expect("should have an active item after navigating back");
19243 assert_eq!(
19244 active_item.item_id(),
19245 multibuffer_item_id,
19246 "Should navigate back to the multi buffer"
19247 );
19248 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19249 })
19250 .unwrap();
19251
19252 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19253 editor.change_selections(
19254 SelectionEffects::scroll(Autoscroll::Next),
19255 window,
19256 cx,
19257 |s| s.select_ranges(Some(39..40)),
19258 );
19259 editor.open_excerpts(&OpenExcerpts, window, cx);
19260 });
19261 cx.executor().run_until_parked();
19262 let second_item_id = workspace
19263 .update(cx, |workspace, window, cx| {
19264 let active_item = workspace
19265 .active_item(cx)
19266 .expect("should have an active item after navigating into the 2nd buffer");
19267 let second_item_id = active_item.item_id();
19268 assert_ne!(
19269 second_item_id, multibuffer_item_id,
19270 "Should navigate away from the multibuffer"
19271 );
19272 assert_ne!(
19273 second_item_id, first_item_id,
19274 "Should navigate into the 2nd buffer and activate it"
19275 );
19276 assert_eq!(
19277 active_item.buffer_kind(cx),
19278 ItemBufferKind::Singleton,
19279 "New active item should be a singleton buffer"
19280 );
19281 assert_eq!(
19282 active_item
19283 .act_as::<Editor>(cx)
19284 .expect("should have navigated into an editor")
19285 .read(cx)
19286 .text(cx),
19287 sample_text_2
19288 );
19289
19290 workspace
19291 .go_back(workspace.active_pane().downgrade(), window, cx)
19292 .detach_and_log_err(cx);
19293
19294 second_item_id
19295 })
19296 .unwrap();
19297 cx.executor().run_until_parked();
19298 workspace
19299 .update(cx, |workspace, _, cx| {
19300 let active_item = workspace
19301 .active_item(cx)
19302 .expect("should have an active item after navigating back from the 2nd buffer");
19303 assert_eq!(
19304 active_item.item_id(),
19305 multibuffer_item_id,
19306 "Should navigate back from the 2nd buffer to the multi buffer"
19307 );
19308 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19309 })
19310 .unwrap();
19311
19312 multi_buffer_editor.update_in(cx, |editor, window, cx| {
19313 editor.change_selections(
19314 SelectionEffects::scroll(Autoscroll::Next),
19315 window,
19316 cx,
19317 |s| s.select_ranges(Some(70..70)),
19318 );
19319 editor.open_excerpts(&OpenExcerpts, window, cx);
19320 });
19321 cx.executor().run_until_parked();
19322 workspace
19323 .update(cx, |workspace, window, cx| {
19324 let active_item = workspace
19325 .active_item(cx)
19326 .expect("should have an active item after navigating into the 3rd buffer");
19327 let third_item_id = active_item.item_id();
19328 assert_ne!(
19329 third_item_id, multibuffer_item_id,
19330 "Should navigate into the 3rd buffer and activate it"
19331 );
19332 assert_ne!(third_item_id, first_item_id);
19333 assert_ne!(third_item_id, second_item_id);
19334 assert_eq!(
19335 active_item.buffer_kind(cx),
19336 ItemBufferKind::Singleton,
19337 "New active item should be a singleton buffer"
19338 );
19339 assert_eq!(
19340 active_item
19341 .act_as::<Editor>(cx)
19342 .expect("should have navigated into an editor")
19343 .read(cx)
19344 .text(cx),
19345 sample_text_3
19346 );
19347
19348 workspace
19349 .go_back(workspace.active_pane().downgrade(), window, cx)
19350 .detach_and_log_err(cx);
19351 })
19352 .unwrap();
19353 cx.executor().run_until_parked();
19354 workspace
19355 .update(cx, |workspace, _, cx| {
19356 let active_item = workspace
19357 .active_item(cx)
19358 .expect("should have an active item after navigating back from the 3rd buffer");
19359 assert_eq!(
19360 active_item.item_id(),
19361 multibuffer_item_id,
19362 "Should navigate back from the 3rd buffer to the multi buffer"
19363 );
19364 assert_eq!(active_item.buffer_kind(cx), ItemBufferKind::Multibuffer);
19365 })
19366 .unwrap();
19367}
19368
19369#[gpui::test]
19370async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
19371 init_test(cx, |_| {});
19372
19373 let mut cx = EditorTestContext::new(cx).await;
19374
19375 let diff_base = r#"
19376 use some::mod;
19377
19378 const A: u32 = 42;
19379
19380 fn main() {
19381 println!("hello");
19382
19383 println!("world");
19384 }
19385 "#
19386 .unindent();
19387
19388 cx.set_state(
19389 &r#"
19390 use some::modified;
19391
19392 ˇ
19393 fn main() {
19394 println!("hello there");
19395
19396 println!("around the");
19397 println!("world");
19398 }
19399 "#
19400 .unindent(),
19401 );
19402
19403 cx.set_head_text(&diff_base);
19404 executor.run_until_parked();
19405
19406 cx.update_editor(|editor, window, cx| {
19407 editor.go_to_next_hunk(&GoToHunk, window, cx);
19408 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19409 });
19410 executor.run_until_parked();
19411 cx.assert_state_with_diff(
19412 r#"
19413 use some::modified;
19414
19415
19416 fn main() {
19417 - println!("hello");
19418 + ˇ println!("hello there");
19419
19420 println!("around the");
19421 println!("world");
19422 }
19423 "#
19424 .unindent(),
19425 );
19426
19427 cx.update_editor(|editor, window, cx| {
19428 for _ in 0..2 {
19429 editor.go_to_next_hunk(&GoToHunk, window, cx);
19430 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19431 }
19432 });
19433 executor.run_until_parked();
19434 cx.assert_state_with_diff(
19435 r#"
19436 - use some::mod;
19437 + ˇuse some::modified;
19438
19439
19440 fn main() {
19441 - println!("hello");
19442 + println!("hello there");
19443
19444 + println!("around the");
19445 println!("world");
19446 }
19447 "#
19448 .unindent(),
19449 );
19450
19451 cx.update_editor(|editor, window, cx| {
19452 editor.go_to_next_hunk(&GoToHunk, window, cx);
19453 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19454 });
19455 executor.run_until_parked();
19456 cx.assert_state_with_diff(
19457 r#"
19458 - use some::mod;
19459 + use some::modified;
19460
19461 - const A: u32 = 42;
19462 ˇ
19463 fn main() {
19464 - println!("hello");
19465 + println!("hello there");
19466
19467 + println!("around the");
19468 println!("world");
19469 }
19470 "#
19471 .unindent(),
19472 );
19473
19474 cx.update_editor(|editor, window, cx| {
19475 editor.cancel(&Cancel, window, cx);
19476 });
19477
19478 cx.assert_state_with_diff(
19479 r#"
19480 use some::modified;
19481
19482 ˇ
19483 fn main() {
19484 println!("hello there");
19485
19486 println!("around the");
19487 println!("world");
19488 }
19489 "#
19490 .unindent(),
19491 );
19492}
19493
19494#[gpui::test]
19495async fn test_diff_base_change_with_expanded_diff_hunks(
19496 executor: BackgroundExecutor,
19497 cx: &mut TestAppContext,
19498) {
19499 init_test(cx, |_| {});
19500
19501 let mut cx = EditorTestContext::new(cx).await;
19502
19503 let diff_base = r#"
19504 use some::mod1;
19505 use some::mod2;
19506
19507 const A: u32 = 42;
19508 const B: u32 = 42;
19509 const C: u32 = 42;
19510
19511 fn main() {
19512 println!("hello");
19513
19514 println!("world");
19515 }
19516 "#
19517 .unindent();
19518
19519 cx.set_state(
19520 &r#"
19521 use some::mod2;
19522
19523 const A: u32 = 42;
19524 const C: u32 = 42;
19525
19526 fn main(ˇ) {
19527 //println!("hello");
19528
19529 println!("world");
19530 //
19531 //
19532 }
19533 "#
19534 .unindent(),
19535 );
19536
19537 cx.set_head_text(&diff_base);
19538 executor.run_until_parked();
19539
19540 cx.update_editor(|editor, window, cx| {
19541 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19542 });
19543 executor.run_until_parked();
19544 cx.assert_state_with_diff(
19545 r#"
19546 - use some::mod1;
19547 use some::mod2;
19548
19549 const A: u32 = 42;
19550 - const B: u32 = 42;
19551 const C: u32 = 42;
19552
19553 fn main(ˇ) {
19554 - println!("hello");
19555 + //println!("hello");
19556
19557 println!("world");
19558 + //
19559 + //
19560 }
19561 "#
19562 .unindent(),
19563 );
19564
19565 cx.set_head_text("new diff base!");
19566 executor.run_until_parked();
19567 cx.assert_state_with_diff(
19568 r#"
19569 - new diff base!
19570 + use some::mod2;
19571 +
19572 + const A: u32 = 42;
19573 + const C: u32 = 42;
19574 +
19575 + fn main(ˇ) {
19576 + //println!("hello");
19577 +
19578 + println!("world");
19579 + //
19580 + //
19581 + }
19582 "#
19583 .unindent(),
19584 );
19585}
19586
19587#[gpui::test]
19588async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
19589 init_test(cx, |_| {});
19590
19591 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19592 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
19593 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19594 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
19595 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
19596 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
19597
19598 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
19599 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
19600 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
19601
19602 let multi_buffer = cx.new(|cx| {
19603 let mut multibuffer = MultiBuffer::new(ReadWrite);
19604 multibuffer.push_excerpts(
19605 buffer_1.clone(),
19606 [
19607 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19608 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19609 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19610 ],
19611 cx,
19612 );
19613 multibuffer.push_excerpts(
19614 buffer_2.clone(),
19615 [
19616 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19617 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19618 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19619 ],
19620 cx,
19621 );
19622 multibuffer.push_excerpts(
19623 buffer_3.clone(),
19624 [
19625 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
19626 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
19627 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
19628 ],
19629 cx,
19630 );
19631 multibuffer
19632 });
19633
19634 let editor =
19635 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19636 editor
19637 .update(cx, |editor, _window, cx| {
19638 for (buffer, diff_base) in [
19639 (buffer_1.clone(), file_1_old),
19640 (buffer_2.clone(), file_2_old),
19641 (buffer_3.clone(), file_3_old),
19642 ] {
19643 let diff = cx.new(|cx| BufferDiff::new_with_base_text(diff_base, &buffer, cx));
19644 editor
19645 .buffer
19646 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
19647 }
19648 })
19649 .unwrap();
19650
19651 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19652 cx.run_until_parked();
19653
19654 cx.assert_editor_state(
19655 &"
19656 ˇaaa
19657 ccc
19658 ddd
19659
19660 ggg
19661 hhh
19662
19663
19664 lll
19665 mmm
19666 NNN
19667
19668 qqq
19669 rrr
19670
19671 uuu
19672 111
19673 222
19674 333
19675
19676 666
19677 777
19678
19679 000
19680 !!!"
19681 .unindent(),
19682 );
19683
19684 cx.update_editor(|editor, window, cx| {
19685 editor.select_all(&SelectAll, window, cx);
19686 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
19687 });
19688 cx.executor().run_until_parked();
19689
19690 cx.assert_state_with_diff(
19691 "
19692 «aaa
19693 - bbb
19694 ccc
19695 ddd
19696
19697 ggg
19698 hhh
19699
19700
19701 lll
19702 mmm
19703 - nnn
19704 + NNN
19705
19706 qqq
19707 rrr
19708
19709 uuu
19710 111
19711 222
19712 333
19713
19714 + 666
19715 777
19716
19717 000
19718 !!!ˇ»"
19719 .unindent(),
19720 );
19721}
19722
19723#[gpui::test]
19724async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
19725 init_test(cx, |_| {});
19726
19727 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
19728 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
19729
19730 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
19731 let multi_buffer = cx.new(|cx| {
19732 let mut multibuffer = MultiBuffer::new(ReadWrite);
19733 multibuffer.push_excerpts(
19734 buffer.clone(),
19735 [
19736 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
19737 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
19738 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
19739 ],
19740 cx,
19741 );
19742 multibuffer
19743 });
19744
19745 let editor =
19746 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
19747 editor
19748 .update(cx, |editor, _window, cx| {
19749 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
19750 editor
19751 .buffer
19752 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
19753 })
19754 .unwrap();
19755
19756 let mut cx = EditorTestContext::for_editor(editor, cx).await;
19757 cx.run_until_parked();
19758
19759 cx.update_editor(|editor, window, cx| {
19760 editor.expand_all_diff_hunks(&Default::default(), window, cx)
19761 });
19762 cx.executor().run_until_parked();
19763
19764 // When the start of a hunk coincides with the start of its excerpt,
19765 // the hunk is expanded. When the start of a hunk is earlier than
19766 // the start of its excerpt, the hunk is not expanded.
19767 cx.assert_state_with_diff(
19768 "
19769 ˇaaa
19770 - bbb
19771 + BBB
19772
19773 - ddd
19774 - eee
19775 + DDD
19776 + EEE
19777 fff
19778
19779 iii
19780 "
19781 .unindent(),
19782 );
19783}
19784
19785#[gpui::test]
19786async fn test_edits_around_expanded_insertion_hunks(
19787 executor: BackgroundExecutor,
19788 cx: &mut TestAppContext,
19789) {
19790 init_test(cx, |_| {});
19791
19792 let mut cx = EditorTestContext::new(cx).await;
19793
19794 let diff_base = r#"
19795 use some::mod1;
19796 use some::mod2;
19797
19798 const A: u32 = 42;
19799
19800 fn main() {
19801 println!("hello");
19802
19803 println!("world");
19804 }
19805 "#
19806 .unindent();
19807 executor.run_until_parked();
19808 cx.set_state(
19809 &r#"
19810 use some::mod1;
19811 use some::mod2;
19812
19813 const A: u32 = 42;
19814 const B: u32 = 42;
19815 const C: u32 = 42;
19816 ˇ
19817
19818 fn main() {
19819 println!("hello");
19820
19821 println!("world");
19822 }
19823 "#
19824 .unindent(),
19825 );
19826
19827 cx.set_head_text(&diff_base);
19828 executor.run_until_parked();
19829
19830 cx.update_editor(|editor, window, cx| {
19831 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
19832 });
19833 executor.run_until_parked();
19834
19835 cx.assert_state_with_diff(
19836 r#"
19837 use some::mod1;
19838 use some::mod2;
19839
19840 const A: u32 = 42;
19841 + const B: u32 = 42;
19842 + const C: u32 = 42;
19843 + ˇ
19844
19845 fn main() {
19846 println!("hello");
19847
19848 println!("world");
19849 }
19850 "#
19851 .unindent(),
19852 );
19853
19854 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
19855 executor.run_until_parked();
19856
19857 cx.assert_state_with_diff(
19858 r#"
19859 use some::mod1;
19860 use some::mod2;
19861
19862 const A: u32 = 42;
19863 + const B: u32 = 42;
19864 + const C: u32 = 42;
19865 + const D: u32 = 42;
19866 + ˇ
19867
19868 fn main() {
19869 println!("hello");
19870
19871 println!("world");
19872 }
19873 "#
19874 .unindent(),
19875 );
19876
19877 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
19878 executor.run_until_parked();
19879
19880 cx.assert_state_with_diff(
19881 r#"
19882 use some::mod1;
19883 use some::mod2;
19884
19885 const A: u32 = 42;
19886 + const B: u32 = 42;
19887 + const C: u32 = 42;
19888 + const D: u32 = 42;
19889 + const E: u32 = 42;
19890 + ˇ
19891
19892 fn main() {
19893 println!("hello");
19894
19895 println!("world");
19896 }
19897 "#
19898 .unindent(),
19899 );
19900
19901 cx.update_editor(|editor, window, cx| {
19902 editor.delete_line(&DeleteLine, window, cx);
19903 });
19904 executor.run_until_parked();
19905
19906 cx.assert_state_with_diff(
19907 r#"
19908 use some::mod1;
19909 use some::mod2;
19910
19911 const A: u32 = 42;
19912 + const B: u32 = 42;
19913 + const C: u32 = 42;
19914 + const D: u32 = 42;
19915 + const E: u32 = 42;
19916 ˇ
19917 fn main() {
19918 println!("hello");
19919
19920 println!("world");
19921 }
19922 "#
19923 .unindent(),
19924 );
19925
19926 cx.update_editor(|editor, window, cx| {
19927 editor.move_up(&MoveUp, window, cx);
19928 editor.delete_line(&DeleteLine, window, cx);
19929 editor.move_up(&MoveUp, window, cx);
19930 editor.delete_line(&DeleteLine, window, cx);
19931 editor.move_up(&MoveUp, window, cx);
19932 editor.delete_line(&DeleteLine, window, cx);
19933 });
19934 executor.run_until_parked();
19935 cx.assert_state_with_diff(
19936 r#"
19937 use some::mod1;
19938 use some::mod2;
19939
19940 const A: u32 = 42;
19941 + const B: u32 = 42;
19942 ˇ
19943 fn main() {
19944 println!("hello");
19945
19946 println!("world");
19947 }
19948 "#
19949 .unindent(),
19950 );
19951
19952 cx.update_editor(|editor, window, cx| {
19953 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
19954 editor.delete_line(&DeleteLine, window, cx);
19955 });
19956 executor.run_until_parked();
19957 cx.assert_state_with_diff(
19958 r#"
19959 ˇ
19960 fn main() {
19961 println!("hello");
19962
19963 println!("world");
19964 }
19965 "#
19966 .unindent(),
19967 );
19968}
19969
19970#[gpui::test]
19971async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
19972 init_test(cx, |_| {});
19973
19974 let mut cx = EditorTestContext::new(cx).await;
19975 cx.set_head_text(indoc! { "
19976 one
19977 two
19978 three
19979 four
19980 five
19981 "
19982 });
19983 cx.set_state(indoc! { "
19984 one
19985 ˇthree
19986 five
19987 "});
19988 cx.run_until_parked();
19989 cx.update_editor(|editor, window, cx| {
19990 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
19991 });
19992 cx.assert_state_with_diff(
19993 indoc! { "
19994 one
19995 - two
19996 ˇthree
19997 - four
19998 five
19999 "}
20000 .to_string(),
20001 );
20002 cx.update_editor(|editor, window, cx| {
20003 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20004 });
20005
20006 cx.assert_state_with_diff(
20007 indoc! { "
20008 one
20009 ˇthree
20010 five
20011 "}
20012 .to_string(),
20013 );
20014
20015 cx.set_state(indoc! { "
20016 one
20017 ˇTWO
20018 three
20019 four
20020 five
20021 "});
20022 cx.run_until_parked();
20023 cx.update_editor(|editor, window, cx| {
20024 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20025 });
20026
20027 cx.assert_state_with_diff(
20028 indoc! { "
20029 one
20030 - two
20031 + ˇTWO
20032 three
20033 four
20034 five
20035 "}
20036 .to_string(),
20037 );
20038 cx.update_editor(|editor, window, cx| {
20039 editor.move_up(&Default::default(), window, cx);
20040 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
20041 });
20042 cx.assert_state_with_diff(
20043 indoc! { "
20044 one
20045 ˇTWO
20046 three
20047 four
20048 five
20049 "}
20050 .to_string(),
20051 );
20052}
20053
20054#[gpui::test]
20055async fn test_edits_around_expanded_deletion_hunks(
20056 executor: BackgroundExecutor,
20057 cx: &mut TestAppContext,
20058) {
20059 init_test(cx, |_| {});
20060
20061 let mut cx = EditorTestContext::new(cx).await;
20062
20063 let diff_base = r#"
20064 use some::mod1;
20065 use some::mod2;
20066
20067 const A: u32 = 42;
20068 const B: u32 = 42;
20069 const C: u32 = 42;
20070
20071
20072 fn main() {
20073 println!("hello");
20074
20075 println!("world");
20076 }
20077 "#
20078 .unindent();
20079 executor.run_until_parked();
20080 cx.set_state(
20081 &r#"
20082 use some::mod1;
20083 use some::mod2;
20084
20085 ˇconst B: u32 = 42;
20086 const C: u32 = 42;
20087
20088
20089 fn main() {
20090 println!("hello");
20091
20092 println!("world");
20093 }
20094 "#
20095 .unindent(),
20096 );
20097
20098 cx.set_head_text(&diff_base);
20099 executor.run_until_parked();
20100
20101 cx.update_editor(|editor, window, cx| {
20102 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20103 });
20104 executor.run_until_parked();
20105
20106 cx.assert_state_with_diff(
20107 r#"
20108 use some::mod1;
20109 use some::mod2;
20110
20111 - const A: u32 = 42;
20112 ˇconst B: u32 = 42;
20113 const C: u32 = 42;
20114
20115
20116 fn main() {
20117 println!("hello");
20118
20119 println!("world");
20120 }
20121 "#
20122 .unindent(),
20123 );
20124
20125 cx.update_editor(|editor, window, cx| {
20126 editor.delete_line(&DeleteLine, window, cx);
20127 });
20128 executor.run_until_parked();
20129 cx.assert_state_with_diff(
20130 r#"
20131 use some::mod1;
20132 use some::mod2;
20133
20134 - const A: u32 = 42;
20135 - const B: u32 = 42;
20136 ˇconst C: u32 = 42;
20137
20138
20139 fn main() {
20140 println!("hello");
20141
20142 println!("world");
20143 }
20144 "#
20145 .unindent(),
20146 );
20147
20148 cx.update_editor(|editor, window, cx| {
20149 editor.delete_line(&DeleteLine, window, cx);
20150 });
20151 executor.run_until_parked();
20152 cx.assert_state_with_diff(
20153 r#"
20154 use some::mod1;
20155 use some::mod2;
20156
20157 - const A: u32 = 42;
20158 - const B: u32 = 42;
20159 - const C: u32 = 42;
20160 ˇ
20161
20162 fn main() {
20163 println!("hello");
20164
20165 println!("world");
20166 }
20167 "#
20168 .unindent(),
20169 );
20170
20171 cx.update_editor(|editor, window, cx| {
20172 editor.handle_input("replacement", window, cx);
20173 });
20174 executor.run_until_parked();
20175 cx.assert_state_with_diff(
20176 r#"
20177 use some::mod1;
20178 use some::mod2;
20179
20180 - const A: u32 = 42;
20181 - const B: u32 = 42;
20182 - const C: u32 = 42;
20183 -
20184 + replacementˇ
20185
20186 fn main() {
20187 println!("hello");
20188
20189 println!("world");
20190 }
20191 "#
20192 .unindent(),
20193 );
20194}
20195
20196#[gpui::test]
20197async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
20198 init_test(cx, |_| {});
20199
20200 let mut cx = EditorTestContext::new(cx).await;
20201
20202 let base_text = r#"
20203 one
20204 two
20205 three
20206 four
20207 five
20208 "#
20209 .unindent();
20210 executor.run_until_parked();
20211 cx.set_state(
20212 &r#"
20213 one
20214 two
20215 fˇour
20216 five
20217 "#
20218 .unindent(),
20219 );
20220
20221 cx.set_head_text(&base_text);
20222 executor.run_until_parked();
20223
20224 cx.update_editor(|editor, window, cx| {
20225 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20226 });
20227 executor.run_until_parked();
20228
20229 cx.assert_state_with_diff(
20230 r#"
20231 one
20232 two
20233 - three
20234 fˇour
20235 five
20236 "#
20237 .unindent(),
20238 );
20239
20240 cx.update_editor(|editor, window, cx| {
20241 editor.backspace(&Backspace, window, cx);
20242 editor.backspace(&Backspace, window, cx);
20243 });
20244 executor.run_until_parked();
20245 cx.assert_state_with_diff(
20246 r#"
20247 one
20248 two
20249 - threeˇ
20250 - four
20251 + our
20252 five
20253 "#
20254 .unindent(),
20255 );
20256}
20257
20258#[gpui::test]
20259async fn test_edit_after_expanded_modification_hunk(
20260 executor: BackgroundExecutor,
20261 cx: &mut TestAppContext,
20262) {
20263 init_test(cx, |_| {});
20264
20265 let mut cx = EditorTestContext::new(cx).await;
20266
20267 let diff_base = r#"
20268 use some::mod1;
20269 use some::mod2;
20270
20271 const A: u32 = 42;
20272 const B: u32 = 42;
20273 const C: u32 = 42;
20274 const D: u32 = 42;
20275
20276
20277 fn main() {
20278 println!("hello");
20279
20280 println!("world");
20281 }"#
20282 .unindent();
20283
20284 cx.set_state(
20285 &r#"
20286 use some::mod1;
20287 use some::mod2;
20288
20289 const A: u32 = 42;
20290 const B: u32 = 42;
20291 const C: u32 = 43ˇ
20292 const D: u32 = 42;
20293
20294
20295 fn main() {
20296 println!("hello");
20297
20298 println!("world");
20299 }"#
20300 .unindent(),
20301 );
20302
20303 cx.set_head_text(&diff_base);
20304 executor.run_until_parked();
20305 cx.update_editor(|editor, window, cx| {
20306 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
20307 });
20308 executor.run_until_parked();
20309
20310 cx.assert_state_with_diff(
20311 r#"
20312 use some::mod1;
20313 use some::mod2;
20314
20315 const A: u32 = 42;
20316 const B: u32 = 42;
20317 - const C: u32 = 42;
20318 + const C: u32 = 43ˇ
20319 const D: u32 = 42;
20320
20321
20322 fn main() {
20323 println!("hello");
20324
20325 println!("world");
20326 }"#
20327 .unindent(),
20328 );
20329
20330 cx.update_editor(|editor, window, cx| {
20331 editor.handle_input("\nnew_line\n", window, cx);
20332 });
20333 executor.run_until_parked();
20334
20335 cx.assert_state_with_diff(
20336 r#"
20337 use some::mod1;
20338 use some::mod2;
20339
20340 const A: u32 = 42;
20341 const B: u32 = 42;
20342 - const C: u32 = 42;
20343 + const C: u32 = 43
20344 + new_line
20345 + ˇ
20346 const D: u32 = 42;
20347
20348
20349 fn main() {
20350 println!("hello");
20351
20352 println!("world");
20353 }"#
20354 .unindent(),
20355 );
20356}
20357
20358#[gpui::test]
20359async fn test_stage_and_unstage_added_file_hunk(
20360 executor: BackgroundExecutor,
20361 cx: &mut TestAppContext,
20362) {
20363 init_test(cx, |_| {});
20364
20365 let mut cx = EditorTestContext::new(cx).await;
20366 cx.update_editor(|editor, _, cx| {
20367 editor.set_expand_all_diff_hunks(cx);
20368 });
20369
20370 let working_copy = r#"
20371 ˇfn main() {
20372 println!("hello, world!");
20373 }
20374 "#
20375 .unindent();
20376
20377 cx.set_state(&working_copy);
20378 executor.run_until_parked();
20379
20380 cx.assert_state_with_diff(
20381 r#"
20382 + ˇfn main() {
20383 + println!("hello, world!");
20384 + }
20385 "#
20386 .unindent(),
20387 );
20388 cx.assert_index_text(None);
20389
20390 cx.update_editor(|editor, window, cx| {
20391 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20392 });
20393 executor.run_until_parked();
20394 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
20395 cx.assert_state_with_diff(
20396 r#"
20397 + ˇfn main() {
20398 + println!("hello, world!");
20399 + }
20400 "#
20401 .unindent(),
20402 );
20403
20404 cx.update_editor(|editor, window, cx| {
20405 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
20406 });
20407 executor.run_until_parked();
20408 cx.assert_index_text(None);
20409}
20410
20411async fn setup_indent_guides_editor(
20412 text: &str,
20413 cx: &mut TestAppContext,
20414) -> (BufferId, EditorTestContext) {
20415 init_test(cx, |_| {});
20416
20417 let mut cx = EditorTestContext::new(cx).await;
20418
20419 let buffer_id = cx.update_editor(|editor, window, cx| {
20420 editor.set_text(text, window, cx);
20421 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
20422
20423 buffer_ids[0]
20424 });
20425
20426 (buffer_id, cx)
20427}
20428
20429fn assert_indent_guides(
20430 range: Range<u32>,
20431 expected: Vec<IndentGuide>,
20432 active_indices: Option<Vec<usize>>,
20433 cx: &mut EditorTestContext,
20434) {
20435 let indent_guides = cx.update_editor(|editor, window, cx| {
20436 let snapshot = editor.snapshot(window, cx).display_snapshot;
20437 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
20438 editor,
20439 MultiBufferRow(range.start)..MultiBufferRow(range.end),
20440 true,
20441 &snapshot,
20442 cx,
20443 );
20444
20445 indent_guides.sort_by(|a, b| {
20446 a.depth.cmp(&b.depth).then(
20447 a.start_row
20448 .cmp(&b.start_row)
20449 .then(a.end_row.cmp(&b.end_row)),
20450 )
20451 });
20452 indent_guides
20453 });
20454
20455 if let Some(expected) = active_indices {
20456 let active_indices = cx.update_editor(|editor, window, cx| {
20457 let snapshot = editor.snapshot(window, cx).display_snapshot;
20458 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
20459 });
20460
20461 assert_eq!(
20462 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
20463 expected,
20464 "Active indent guide indices do not match"
20465 );
20466 }
20467
20468 assert_eq!(indent_guides, expected, "Indent guides do not match");
20469}
20470
20471fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
20472 IndentGuide {
20473 buffer_id,
20474 start_row: MultiBufferRow(start_row),
20475 end_row: MultiBufferRow(end_row),
20476 depth,
20477 tab_size: 4,
20478 settings: IndentGuideSettings {
20479 enabled: true,
20480 line_width: 1,
20481 active_line_width: 1,
20482 coloring: IndentGuideColoring::default(),
20483 background_coloring: IndentGuideBackgroundColoring::default(),
20484 },
20485 }
20486}
20487
20488#[gpui::test]
20489async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
20490 let (buffer_id, mut cx) = setup_indent_guides_editor(
20491 &"
20492 fn main() {
20493 let a = 1;
20494 }"
20495 .unindent(),
20496 cx,
20497 )
20498 .await;
20499
20500 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20501}
20502
20503#[gpui::test]
20504async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
20505 let (buffer_id, mut cx) = setup_indent_guides_editor(
20506 &"
20507 fn main() {
20508 let a = 1;
20509 let b = 2;
20510 }"
20511 .unindent(),
20512 cx,
20513 )
20514 .await;
20515
20516 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
20517}
20518
20519#[gpui::test]
20520async fn test_indent_guide_nested(cx: &mut TestAppContext) {
20521 let (buffer_id, mut cx) = setup_indent_guides_editor(
20522 &"
20523 fn main() {
20524 let a = 1;
20525 if a == 3 {
20526 let b = 2;
20527 } else {
20528 let c = 3;
20529 }
20530 }"
20531 .unindent(),
20532 cx,
20533 )
20534 .await;
20535
20536 assert_indent_guides(
20537 0..8,
20538 vec![
20539 indent_guide(buffer_id, 1, 6, 0),
20540 indent_guide(buffer_id, 3, 3, 1),
20541 indent_guide(buffer_id, 5, 5, 1),
20542 ],
20543 None,
20544 &mut cx,
20545 );
20546}
20547
20548#[gpui::test]
20549async fn test_indent_guide_tab(cx: &mut TestAppContext) {
20550 let (buffer_id, mut cx) = setup_indent_guides_editor(
20551 &"
20552 fn main() {
20553 let a = 1;
20554 let b = 2;
20555 let c = 3;
20556 }"
20557 .unindent(),
20558 cx,
20559 )
20560 .await;
20561
20562 assert_indent_guides(
20563 0..5,
20564 vec![
20565 indent_guide(buffer_id, 1, 3, 0),
20566 indent_guide(buffer_id, 2, 2, 1),
20567 ],
20568 None,
20569 &mut cx,
20570 );
20571}
20572
20573#[gpui::test]
20574async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
20575 let (buffer_id, mut cx) = setup_indent_guides_editor(
20576 &"
20577 fn main() {
20578 let a = 1;
20579
20580 let c = 3;
20581 }"
20582 .unindent(),
20583 cx,
20584 )
20585 .await;
20586
20587 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
20588}
20589
20590#[gpui::test]
20591async fn test_indent_guide_complex(cx: &mut TestAppContext) {
20592 let (buffer_id, mut cx) = setup_indent_guides_editor(
20593 &"
20594 fn main() {
20595 let a = 1;
20596
20597 let c = 3;
20598
20599 if a == 3 {
20600 let b = 2;
20601 } else {
20602 let c = 3;
20603 }
20604 }"
20605 .unindent(),
20606 cx,
20607 )
20608 .await;
20609
20610 assert_indent_guides(
20611 0..11,
20612 vec![
20613 indent_guide(buffer_id, 1, 9, 0),
20614 indent_guide(buffer_id, 6, 6, 1),
20615 indent_guide(buffer_id, 8, 8, 1),
20616 ],
20617 None,
20618 &mut cx,
20619 );
20620}
20621
20622#[gpui::test]
20623async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
20624 let (buffer_id, mut cx) = setup_indent_guides_editor(
20625 &"
20626 fn main() {
20627 let a = 1;
20628
20629 let c = 3;
20630
20631 if a == 3 {
20632 let b = 2;
20633 } else {
20634 let c = 3;
20635 }
20636 }"
20637 .unindent(),
20638 cx,
20639 )
20640 .await;
20641
20642 assert_indent_guides(
20643 1..11,
20644 vec![
20645 indent_guide(buffer_id, 1, 9, 0),
20646 indent_guide(buffer_id, 6, 6, 1),
20647 indent_guide(buffer_id, 8, 8, 1),
20648 ],
20649 None,
20650 &mut cx,
20651 );
20652}
20653
20654#[gpui::test]
20655async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
20656 let (buffer_id, mut cx) = setup_indent_guides_editor(
20657 &"
20658 fn main() {
20659 let a = 1;
20660
20661 let c = 3;
20662
20663 if a == 3 {
20664 let b = 2;
20665 } else {
20666 let c = 3;
20667 }
20668 }"
20669 .unindent(),
20670 cx,
20671 )
20672 .await;
20673
20674 assert_indent_guides(
20675 1..10,
20676 vec![
20677 indent_guide(buffer_id, 1, 9, 0),
20678 indent_guide(buffer_id, 6, 6, 1),
20679 indent_guide(buffer_id, 8, 8, 1),
20680 ],
20681 None,
20682 &mut cx,
20683 );
20684}
20685
20686#[gpui::test]
20687async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
20688 let (buffer_id, mut cx) = setup_indent_guides_editor(
20689 &"
20690 fn main() {
20691 if a {
20692 b(
20693 c,
20694 d,
20695 )
20696 } else {
20697 e(
20698 f
20699 )
20700 }
20701 }"
20702 .unindent(),
20703 cx,
20704 )
20705 .await;
20706
20707 assert_indent_guides(
20708 0..11,
20709 vec![
20710 indent_guide(buffer_id, 1, 10, 0),
20711 indent_guide(buffer_id, 2, 5, 1),
20712 indent_guide(buffer_id, 7, 9, 1),
20713 indent_guide(buffer_id, 3, 4, 2),
20714 indent_guide(buffer_id, 8, 8, 2),
20715 ],
20716 None,
20717 &mut cx,
20718 );
20719
20720 cx.update_editor(|editor, window, cx| {
20721 editor.fold_at(MultiBufferRow(2), window, cx);
20722 assert_eq!(
20723 editor.display_text(cx),
20724 "
20725 fn main() {
20726 if a {
20727 b(⋯
20728 )
20729 } else {
20730 e(
20731 f
20732 )
20733 }
20734 }"
20735 .unindent()
20736 );
20737 });
20738
20739 assert_indent_guides(
20740 0..11,
20741 vec![
20742 indent_guide(buffer_id, 1, 10, 0),
20743 indent_guide(buffer_id, 2, 5, 1),
20744 indent_guide(buffer_id, 7, 9, 1),
20745 indent_guide(buffer_id, 8, 8, 2),
20746 ],
20747 None,
20748 &mut cx,
20749 );
20750}
20751
20752#[gpui::test]
20753async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
20754 let (buffer_id, mut cx) = setup_indent_guides_editor(
20755 &"
20756 block1
20757 block2
20758 block3
20759 block4
20760 block2
20761 block1
20762 block1"
20763 .unindent(),
20764 cx,
20765 )
20766 .await;
20767
20768 assert_indent_guides(
20769 1..10,
20770 vec![
20771 indent_guide(buffer_id, 1, 4, 0),
20772 indent_guide(buffer_id, 2, 3, 1),
20773 indent_guide(buffer_id, 3, 3, 2),
20774 ],
20775 None,
20776 &mut cx,
20777 );
20778}
20779
20780#[gpui::test]
20781async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
20782 let (buffer_id, mut cx) = setup_indent_guides_editor(
20783 &"
20784 block1
20785 block2
20786 block3
20787
20788 block1
20789 block1"
20790 .unindent(),
20791 cx,
20792 )
20793 .await;
20794
20795 assert_indent_guides(
20796 0..6,
20797 vec![
20798 indent_guide(buffer_id, 1, 2, 0),
20799 indent_guide(buffer_id, 2, 2, 1),
20800 ],
20801 None,
20802 &mut cx,
20803 );
20804}
20805
20806#[gpui::test]
20807async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
20808 let (buffer_id, mut cx) = setup_indent_guides_editor(
20809 &"
20810 function component() {
20811 \treturn (
20812 \t\t\t
20813 \t\t<div>
20814 \t\t\t<abc></abc>
20815 \t\t</div>
20816 \t)
20817 }"
20818 .unindent(),
20819 cx,
20820 )
20821 .await;
20822
20823 assert_indent_guides(
20824 0..8,
20825 vec![
20826 indent_guide(buffer_id, 1, 6, 0),
20827 indent_guide(buffer_id, 2, 5, 1),
20828 indent_guide(buffer_id, 4, 4, 2),
20829 ],
20830 None,
20831 &mut cx,
20832 );
20833}
20834
20835#[gpui::test]
20836async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
20837 let (buffer_id, mut cx) = setup_indent_guides_editor(
20838 &"
20839 function component() {
20840 \treturn (
20841 \t
20842 \t\t<div>
20843 \t\t\t<abc></abc>
20844 \t\t</div>
20845 \t)
20846 }"
20847 .unindent(),
20848 cx,
20849 )
20850 .await;
20851
20852 assert_indent_guides(
20853 0..8,
20854 vec![
20855 indent_guide(buffer_id, 1, 6, 0),
20856 indent_guide(buffer_id, 2, 5, 1),
20857 indent_guide(buffer_id, 4, 4, 2),
20858 ],
20859 None,
20860 &mut cx,
20861 );
20862}
20863
20864#[gpui::test]
20865async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
20866 let (buffer_id, mut cx) = setup_indent_guides_editor(
20867 &"
20868 block1
20869
20870
20871
20872 block2
20873 "
20874 .unindent(),
20875 cx,
20876 )
20877 .await;
20878
20879 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
20880}
20881
20882#[gpui::test]
20883async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
20884 let (buffer_id, mut cx) = setup_indent_guides_editor(
20885 &"
20886 def a:
20887 \tb = 3
20888 \tif True:
20889 \t\tc = 4
20890 \t\td = 5
20891 \tprint(b)
20892 "
20893 .unindent(),
20894 cx,
20895 )
20896 .await;
20897
20898 assert_indent_guides(
20899 0..6,
20900 vec![
20901 indent_guide(buffer_id, 1, 5, 0),
20902 indent_guide(buffer_id, 3, 4, 1),
20903 ],
20904 None,
20905 &mut cx,
20906 );
20907}
20908
20909#[gpui::test]
20910async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
20911 let (buffer_id, mut cx) = setup_indent_guides_editor(
20912 &"
20913 fn main() {
20914 let a = 1;
20915 }"
20916 .unindent(),
20917 cx,
20918 )
20919 .await;
20920
20921 cx.update_editor(|editor, window, cx| {
20922 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20923 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20924 });
20925 });
20926
20927 assert_indent_guides(
20928 0..3,
20929 vec![indent_guide(buffer_id, 1, 1, 0)],
20930 Some(vec![0]),
20931 &mut cx,
20932 );
20933}
20934
20935#[gpui::test]
20936async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
20937 let (buffer_id, mut cx) = setup_indent_guides_editor(
20938 &"
20939 fn main() {
20940 if 1 == 2 {
20941 let a = 1;
20942 }
20943 }"
20944 .unindent(),
20945 cx,
20946 )
20947 .await;
20948
20949 cx.update_editor(|editor, window, cx| {
20950 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20951 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
20952 });
20953 });
20954
20955 assert_indent_guides(
20956 0..4,
20957 vec![
20958 indent_guide(buffer_id, 1, 3, 0),
20959 indent_guide(buffer_id, 2, 2, 1),
20960 ],
20961 Some(vec![1]),
20962 &mut cx,
20963 );
20964
20965 cx.update_editor(|editor, window, cx| {
20966 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20967 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
20968 });
20969 });
20970
20971 assert_indent_guides(
20972 0..4,
20973 vec![
20974 indent_guide(buffer_id, 1, 3, 0),
20975 indent_guide(buffer_id, 2, 2, 1),
20976 ],
20977 Some(vec![1]),
20978 &mut cx,
20979 );
20980
20981 cx.update_editor(|editor, window, cx| {
20982 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
20983 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
20984 });
20985 });
20986
20987 assert_indent_guides(
20988 0..4,
20989 vec![
20990 indent_guide(buffer_id, 1, 3, 0),
20991 indent_guide(buffer_id, 2, 2, 1),
20992 ],
20993 Some(vec![0]),
20994 &mut cx,
20995 );
20996}
20997
20998#[gpui::test]
20999async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
21000 let (buffer_id, mut cx) = setup_indent_guides_editor(
21001 &"
21002 fn main() {
21003 let a = 1;
21004
21005 let b = 2;
21006 }"
21007 .unindent(),
21008 cx,
21009 )
21010 .await;
21011
21012 cx.update_editor(|editor, window, cx| {
21013 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21014 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
21015 });
21016 });
21017
21018 assert_indent_guides(
21019 0..5,
21020 vec![indent_guide(buffer_id, 1, 3, 0)],
21021 Some(vec![0]),
21022 &mut cx,
21023 );
21024}
21025
21026#[gpui::test]
21027async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
21028 let (buffer_id, mut cx) = setup_indent_guides_editor(
21029 &"
21030 def m:
21031 a = 1
21032 pass"
21033 .unindent(),
21034 cx,
21035 )
21036 .await;
21037
21038 cx.update_editor(|editor, window, cx| {
21039 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
21040 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
21041 });
21042 });
21043
21044 assert_indent_guides(
21045 0..3,
21046 vec![indent_guide(buffer_id, 1, 2, 0)],
21047 Some(vec![0]),
21048 &mut cx,
21049 );
21050}
21051
21052#[gpui::test]
21053async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
21054 init_test(cx, |_| {});
21055 let mut cx = EditorTestContext::new(cx).await;
21056 let text = indoc! {
21057 "
21058 impl A {
21059 fn b() {
21060 0;
21061 3;
21062 5;
21063 6;
21064 7;
21065 }
21066 }
21067 "
21068 };
21069 let base_text = indoc! {
21070 "
21071 impl A {
21072 fn b() {
21073 0;
21074 1;
21075 2;
21076 3;
21077 4;
21078 }
21079 fn c() {
21080 5;
21081 6;
21082 7;
21083 }
21084 }
21085 "
21086 };
21087
21088 cx.update_editor(|editor, window, cx| {
21089 editor.set_text(text, window, cx);
21090
21091 editor.buffer().update(cx, |multibuffer, cx| {
21092 let buffer = multibuffer.as_singleton().unwrap();
21093 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
21094
21095 multibuffer.set_all_diff_hunks_expanded(cx);
21096 multibuffer.add_diff(diff, cx);
21097
21098 buffer.read(cx).remote_id()
21099 })
21100 });
21101 cx.run_until_parked();
21102
21103 cx.assert_state_with_diff(
21104 indoc! { "
21105 impl A {
21106 fn b() {
21107 0;
21108 - 1;
21109 - 2;
21110 3;
21111 - 4;
21112 - }
21113 - fn c() {
21114 5;
21115 6;
21116 7;
21117 }
21118 }
21119 ˇ"
21120 }
21121 .to_string(),
21122 );
21123
21124 let mut actual_guides = cx.update_editor(|editor, window, cx| {
21125 editor
21126 .snapshot(window, cx)
21127 .buffer_snapshot()
21128 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
21129 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
21130 .collect::<Vec<_>>()
21131 });
21132 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
21133 assert_eq!(
21134 actual_guides,
21135 vec![
21136 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
21137 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
21138 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
21139 ]
21140 );
21141}
21142
21143#[gpui::test]
21144async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
21145 init_test(cx, |_| {});
21146 let mut cx = EditorTestContext::new(cx).await;
21147
21148 let diff_base = r#"
21149 a
21150 b
21151 c
21152 "#
21153 .unindent();
21154
21155 cx.set_state(
21156 &r#"
21157 ˇA
21158 b
21159 C
21160 "#
21161 .unindent(),
21162 );
21163 cx.set_head_text(&diff_base);
21164 cx.update_editor(|editor, window, cx| {
21165 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21166 });
21167 executor.run_until_parked();
21168
21169 let both_hunks_expanded = r#"
21170 - a
21171 + ˇA
21172 b
21173 - c
21174 + C
21175 "#
21176 .unindent();
21177
21178 cx.assert_state_with_diff(both_hunks_expanded.clone());
21179
21180 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21181 let snapshot = editor.snapshot(window, cx);
21182 let hunks = editor
21183 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21184 .collect::<Vec<_>>();
21185 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21186 let buffer_id = hunks[0].buffer_id;
21187 hunks
21188 .into_iter()
21189 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21190 .collect::<Vec<_>>()
21191 });
21192 assert_eq!(hunk_ranges.len(), 2);
21193
21194 cx.update_editor(|editor, _, cx| {
21195 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21196 });
21197 executor.run_until_parked();
21198
21199 let second_hunk_expanded = r#"
21200 ˇA
21201 b
21202 - c
21203 + C
21204 "#
21205 .unindent();
21206
21207 cx.assert_state_with_diff(second_hunk_expanded);
21208
21209 cx.update_editor(|editor, _, cx| {
21210 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21211 });
21212 executor.run_until_parked();
21213
21214 cx.assert_state_with_diff(both_hunks_expanded.clone());
21215
21216 cx.update_editor(|editor, _, cx| {
21217 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21218 });
21219 executor.run_until_parked();
21220
21221 let first_hunk_expanded = r#"
21222 - a
21223 + ˇA
21224 b
21225 C
21226 "#
21227 .unindent();
21228
21229 cx.assert_state_with_diff(first_hunk_expanded);
21230
21231 cx.update_editor(|editor, _, cx| {
21232 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21233 });
21234 executor.run_until_parked();
21235
21236 cx.assert_state_with_diff(both_hunks_expanded);
21237
21238 cx.set_state(
21239 &r#"
21240 ˇA
21241 b
21242 "#
21243 .unindent(),
21244 );
21245 cx.run_until_parked();
21246
21247 // TODO this cursor position seems bad
21248 cx.assert_state_with_diff(
21249 r#"
21250 - ˇa
21251 + A
21252 b
21253 "#
21254 .unindent(),
21255 );
21256
21257 cx.update_editor(|editor, window, cx| {
21258 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21259 });
21260
21261 cx.assert_state_with_diff(
21262 r#"
21263 - ˇa
21264 + A
21265 b
21266 - c
21267 "#
21268 .unindent(),
21269 );
21270
21271 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21272 let snapshot = editor.snapshot(window, cx);
21273 let hunks = editor
21274 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21275 .collect::<Vec<_>>();
21276 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21277 let buffer_id = hunks[0].buffer_id;
21278 hunks
21279 .into_iter()
21280 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21281 .collect::<Vec<_>>()
21282 });
21283 assert_eq!(hunk_ranges.len(), 2);
21284
21285 cx.update_editor(|editor, _, cx| {
21286 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
21287 });
21288 executor.run_until_parked();
21289
21290 cx.assert_state_with_diff(
21291 r#"
21292 - ˇa
21293 + A
21294 b
21295 "#
21296 .unindent(),
21297 );
21298}
21299
21300#[gpui::test]
21301async fn test_toggle_deletion_hunk_at_start_of_file(
21302 executor: BackgroundExecutor,
21303 cx: &mut TestAppContext,
21304) {
21305 init_test(cx, |_| {});
21306 let mut cx = EditorTestContext::new(cx).await;
21307
21308 let diff_base = r#"
21309 a
21310 b
21311 c
21312 "#
21313 .unindent();
21314
21315 cx.set_state(
21316 &r#"
21317 ˇb
21318 c
21319 "#
21320 .unindent(),
21321 );
21322 cx.set_head_text(&diff_base);
21323 cx.update_editor(|editor, window, cx| {
21324 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
21325 });
21326 executor.run_until_parked();
21327
21328 let hunk_expanded = r#"
21329 - a
21330 ˇb
21331 c
21332 "#
21333 .unindent();
21334
21335 cx.assert_state_with_diff(hunk_expanded.clone());
21336
21337 let hunk_ranges = cx.update_editor(|editor, window, cx| {
21338 let snapshot = editor.snapshot(window, cx);
21339 let hunks = editor
21340 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21341 .collect::<Vec<_>>();
21342 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
21343 let buffer_id = hunks[0].buffer_id;
21344 hunks
21345 .into_iter()
21346 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range))
21347 .collect::<Vec<_>>()
21348 });
21349 assert_eq!(hunk_ranges.len(), 1);
21350
21351 cx.update_editor(|editor, _, cx| {
21352 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21353 });
21354 executor.run_until_parked();
21355
21356 let hunk_collapsed = r#"
21357 ˇb
21358 c
21359 "#
21360 .unindent();
21361
21362 cx.assert_state_with_diff(hunk_collapsed);
21363
21364 cx.update_editor(|editor, _, cx| {
21365 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
21366 });
21367 executor.run_until_parked();
21368
21369 cx.assert_state_with_diff(hunk_expanded);
21370}
21371
21372#[gpui::test]
21373async fn test_display_diff_hunks(cx: &mut TestAppContext) {
21374 init_test(cx, |_| {});
21375
21376 let fs = FakeFs::new(cx.executor());
21377 fs.insert_tree(
21378 path!("/test"),
21379 json!({
21380 ".git": {},
21381 "file-1": "ONE\n",
21382 "file-2": "TWO\n",
21383 "file-3": "THREE\n",
21384 }),
21385 )
21386 .await;
21387
21388 fs.set_head_for_repo(
21389 path!("/test/.git").as_ref(),
21390 &[
21391 ("file-1", "one\n".into()),
21392 ("file-2", "two\n".into()),
21393 ("file-3", "three\n".into()),
21394 ],
21395 "deadbeef",
21396 );
21397
21398 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
21399 let mut buffers = vec![];
21400 for i in 1..=3 {
21401 let buffer = project
21402 .update(cx, |project, cx| {
21403 let path = format!(path!("/test/file-{}"), i);
21404 project.open_local_buffer(path, cx)
21405 })
21406 .await
21407 .unwrap();
21408 buffers.push(buffer);
21409 }
21410
21411 let multibuffer = cx.new(|cx| {
21412 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
21413 multibuffer.set_all_diff_hunks_expanded(cx);
21414 for buffer in &buffers {
21415 let snapshot = buffer.read(cx).snapshot();
21416 multibuffer.set_excerpts_for_path(
21417 PathKey::with_sort_prefix(0, buffer.read(cx).file().unwrap().path().clone()),
21418 buffer.clone(),
21419 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
21420 2,
21421 cx,
21422 );
21423 }
21424 multibuffer
21425 });
21426
21427 let editor = cx.add_window(|window, cx| {
21428 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
21429 });
21430 cx.run_until_parked();
21431
21432 let snapshot = editor
21433 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21434 .unwrap();
21435 let hunks = snapshot
21436 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
21437 .map(|hunk| match hunk {
21438 DisplayDiffHunk::Unfolded {
21439 display_row_range, ..
21440 } => display_row_range,
21441 DisplayDiffHunk::Folded { .. } => unreachable!(),
21442 })
21443 .collect::<Vec<_>>();
21444 assert_eq!(
21445 hunks,
21446 [
21447 DisplayRow(2)..DisplayRow(4),
21448 DisplayRow(7)..DisplayRow(9),
21449 DisplayRow(12)..DisplayRow(14),
21450 ]
21451 );
21452}
21453
21454#[gpui::test]
21455async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
21456 init_test(cx, |_| {});
21457
21458 let mut cx = EditorTestContext::new(cx).await;
21459 cx.set_head_text(indoc! { "
21460 one
21461 two
21462 three
21463 four
21464 five
21465 "
21466 });
21467 cx.set_index_text(indoc! { "
21468 one
21469 two
21470 three
21471 four
21472 five
21473 "
21474 });
21475 cx.set_state(indoc! {"
21476 one
21477 TWO
21478 ˇTHREE
21479 FOUR
21480 five
21481 "});
21482 cx.run_until_parked();
21483 cx.update_editor(|editor, window, cx| {
21484 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21485 });
21486 cx.run_until_parked();
21487 cx.assert_index_text(Some(indoc! {"
21488 one
21489 TWO
21490 THREE
21491 FOUR
21492 five
21493 "}));
21494 cx.set_state(indoc! { "
21495 one
21496 TWO
21497 ˇTHREE-HUNDRED
21498 FOUR
21499 five
21500 "});
21501 cx.run_until_parked();
21502 cx.update_editor(|editor, window, cx| {
21503 let snapshot = editor.snapshot(window, cx);
21504 let hunks = editor
21505 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot())
21506 .collect::<Vec<_>>();
21507 assert_eq!(hunks.len(), 1);
21508 assert_eq!(
21509 hunks[0].status(),
21510 DiffHunkStatus {
21511 kind: DiffHunkStatusKind::Modified,
21512 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
21513 }
21514 );
21515
21516 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
21517 });
21518 cx.run_until_parked();
21519 cx.assert_index_text(Some(indoc! {"
21520 one
21521 TWO
21522 THREE-HUNDRED
21523 FOUR
21524 five
21525 "}));
21526}
21527
21528#[gpui::test]
21529fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
21530 init_test(cx, |_| {});
21531
21532 let editor = cx.add_window(|window, cx| {
21533 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
21534 build_editor(buffer, window, cx)
21535 });
21536
21537 let render_args = Arc::new(Mutex::new(None));
21538 let snapshot = editor
21539 .update(cx, |editor, window, cx| {
21540 let snapshot = editor.buffer().read(cx).snapshot(cx);
21541 let range =
21542 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
21543
21544 struct RenderArgs {
21545 row: MultiBufferRow,
21546 folded: bool,
21547 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
21548 }
21549
21550 let crease = Crease::inline(
21551 range,
21552 FoldPlaceholder::test(),
21553 {
21554 let toggle_callback = render_args.clone();
21555 move |row, folded, callback, _window, _cx| {
21556 *toggle_callback.lock() = Some(RenderArgs {
21557 row,
21558 folded,
21559 callback,
21560 });
21561 div()
21562 }
21563 },
21564 |_row, _folded, _window, _cx| div(),
21565 );
21566
21567 editor.insert_creases(Some(crease), cx);
21568 let snapshot = editor.snapshot(window, cx);
21569 let _div =
21570 snapshot.render_crease_toggle(MultiBufferRow(1), false, cx.entity(), window, cx);
21571 snapshot
21572 })
21573 .unwrap();
21574
21575 let render_args = render_args.lock().take().unwrap();
21576 assert_eq!(render_args.row, MultiBufferRow(1));
21577 assert!(!render_args.folded);
21578 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21579
21580 cx.update_window(*editor, |_, window, cx| {
21581 (render_args.callback)(true, window, cx)
21582 })
21583 .unwrap();
21584 let snapshot = editor
21585 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21586 .unwrap();
21587 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
21588
21589 cx.update_window(*editor, |_, window, cx| {
21590 (render_args.callback)(false, window, cx)
21591 })
21592 .unwrap();
21593 let snapshot = editor
21594 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
21595 .unwrap();
21596 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
21597}
21598
21599#[gpui::test]
21600async fn test_input_text(cx: &mut TestAppContext) {
21601 init_test(cx, |_| {});
21602 let mut cx = EditorTestContext::new(cx).await;
21603
21604 cx.set_state(
21605 &r#"ˇone
21606 two
21607
21608 three
21609 fourˇ
21610 five
21611
21612 siˇx"#
21613 .unindent(),
21614 );
21615
21616 cx.dispatch_action(HandleInput(String::new()));
21617 cx.assert_editor_state(
21618 &r#"ˇone
21619 two
21620
21621 three
21622 fourˇ
21623 five
21624
21625 siˇx"#
21626 .unindent(),
21627 );
21628
21629 cx.dispatch_action(HandleInput("AAAA".to_string()));
21630 cx.assert_editor_state(
21631 &r#"AAAAˇone
21632 two
21633
21634 three
21635 fourAAAAˇ
21636 five
21637
21638 siAAAAˇx"#
21639 .unindent(),
21640 );
21641}
21642
21643#[gpui::test]
21644async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
21645 init_test(cx, |_| {});
21646
21647 let mut cx = EditorTestContext::new(cx).await;
21648 cx.set_state(
21649 r#"let foo = 1;
21650let foo = 2;
21651let foo = 3;
21652let fooˇ = 4;
21653let foo = 5;
21654let foo = 6;
21655let foo = 7;
21656let foo = 8;
21657let foo = 9;
21658let foo = 10;
21659let foo = 11;
21660let foo = 12;
21661let foo = 13;
21662let foo = 14;
21663let foo = 15;"#,
21664 );
21665
21666 cx.update_editor(|e, window, cx| {
21667 assert_eq!(
21668 e.next_scroll_position,
21669 NextScrollCursorCenterTopBottom::Center,
21670 "Default next scroll direction is center",
21671 );
21672
21673 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21674 assert_eq!(
21675 e.next_scroll_position,
21676 NextScrollCursorCenterTopBottom::Top,
21677 "After center, next scroll direction should be top",
21678 );
21679
21680 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21681 assert_eq!(
21682 e.next_scroll_position,
21683 NextScrollCursorCenterTopBottom::Bottom,
21684 "After top, next scroll direction should be bottom",
21685 );
21686
21687 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21688 assert_eq!(
21689 e.next_scroll_position,
21690 NextScrollCursorCenterTopBottom::Center,
21691 "After bottom, scrolling should start over",
21692 );
21693
21694 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
21695 assert_eq!(
21696 e.next_scroll_position,
21697 NextScrollCursorCenterTopBottom::Top,
21698 "Scrolling continues if retriggered fast enough"
21699 );
21700 });
21701
21702 cx.executor()
21703 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
21704 cx.executor().run_until_parked();
21705 cx.update_editor(|e, _, _| {
21706 assert_eq!(
21707 e.next_scroll_position,
21708 NextScrollCursorCenterTopBottom::Center,
21709 "If scrolling is not triggered fast enough, it should reset"
21710 );
21711 });
21712}
21713
21714#[gpui::test]
21715async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
21716 init_test(cx, |_| {});
21717 let mut cx = EditorLspTestContext::new_rust(
21718 lsp::ServerCapabilities {
21719 definition_provider: Some(lsp::OneOf::Left(true)),
21720 references_provider: Some(lsp::OneOf::Left(true)),
21721 ..lsp::ServerCapabilities::default()
21722 },
21723 cx,
21724 )
21725 .await;
21726
21727 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
21728 let go_to_definition = cx
21729 .lsp
21730 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21731 move |params, _| async move {
21732 if empty_go_to_definition {
21733 Ok(None)
21734 } else {
21735 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
21736 uri: params.text_document_position_params.text_document.uri,
21737 range: lsp::Range::new(
21738 lsp::Position::new(4, 3),
21739 lsp::Position::new(4, 6),
21740 ),
21741 })))
21742 }
21743 },
21744 );
21745 let references = cx
21746 .lsp
21747 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21748 Ok(Some(vec![lsp::Location {
21749 uri: params.text_document_position.text_document.uri,
21750 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
21751 }]))
21752 });
21753 (go_to_definition, references)
21754 };
21755
21756 cx.set_state(
21757 &r#"fn one() {
21758 let mut a = ˇtwo();
21759 }
21760
21761 fn two() {}"#
21762 .unindent(),
21763 );
21764 set_up_lsp_handlers(false, &mut cx);
21765 let navigated = cx
21766 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21767 .await
21768 .expect("Failed to navigate to definition");
21769 assert_eq!(
21770 navigated,
21771 Navigated::Yes,
21772 "Should have navigated to definition from the GetDefinition response"
21773 );
21774 cx.assert_editor_state(
21775 &r#"fn one() {
21776 let mut a = two();
21777 }
21778
21779 fn «twoˇ»() {}"#
21780 .unindent(),
21781 );
21782
21783 let editors = cx.update_workspace(|workspace, _, cx| {
21784 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21785 });
21786 cx.update_editor(|_, _, test_editor_cx| {
21787 assert_eq!(
21788 editors.len(),
21789 1,
21790 "Initially, only one, test, editor should be open in the workspace"
21791 );
21792 assert_eq!(
21793 test_editor_cx.entity(),
21794 editors.last().expect("Asserted len is 1").clone()
21795 );
21796 });
21797
21798 set_up_lsp_handlers(true, &mut cx);
21799 let navigated = cx
21800 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21801 .await
21802 .expect("Failed to navigate to lookup references");
21803 assert_eq!(
21804 navigated,
21805 Navigated::Yes,
21806 "Should have navigated to references as a fallback after empty GoToDefinition response"
21807 );
21808 // We should not change the selections in the existing file,
21809 // if opening another milti buffer with the references
21810 cx.assert_editor_state(
21811 &r#"fn one() {
21812 let mut a = two();
21813 }
21814
21815 fn «twoˇ»() {}"#
21816 .unindent(),
21817 );
21818 let editors = cx.update_workspace(|workspace, _, cx| {
21819 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21820 });
21821 cx.update_editor(|_, _, test_editor_cx| {
21822 assert_eq!(
21823 editors.len(),
21824 2,
21825 "After falling back to references search, we open a new editor with the results"
21826 );
21827 let references_fallback_text = editors
21828 .into_iter()
21829 .find(|new_editor| *new_editor != test_editor_cx.entity())
21830 .expect("Should have one non-test editor now")
21831 .read(test_editor_cx)
21832 .text(test_editor_cx);
21833 assert_eq!(
21834 references_fallback_text, "fn one() {\n let mut a = two();\n}",
21835 "Should use the range from the references response and not the GoToDefinition one"
21836 );
21837 });
21838}
21839
21840#[gpui::test]
21841async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
21842 init_test(cx, |_| {});
21843 cx.update(|cx| {
21844 let mut editor_settings = EditorSettings::get_global(cx).clone();
21845 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
21846 EditorSettings::override_global(editor_settings, cx);
21847 });
21848 let mut cx = EditorLspTestContext::new_rust(
21849 lsp::ServerCapabilities {
21850 definition_provider: Some(lsp::OneOf::Left(true)),
21851 references_provider: Some(lsp::OneOf::Left(true)),
21852 ..lsp::ServerCapabilities::default()
21853 },
21854 cx,
21855 )
21856 .await;
21857 let original_state = r#"fn one() {
21858 let mut a = ˇtwo();
21859 }
21860
21861 fn two() {}"#
21862 .unindent();
21863 cx.set_state(&original_state);
21864
21865 let mut go_to_definition = cx
21866 .lsp
21867 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
21868 move |_, _| async move { Ok(None) },
21869 );
21870 let _references = cx
21871 .lsp
21872 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
21873 panic!("Should not call for references with no go to definition fallback")
21874 });
21875
21876 let navigated = cx
21877 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
21878 .await
21879 .expect("Failed to navigate to lookup references");
21880 go_to_definition
21881 .next()
21882 .await
21883 .expect("Should have called the go_to_definition handler");
21884
21885 assert_eq!(
21886 navigated,
21887 Navigated::No,
21888 "Should have navigated to references as a fallback after empty GoToDefinition response"
21889 );
21890 cx.assert_editor_state(&original_state);
21891 let editors = cx.update_workspace(|workspace, _, cx| {
21892 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21893 });
21894 cx.update_editor(|_, _, _| {
21895 assert_eq!(
21896 editors.len(),
21897 1,
21898 "After unsuccessful fallback, no other editor should have been opened"
21899 );
21900 });
21901}
21902
21903#[gpui::test]
21904async fn test_find_all_references_editor_reuse(cx: &mut TestAppContext) {
21905 init_test(cx, |_| {});
21906 let mut cx = EditorLspTestContext::new_rust(
21907 lsp::ServerCapabilities {
21908 references_provider: Some(lsp::OneOf::Left(true)),
21909 ..lsp::ServerCapabilities::default()
21910 },
21911 cx,
21912 )
21913 .await;
21914
21915 cx.set_state(
21916 &r#"
21917 fn one() {
21918 let mut a = two();
21919 }
21920
21921 fn ˇtwo() {}"#
21922 .unindent(),
21923 );
21924 cx.lsp
21925 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
21926 Ok(Some(vec![
21927 lsp::Location {
21928 uri: params.text_document_position.text_document.uri.clone(),
21929 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
21930 },
21931 lsp::Location {
21932 uri: params.text_document_position.text_document.uri,
21933 range: lsp::Range::new(lsp::Position::new(4, 4), lsp::Position::new(4, 7)),
21934 },
21935 ]))
21936 });
21937 let navigated = cx
21938 .update_editor(|editor, window, cx| {
21939 editor.find_all_references(&FindAllReferences, window, cx)
21940 })
21941 .unwrap()
21942 .await
21943 .expect("Failed to navigate to references");
21944 assert_eq!(
21945 navigated,
21946 Navigated::Yes,
21947 "Should have navigated to references from the FindAllReferences response"
21948 );
21949 cx.assert_editor_state(
21950 &r#"fn one() {
21951 let mut a = two();
21952 }
21953
21954 fn ˇtwo() {}"#
21955 .unindent(),
21956 );
21957
21958 let editors = cx.update_workspace(|workspace, _, cx| {
21959 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21960 });
21961 cx.update_editor(|_, _, _| {
21962 assert_eq!(editors.len(), 2, "We should have opened a new multibuffer");
21963 });
21964
21965 cx.set_state(
21966 &r#"fn one() {
21967 let mut a = ˇtwo();
21968 }
21969
21970 fn two() {}"#
21971 .unindent(),
21972 );
21973 let navigated = cx
21974 .update_editor(|editor, window, cx| {
21975 editor.find_all_references(&FindAllReferences, window, cx)
21976 })
21977 .unwrap()
21978 .await
21979 .expect("Failed to navigate to references");
21980 assert_eq!(
21981 navigated,
21982 Navigated::Yes,
21983 "Should have navigated to references from the FindAllReferences response"
21984 );
21985 cx.assert_editor_state(
21986 &r#"fn one() {
21987 let mut a = ˇtwo();
21988 }
21989
21990 fn two() {}"#
21991 .unindent(),
21992 );
21993 let editors = cx.update_workspace(|workspace, _, cx| {
21994 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
21995 });
21996 cx.update_editor(|_, _, _| {
21997 assert_eq!(
21998 editors.len(),
21999 2,
22000 "should have re-used the previous multibuffer"
22001 );
22002 });
22003
22004 cx.set_state(
22005 &r#"fn one() {
22006 let mut a = ˇtwo();
22007 }
22008 fn three() {}
22009 fn two() {}"#
22010 .unindent(),
22011 );
22012 cx.lsp
22013 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
22014 Ok(Some(vec![
22015 lsp::Location {
22016 uri: params.text_document_position.text_document.uri.clone(),
22017 range: lsp::Range::new(lsp::Position::new(0, 16), lsp::Position::new(0, 19)),
22018 },
22019 lsp::Location {
22020 uri: params.text_document_position.text_document.uri,
22021 range: lsp::Range::new(lsp::Position::new(5, 4), lsp::Position::new(5, 7)),
22022 },
22023 ]))
22024 });
22025 let navigated = cx
22026 .update_editor(|editor, window, cx| {
22027 editor.find_all_references(&FindAllReferences, window, cx)
22028 })
22029 .unwrap()
22030 .await
22031 .expect("Failed to navigate to references");
22032 assert_eq!(
22033 navigated,
22034 Navigated::Yes,
22035 "Should have navigated to references from the FindAllReferences response"
22036 );
22037 cx.assert_editor_state(
22038 &r#"fn one() {
22039 let mut a = ˇtwo();
22040 }
22041 fn three() {}
22042 fn two() {}"#
22043 .unindent(),
22044 );
22045 let editors = cx.update_workspace(|workspace, _, cx| {
22046 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
22047 });
22048 cx.update_editor(|_, _, _| {
22049 assert_eq!(
22050 editors.len(),
22051 3,
22052 "should have used a new multibuffer as offsets changed"
22053 );
22054 });
22055}
22056#[gpui::test]
22057async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
22058 init_test(cx, |_| {});
22059
22060 let language = Arc::new(Language::new(
22061 LanguageConfig::default(),
22062 Some(tree_sitter_rust::LANGUAGE.into()),
22063 ));
22064
22065 let text = r#"
22066 #[cfg(test)]
22067 mod tests() {
22068 #[test]
22069 fn runnable_1() {
22070 let a = 1;
22071 }
22072
22073 #[test]
22074 fn runnable_2() {
22075 let a = 1;
22076 let b = 2;
22077 }
22078 }
22079 "#
22080 .unindent();
22081
22082 let fs = FakeFs::new(cx.executor());
22083 fs.insert_file("/file.rs", Default::default()).await;
22084
22085 let project = Project::test(fs, ["/a".as_ref()], cx).await;
22086 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22087 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22088 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
22089 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
22090
22091 let editor = cx.new_window_entity(|window, cx| {
22092 Editor::new(
22093 EditorMode::full(),
22094 multi_buffer,
22095 Some(project.clone()),
22096 window,
22097 cx,
22098 )
22099 });
22100
22101 editor.update_in(cx, |editor, window, cx| {
22102 let snapshot = editor.buffer().read(cx).snapshot(cx);
22103 editor.tasks.insert(
22104 (buffer.read(cx).remote_id(), 3),
22105 RunnableTasks {
22106 templates: vec![],
22107 offset: snapshot.anchor_before(43),
22108 column: 0,
22109 extra_variables: HashMap::default(),
22110 context_range: BufferOffset(43)..BufferOffset(85),
22111 },
22112 );
22113 editor.tasks.insert(
22114 (buffer.read(cx).remote_id(), 8),
22115 RunnableTasks {
22116 templates: vec![],
22117 offset: snapshot.anchor_before(86),
22118 column: 0,
22119 extra_variables: HashMap::default(),
22120 context_range: BufferOffset(86)..BufferOffset(191),
22121 },
22122 );
22123
22124 // Test finding task when cursor is inside function body
22125 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22126 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
22127 });
22128 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22129 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
22130
22131 // Test finding task when cursor is on function name
22132 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22133 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
22134 });
22135 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
22136 assert_eq!(row, 8, "Should find task when cursor is on function name");
22137 });
22138}
22139
22140#[gpui::test]
22141async fn test_folding_buffers(cx: &mut TestAppContext) {
22142 init_test(cx, |_| {});
22143
22144 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22145 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
22146 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
22147
22148 let fs = FakeFs::new(cx.executor());
22149 fs.insert_tree(
22150 path!("/a"),
22151 json!({
22152 "first.rs": sample_text_1,
22153 "second.rs": sample_text_2,
22154 "third.rs": sample_text_3,
22155 }),
22156 )
22157 .await;
22158 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22159 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22160 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22161 let worktree = project.update(cx, |project, cx| {
22162 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22163 assert_eq!(worktrees.len(), 1);
22164 worktrees.pop().unwrap()
22165 });
22166 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22167
22168 let buffer_1 = project
22169 .update(cx, |project, cx| {
22170 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22171 })
22172 .await
22173 .unwrap();
22174 let buffer_2 = project
22175 .update(cx, |project, cx| {
22176 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22177 })
22178 .await
22179 .unwrap();
22180 let buffer_3 = project
22181 .update(cx, |project, cx| {
22182 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22183 })
22184 .await
22185 .unwrap();
22186
22187 let multi_buffer = cx.new(|cx| {
22188 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22189 multi_buffer.push_excerpts(
22190 buffer_1.clone(),
22191 [
22192 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22193 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22194 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22195 ],
22196 cx,
22197 );
22198 multi_buffer.push_excerpts(
22199 buffer_2.clone(),
22200 [
22201 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22202 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22203 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22204 ],
22205 cx,
22206 );
22207 multi_buffer.push_excerpts(
22208 buffer_3.clone(),
22209 [
22210 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
22211 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
22212 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
22213 ],
22214 cx,
22215 );
22216 multi_buffer
22217 });
22218 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22219 Editor::new(
22220 EditorMode::full(),
22221 multi_buffer.clone(),
22222 Some(project.clone()),
22223 window,
22224 cx,
22225 )
22226 });
22227
22228 assert_eq!(
22229 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22230 "\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",
22231 );
22232
22233 multi_buffer_editor.update(cx, |editor, cx| {
22234 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22235 });
22236 assert_eq!(
22237 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22238 "\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",
22239 "After folding the first buffer, its text should not be displayed"
22240 );
22241
22242 multi_buffer_editor.update(cx, |editor, cx| {
22243 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22244 });
22245 assert_eq!(
22246 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22247 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
22248 "After folding the second buffer, its text should not be displayed"
22249 );
22250
22251 multi_buffer_editor.update(cx, |editor, cx| {
22252 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22253 });
22254 assert_eq!(
22255 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22256 "\n\n\n\n\n",
22257 "After folding the third buffer, its text should not be displayed"
22258 );
22259
22260 // Emulate selection inside the fold logic, that should work
22261 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22262 editor
22263 .snapshot(window, cx)
22264 .next_line_boundary(Point::new(0, 4));
22265 });
22266
22267 multi_buffer_editor.update(cx, |editor, cx| {
22268 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22269 });
22270 assert_eq!(
22271 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22272 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22273 "After unfolding the second buffer, its text should be displayed"
22274 );
22275
22276 // Typing inside of buffer 1 causes that buffer to be unfolded.
22277 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22278 assert_eq!(
22279 multi_buffer
22280 .read(cx)
22281 .snapshot(cx)
22282 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
22283 .collect::<String>(),
22284 "bbbb"
22285 );
22286 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
22287 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
22288 });
22289 editor.handle_input("B", window, cx);
22290 });
22291
22292 assert_eq!(
22293 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22294 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
22295 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
22296 );
22297
22298 multi_buffer_editor.update(cx, |editor, cx| {
22299 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22300 });
22301 assert_eq!(
22302 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22303 "\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",
22304 "After unfolding the all buffers, all original text should be displayed"
22305 );
22306}
22307
22308#[gpui::test]
22309async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
22310 init_test(cx, |_| {});
22311
22312 let sample_text_1 = "1111\n2222\n3333".to_string();
22313 let sample_text_2 = "4444\n5555\n6666".to_string();
22314 let sample_text_3 = "7777\n8888\n9999".to_string();
22315
22316 let fs = FakeFs::new(cx.executor());
22317 fs.insert_tree(
22318 path!("/a"),
22319 json!({
22320 "first.rs": sample_text_1,
22321 "second.rs": sample_text_2,
22322 "third.rs": sample_text_3,
22323 }),
22324 )
22325 .await;
22326 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22327 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22328 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22329 let worktree = project.update(cx, |project, cx| {
22330 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22331 assert_eq!(worktrees.len(), 1);
22332 worktrees.pop().unwrap()
22333 });
22334 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22335
22336 let buffer_1 = project
22337 .update(cx, |project, cx| {
22338 project.open_buffer((worktree_id, rel_path("first.rs")), cx)
22339 })
22340 .await
22341 .unwrap();
22342 let buffer_2 = project
22343 .update(cx, |project, cx| {
22344 project.open_buffer((worktree_id, rel_path("second.rs")), cx)
22345 })
22346 .await
22347 .unwrap();
22348 let buffer_3 = project
22349 .update(cx, |project, cx| {
22350 project.open_buffer((worktree_id, rel_path("third.rs")), cx)
22351 })
22352 .await
22353 .unwrap();
22354
22355 let multi_buffer = cx.new(|cx| {
22356 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22357 multi_buffer.push_excerpts(
22358 buffer_1.clone(),
22359 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22360 cx,
22361 );
22362 multi_buffer.push_excerpts(
22363 buffer_2.clone(),
22364 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22365 cx,
22366 );
22367 multi_buffer.push_excerpts(
22368 buffer_3.clone(),
22369 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
22370 cx,
22371 );
22372 multi_buffer
22373 });
22374
22375 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22376 Editor::new(
22377 EditorMode::full(),
22378 multi_buffer,
22379 Some(project.clone()),
22380 window,
22381 cx,
22382 )
22383 });
22384
22385 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
22386 assert_eq!(
22387 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22388 full_text,
22389 );
22390
22391 multi_buffer_editor.update(cx, |editor, cx| {
22392 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
22393 });
22394 assert_eq!(
22395 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22396 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
22397 "After folding the first buffer, its text should not be displayed"
22398 );
22399
22400 multi_buffer_editor.update(cx, |editor, cx| {
22401 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
22402 });
22403
22404 assert_eq!(
22405 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22406 "\n\n\n\n\n\n7777\n8888\n9999",
22407 "After folding the second buffer, its text should not be displayed"
22408 );
22409
22410 multi_buffer_editor.update(cx, |editor, cx| {
22411 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
22412 });
22413 assert_eq!(
22414 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22415 "\n\n\n\n\n",
22416 "After folding the third buffer, its text should not be displayed"
22417 );
22418
22419 multi_buffer_editor.update(cx, |editor, cx| {
22420 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
22421 });
22422 assert_eq!(
22423 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22424 "\n\n\n\n4444\n5555\n6666\n\n",
22425 "After unfolding the second buffer, its text should be displayed"
22426 );
22427
22428 multi_buffer_editor.update(cx, |editor, cx| {
22429 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
22430 });
22431 assert_eq!(
22432 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22433 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
22434 "After unfolding the first buffer, its text should be displayed"
22435 );
22436
22437 multi_buffer_editor.update(cx, |editor, cx| {
22438 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
22439 });
22440 assert_eq!(
22441 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22442 full_text,
22443 "After unfolding all buffers, all original text should be displayed"
22444 );
22445}
22446
22447#[gpui::test]
22448async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
22449 init_test(cx, |_| {});
22450
22451 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
22452
22453 let fs = FakeFs::new(cx.executor());
22454 fs.insert_tree(
22455 path!("/a"),
22456 json!({
22457 "main.rs": sample_text,
22458 }),
22459 )
22460 .await;
22461 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22462 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22463 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22464 let worktree = project.update(cx, |project, cx| {
22465 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
22466 assert_eq!(worktrees.len(), 1);
22467 worktrees.pop().unwrap()
22468 });
22469 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
22470
22471 let buffer_1 = project
22472 .update(cx, |project, cx| {
22473 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
22474 })
22475 .await
22476 .unwrap();
22477
22478 let multi_buffer = cx.new(|cx| {
22479 let mut multi_buffer = MultiBuffer::new(ReadWrite);
22480 multi_buffer.push_excerpts(
22481 buffer_1.clone(),
22482 [ExcerptRange::new(
22483 Point::new(0, 0)
22484 ..Point::new(
22485 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
22486 0,
22487 ),
22488 )],
22489 cx,
22490 );
22491 multi_buffer
22492 });
22493 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
22494 Editor::new(
22495 EditorMode::full(),
22496 multi_buffer,
22497 Some(project.clone()),
22498 window,
22499 cx,
22500 )
22501 });
22502
22503 let selection_range = Point::new(1, 0)..Point::new(2, 0);
22504 multi_buffer_editor.update_in(cx, |editor, window, cx| {
22505 enum TestHighlight {}
22506 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
22507 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
22508 editor.highlight_text::<TestHighlight>(
22509 vec![highlight_range.clone()],
22510 HighlightStyle::color(Hsla::green()),
22511 cx,
22512 );
22513 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
22514 s.select_ranges(Some(highlight_range))
22515 });
22516 });
22517
22518 let full_text = format!("\n\n{sample_text}");
22519 assert_eq!(
22520 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
22521 full_text,
22522 );
22523}
22524
22525#[gpui::test]
22526async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
22527 init_test(cx, |_| {});
22528 cx.update(|cx| {
22529 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
22530 "keymaps/default-linux.json",
22531 cx,
22532 )
22533 .unwrap();
22534 cx.bind_keys(default_key_bindings);
22535 });
22536
22537 let (editor, cx) = cx.add_window_view(|window, cx| {
22538 let multi_buffer = MultiBuffer::build_multi(
22539 [
22540 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
22541 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
22542 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
22543 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
22544 ],
22545 cx,
22546 );
22547 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
22548
22549 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
22550 // fold all but the second buffer, so that we test navigating between two
22551 // adjacent folded buffers, as well as folded buffers at the start and
22552 // end the multibuffer
22553 editor.fold_buffer(buffer_ids[0], cx);
22554 editor.fold_buffer(buffer_ids[2], cx);
22555 editor.fold_buffer(buffer_ids[3], cx);
22556
22557 editor
22558 });
22559 cx.simulate_resize(size(px(1000.), px(1000.)));
22560
22561 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
22562 cx.assert_excerpts_with_selections(indoc! {"
22563 [EXCERPT]
22564 ˇ[FOLDED]
22565 [EXCERPT]
22566 a1
22567 b1
22568 [EXCERPT]
22569 [FOLDED]
22570 [EXCERPT]
22571 [FOLDED]
22572 "
22573 });
22574 cx.simulate_keystroke("down");
22575 cx.assert_excerpts_with_selections(indoc! {"
22576 [EXCERPT]
22577 [FOLDED]
22578 [EXCERPT]
22579 ˇa1
22580 b1
22581 [EXCERPT]
22582 [FOLDED]
22583 [EXCERPT]
22584 [FOLDED]
22585 "
22586 });
22587 cx.simulate_keystroke("down");
22588 cx.assert_excerpts_with_selections(indoc! {"
22589 [EXCERPT]
22590 [FOLDED]
22591 [EXCERPT]
22592 a1
22593 ˇb1
22594 [EXCERPT]
22595 [FOLDED]
22596 [EXCERPT]
22597 [FOLDED]
22598 "
22599 });
22600 cx.simulate_keystroke("down");
22601 cx.assert_excerpts_with_selections(indoc! {"
22602 [EXCERPT]
22603 [FOLDED]
22604 [EXCERPT]
22605 a1
22606 b1
22607 ˇ[EXCERPT]
22608 [FOLDED]
22609 [EXCERPT]
22610 [FOLDED]
22611 "
22612 });
22613 cx.simulate_keystroke("down");
22614 cx.assert_excerpts_with_selections(indoc! {"
22615 [EXCERPT]
22616 [FOLDED]
22617 [EXCERPT]
22618 a1
22619 b1
22620 [EXCERPT]
22621 ˇ[FOLDED]
22622 [EXCERPT]
22623 [FOLDED]
22624 "
22625 });
22626 for _ in 0..5 {
22627 cx.simulate_keystroke("down");
22628 cx.assert_excerpts_with_selections(indoc! {"
22629 [EXCERPT]
22630 [FOLDED]
22631 [EXCERPT]
22632 a1
22633 b1
22634 [EXCERPT]
22635 [FOLDED]
22636 [EXCERPT]
22637 ˇ[FOLDED]
22638 "
22639 });
22640 }
22641
22642 cx.simulate_keystroke("up");
22643 cx.assert_excerpts_with_selections(indoc! {"
22644 [EXCERPT]
22645 [FOLDED]
22646 [EXCERPT]
22647 a1
22648 b1
22649 [EXCERPT]
22650 ˇ[FOLDED]
22651 [EXCERPT]
22652 [FOLDED]
22653 "
22654 });
22655 cx.simulate_keystroke("up");
22656 cx.assert_excerpts_with_selections(indoc! {"
22657 [EXCERPT]
22658 [FOLDED]
22659 [EXCERPT]
22660 a1
22661 b1
22662 ˇ[EXCERPT]
22663 [FOLDED]
22664 [EXCERPT]
22665 [FOLDED]
22666 "
22667 });
22668 cx.simulate_keystroke("up");
22669 cx.assert_excerpts_with_selections(indoc! {"
22670 [EXCERPT]
22671 [FOLDED]
22672 [EXCERPT]
22673 a1
22674 ˇb1
22675 [EXCERPT]
22676 [FOLDED]
22677 [EXCERPT]
22678 [FOLDED]
22679 "
22680 });
22681 cx.simulate_keystroke("up");
22682 cx.assert_excerpts_with_selections(indoc! {"
22683 [EXCERPT]
22684 [FOLDED]
22685 [EXCERPT]
22686 ˇa1
22687 b1
22688 [EXCERPT]
22689 [FOLDED]
22690 [EXCERPT]
22691 [FOLDED]
22692 "
22693 });
22694 for _ in 0..5 {
22695 cx.simulate_keystroke("up");
22696 cx.assert_excerpts_with_selections(indoc! {"
22697 [EXCERPT]
22698 ˇ[FOLDED]
22699 [EXCERPT]
22700 a1
22701 b1
22702 [EXCERPT]
22703 [FOLDED]
22704 [EXCERPT]
22705 [FOLDED]
22706 "
22707 });
22708 }
22709}
22710
22711#[gpui::test]
22712async fn test_edit_prediction_text(cx: &mut TestAppContext) {
22713 init_test(cx, |_| {});
22714
22715 // Simple insertion
22716 assert_highlighted_edits(
22717 "Hello, world!",
22718 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
22719 true,
22720 cx,
22721 |highlighted_edits, cx| {
22722 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
22723 assert_eq!(highlighted_edits.highlights.len(), 1);
22724 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
22725 assert_eq!(
22726 highlighted_edits.highlights[0].1.background_color,
22727 Some(cx.theme().status().created_background)
22728 );
22729 },
22730 )
22731 .await;
22732
22733 // Replacement
22734 assert_highlighted_edits(
22735 "This is a test.",
22736 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
22737 false,
22738 cx,
22739 |highlighted_edits, cx| {
22740 assert_eq!(highlighted_edits.text, "That is a test.");
22741 assert_eq!(highlighted_edits.highlights.len(), 1);
22742 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
22743 assert_eq!(
22744 highlighted_edits.highlights[0].1.background_color,
22745 Some(cx.theme().status().created_background)
22746 );
22747 },
22748 )
22749 .await;
22750
22751 // Multiple edits
22752 assert_highlighted_edits(
22753 "Hello, world!",
22754 vec![
22755 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
22756 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
22757 ],
22758 false,
22759 cx,
22760 |highlighted_edits, cx| {
22761 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
22762 assert_eq!(highlighted_edits.highlights.len(), 2);
22763 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
22764 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
22765 assert_eq!(
22766 highlighted_edits.highlights[0].1.background_color,
22767 Some(cx.theme().status().created_background)
22768 );
22769 assert_eq!(
22770 highlighted_edits.highlights[1].1.background_color,
22771 Some(cx.theme().status().created_background)
22772 );
22773 },
22774 )
22775 .await;
22776
22777 // Multiple lines with edits
22778 assert_highlighted_edits(
22779 "First line\nSecond line\nThird line\nFourth line",
22780 vec![
22781 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
22782 (
22783 Point::new(2, 0)..Point::new(2, 10),
22784 "New third line".to_string(),
22785 ),
22786 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
22787 ],
22788 false,
22789 cx,
22790 |highlighted_edits, cx| {
22791 assert_eq!(
22792 highlighted_edits.text,
22793 "Second modified\nNew third line\nFourth updated line"
22794 );
22795 assert_eq!(highlighted_edits.highlights.len(), 3);
22796 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
22797 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
22798 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
22799 for highlight in &highlighted_edits.highlights {
22800 assert_eq!(
22801 highlight.1.background_color,
22802 Some(cx.theme().status().created_background)
22803 );
22804 }
22805 },
22806 )
22807 .await;
22808}
22809
22810#[gpui::test]
22811async fn test_edit_prediction_text_with_deletions(cx: &mut TestAppContext) {
22812 init_test(cx, |_| {});
22813
22814 // Deletion
22815 assert_highlighted_edits(
22816 "Hello, world!",
22817 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
22818 true,
22819 cx,
22820 |highlighted_edits, cx| {
22821 assert_eq!(highlighted_edits.text, "Hello, world!");
22822 assert_eq!(highlighted_edits.highlights.len(), 1);
22823 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
22824 assert_eq!(
22825 highlighted_edits.highlights[0].1.background_color,
22826 Some(cx.theme().status().deleted_background)
22827 );
22828 },
22829 )
22830 .await;
22831
22832 // Insertion
22833 assert_highlighted_edits(
22834 "Hello, world!",
22835 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
22836 true,
22837 cx,
22838 |highlighted_edits, cx| {
22839 assert_eq!(highlighted_edits.highlights.len(), 1);
22840 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
22841 assert_eq!(
22842 highlighted_edits.highlights[0].1.background_color,
22843 Some(cx.theme().status().created_background)
22844 );
22845 },
22846 )
22847 .await;
22848}
22849
22850async fn assert_highlighted_edits(
22851 text: &str,
22852 edits: Vec<(Range<Point>, String)>,
22853 include_deletions: bool,
22854 cx: &mut TestAppContext,
22855 assertion_fn: impl Fn(HighlightedText, &App),
22856) {
22857 let window = cx.add_window(|window, cx| {
22858 let buffer = MultiBuffer::build_simple(text, cx);
22859 Editor::new(EditorMode::full(), buffer, None, window, cx)
22860 });
22861 let cx = &mut VisualTestContext::from_window(*window, cx);
22862
22863 let (buffer, snapshot) = window
22864 .update(cx, |editor, _window, cx| {
22865 (
22866 editor.buffer().clone(),
22867 editor.buffer().read(cx).snapshot(cx),
22868 )
22869 })
22870 .unwrap();
22871
22872 let edits = edits
22873 .into_iter()
22874 .map(|(range, edit)| {
22875 (
22876 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
22877 edit,
22878 )
22879 })
22880 .collect::<Vec<_>>();
22881
22882 let text_anchor_edits = edits
22883 .clone()
22884 .into_iter()
22885 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit.into()))
22886 .collect::<Vec<_>>();
22887
22888 let edit_preview = window
22889 .update(cx, |_, _window, cx| {
22890 buffer
22891 .read(cx)
22892 .as_singleton()
22893 .unwrap()
22894 .read(cx)
22895 .preview_edits(text_anchor_edits.into(), cx)
22896 })
22897 .unwrap()
22898 .await;
22899
22900 cx.update(|_window, cx| {
22901 let highlighted_edits = edit_prediction_edit_text(
22902 snapshot.as_singleton().unwrap().2,
22903 &edits,
22904 &edit_preview,
22905 include_deletions,
22906 cx,
22907 );
22908 assertion_fn(highlighted_edits, cx)
22909 });
22910}
22911
22912#[track_caller]
22913fn assert_breakpoint(
22914 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
22915 path: &Arc<Path>,
22916 expected: Vec<(u32, Breakpoint)>,
22917) {
22918 if expected.is_empty() {
22919 assert!(!breakpoints.contains_key(path), "{}", path.display());
22920 } else {
22921 let mut breakpoint = breakpoints
22922 .get(path)
22923 .unwrap()
22924 .iter()
22925 .map(|breakpoint| {
22926 (
22927 breakpoint.row,
22928 Breakpoint {
22929 message: breakpoint.message.clone(),
22930 state: breakpoint.state,
22931 condition: breakpoint.condition.clone(),
22932 hit_condition: breakpoint.hit_condition.clone(),
22933 },
22934 )
22935 })
22936 .collect::<Vec<_>>();
22937
22938 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
22939
22940 assert_eq!(expected, breakpoint);
22941 }
22942}
22943
22944fn add_log_breakpoint_at_cursor(
22945 editor: &mut Editor,
22946 log_message: &str,
22947 window: &mut Window,
22948 cx: &mut Context<Editor>,
22949) {
22950 let (anchor, bp) = editor
22951 .breakpoints_at_cursors(window, cx)
22952 .first()
22953 .and_then(|(anchor, bp)| bp.as_ref().map(|bp| (*anchor, bp.clone())))
22954 .unwrap_or_else(|| {
22955 let snapshot = editor.snapshot(window, cx);
22956 let cursor_position: Point =
22957 editor.selections.newest(&snapshot.display_snapshot).head();
22958
22959 let breakpoint_position = snapshot
22960 .buffer_snapshot()
22961 .anchor_before(Point::new(cursor_position.row, 0));
22962
22963 (breakpoint_position, Breakpoint::new_log(log_message))
22964 });
22965
22966 editor.edit_breakpoint_at_anchor(
22967 anchor,
22968 bp,
22969 BreakpointEditAction::EditLogMessage(log_message.into()),
22970 cx,
22971 );
22972}
22973
22974#[gpui::test]
22975async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
22976 init_test(cx, |_| {});
22977
22978 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
22979 let fs = FakeFs::new(cx.executor());
22980 fs.insert_tree(
22981 path!("/a"),
22982 json!({
22983 "main.rs": sample_text,
22984 }),
22985 )
22986 .await;
22987 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
22988 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
22989 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
22990
22991 let fs = FakeFs::new(cx.executor());
22992 fs.insert_tree(
22993 path!("/a"),
22994 json!({
22995 "main.rs": sample_text,
22996 }),
22997 )
22998 .await;
22999 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23000 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23001 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23002 let worktree_id = workspace
23003 .update(cx, |workspace, _window, cx| {
23004 workspace.project().update(cx, |project, cx| {
23005 project.worktrees(cx).next().unwrap().read(cx).id()
23006 })
23007 })
23008 .unwrap();
23009
23010 let buffer = project
23011 .update(cx, |project, cx| {
23012 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23013 })
23014 .await
23015 .unwrap();
23016
23017 let (editor, cx) = cx.add_window_view(|window, cx| {
23018 Editor::new(
23019 EditorMode::full(),
23020 MultiBuffer::build_from_buffer(buffer, cx),
23021 Some(project.clone()),
23022 window,
23023 cx,
23024 )
23025 });
23026
23027 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23028 let abs_path = project.read_with(cx, |project, cx| {
23029 project
23030 .absolute_path(&project_path, cx)
23031 .map(Arc::from)
23032 .unwrap()
23033 });
23034
23035 // assert we can add breakpoint on the first line
23036 editor.update_in(cx, |editor, window, cx| {
23037 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23038 editor.move_to_end(&MoveToEnd, window, cx);
23039 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23040 });
23041
23042 let breakpoints = editor.update(cx, |editor, cx| {
23043 editor
23044 .breakpoint_store()
23045 .as_ref()
23046 .unwrap()
23047 .read(cx)
23048 .all_source_breakpoints(cx)
23049 });
23050
23051 assert_eq!(1, breakpoints.len());
23052 assert_breakpoint(
23053 &breakpoints,
23054 &abs_path,
23055 vec![
23056 (0, Breakpoint::new_standard()),
23057 (3, Breakpoint::new_standard()),
23058 ],
23059 );
23060
23061 editor.update_in(cx, |editor, window, cx| {
23062 editor.move_to_beginning(&MoveToBeginning, window, cx);
23063 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23064 });
23065
23066 let breakpoints = editor.update(cx, |editor, cx| {
23067 editor
23068 .breakpoint_store()
23069 .as_ref()
23070 .unwrap()
23071 .read(cx)
23072 .all_source_breakpoints(cx)
23073 });
23074
23075 assert_eq!(1, breakpoints.len());
23076 assert_breakpoint(
23077 &breakpoints,
23078 &abs_path,
23079 vec![(3, Breakpoint::new_standard())],
23080 );
23081
23082 editor.update_in(cx, |editor, window, cx| {
23083 editor.move_to_end(&MoveToEnd, window, cx);
23084 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23085 });
23086
23087 let breakpoints = editor.update(cx, |editor, cx| {
23088 editor
23089 .breakpoint_store()
23090 .as_ref()
23091 .unwrap()
23092 .read(cx)
23093 .all_source_breakpoints(cx)
23094 });
23095
23096 assert_eq!(0, breakpoints.len());
23097 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23098}
23099
23100#[gpui::test]
23101async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
23102 init_test(cx, |_| {});
23103
23104 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23105
23106 let fs = FakeFs::new(cx.executor());
23107 fs.insert_tree(
23108 path!("/a"),
23109 json!({
23110 "main.rs": sample_text,
23111 }),
23112 )
23113 .await;
23114 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23115 let (workspace, cx) =
23116 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23117
23118 let worktree_id = workspace.update(cx, |workspace, cx| {
23119 workspace.project().update(cx, |project, cx| {
23120 project.worktrees(cx).next().unwrap().read(cx).id()
23121 })
23122 });
23123
23124 let buffer = project
23125 .update(cx, |project, cx| {
23126 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23127 })
23128 .await
23129 .unwrap();
23130
23131 let (editor, cx) = cx.add_window_view(|window, cx| {
23132 Editor::new(
23133 EditorMode::full(),
23134 MultiBuffer::build_from_buffer(buffer, cx),
23135 Some(project.clone()),
23136 window,
23137 cx,
23138 )
23139 });
23140
23141 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23142 let abs_path = project.read_with(cx, |project, cx| {
23143 project
23144 .absolute_path(&project_path, cx)
23145 .map(Arc::from)
23146 .unwrap()
23147 });
23148
23149 editor.update_in(cx, |editor, window, cx| {
23150 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23151 });
23152
23153 let breakpoints = editor.update(cx, |editor, cx| {
23154 editor
23155 .breakpoint_store()
23156 .as_ref()
23157 .unwrap()
23158 .read(cx)
23159 .all_source_breakpoints(cx)
23160 });
23161
23162 assert_breakpoint(
23163 &breakpoints,
23164 &abs_path,
23165 vec![(0, Breakpoint::new_log("hello world"))],
23166 );
23167
23168 // Removing a log message from a log breakpoint should remove it
23169 editor.update_in(cx, |editor, window, cx| {
23170 add_log_breakpoint_at_cursor(editor, "", window, cx);
23171 });
23172
23173 let breakpoints = editor.update(cx, |editor, cx| {
23174 editor
23175 .breakpoint_store()
23176 .as_ref()
23177 .unwrap()
23178 .read(cx)
23179 .all_source_breakpoints(cx)
23180 });
23181
23182 assert_breakpoint(&breakpoints, &abs_path, vec![]);
23183
23184 editor.update_in(cx, |editor, window, cx| {
23185 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23186 editor.move_to_end(&MoveToEnd, window, cx);
23187 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23188 // Not adding a log message to a standard breakpoint shouldn't remove it
23189 add_log_breakpoint_at_cursor(editor, "", window, cx);
23190 });
23191
23192 let breakpoints = editor.update(cx, |editor, cx| {
23193 editor
23194 .breakpoint_store()
23195 .as_ref()
23196 .unwrap()
23197 .read(cx)
23198 .all_source_breakpoints(cx)
23199 });
23200
23201 assert_breakpoint(
23202 &breakpoints,
23203 &abs_path,
23204 vec![
23205 (0, Breakpoint::new_standard()),
23206 (3, Breakpoint::new_standard()),
23207 ],
23208 );
23209
23210 editor.update_in(cx, |editor, window, cx| {
23211 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
23212 });
23213
23214 let breakpoints = editor.update(cx, |editor, cx| {
23215 editor
23216 .breakpoint_store()
23217 .as_ref()
23218 .unwrap()
23219 .read(cx)
23220 .all_source_breakpoints(cx)
23221 });
23222
23223 assert_breakpoint(
23224 &breakpoints,
23225 &abs_path,
23226 vec![
23227 (0, Breakpoint::new_standard()),
23228 (3, Breakpoint::new_log("hello world")),
23229 ],
23230 );
23231
23232 editor.update_in(cx, |editor, window, cx| {
23233 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
23234 });
23235
23236 let breakpoints = editor.update(cx, |editor, cx| {
23237 editor
23238 .breakpoint_store()
23239 .as_ref()
23240 .unwrap()
23241 .read(cx)
23242 .all_source_breakpoints(cx)
23243 });
23244
23245 assert_breakpoint(
23246 &breakpoints,
23247 &abs_path,
23248 vec![
23249 (0, Breakpoint::new_standard()),
23250 (3, Breakpoint::new_log("hello Earth!!")),
23251 ],
23252 );
23253}
23254
23255/// This also tests that Editor::breakpoint_at_cursor_head is working properly
23256/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
23257/// or when breakpoints were placed out of order. This tests for a regression too
23258#[gpui::test]
23259async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
23260 init_test(cx, |_| {});
23261
23262 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
23263 let fs = FakeFs::new(cx.executor());
23264 fs.insert_tree(
23265 path!("/a"),
23266 json!({
23267 "main.rs": sample_text,
23268 }),
23269 )
23270 .await;
23271 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23272 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23273 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23274
23275 let fs = FakeFs::new(cx.executor());
23276 fs.insert_tree(
23277 path!("/a"),
23278 json!({
23279 "main.rs": sample_text,
23280 }),
23281 )
23282 .await;
23283 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23284 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23285 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23286 let worktree_id = workspace
23287 .update(cx, |workspace, _window, cx| {
23288 workspace.project().update(cx, |project, cx| {
23289 project.worktrees(cx).next().unwrap().read(cx).id()
23290 })
23291 })
23292 .unwrap();
23293
23294 let buffer = project
23295 .update(cx, |project, cx| {
23296 project.open_buffer((worktree_id, rel_path("main.rs")), cx)
23297 })
23298 .await
23299 .unwrap();
23300
23301 let (editor, cx) = cx.add_window_view(|window, cx| {
23302 Editor::new(
23303 EditorMode::full(),
23304 MultiBuffer::build_from_buffer(buffer, cx),
23305 Some(project.clone()),
23306 window,
23307 cx,
23308 )
23309 });
23310
23311 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
23312 let abs_path = project.read_with(cx, |project, cx| {
23313 project
23314 .absolute_path(&project_path, cx)
23315 .map(Arc::from)
23316 .unwrap()
23317 });
23318
23319 // assert we can add breakpoint on the first line
23320 editor.update_in(cx, |editor, window, cx| {
23321 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23322 editor.move_to_end(&MoveToEnd, window, cx);
23323 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23324 editor.move_up(&MoveUp, window, cx);
23325 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
23326 });
23327
23328 let breakpoints = editor.update(cx, |editor, cx| {
23329 editor
23330 .breakpoint_store()
23331 .as_ref()
23332 .unwrap()
23333 .read(cx)
23334 .all_source_breakpoints(cx)
23335 });
23336
23337 assert_eq!(1, breakpoints.len());
23338 assert_breakpoint(
23339 &breakpoints,
23340 &abs_path,
23341 vec![
23342 (0, Breakpoint::new_standard()),
23343 (2, Breakpoint::new_standard()),
23344 (3, Breakpoint::new_standard()),
23345 ],
23346 );
23347
23348 editor.update_in(cx, |editor, window, cx| {
23349 editor.move_to_beginning(&MoveToBeginning, window, cx);
23350 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23351 editor.move_to_end(&MoveToEnd, window, cx);
23352 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23353 // Disabling a breakpoint that doesn't exist should do nothing
23354 editor.move_up(&MoveUp, window, cx);
23355 editor.move_up(&MoveUp, window, cx);
23356 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23357 });
23358
23359 let breakpoints = editor.update(cx, |editor, cx| {
23360 editor
23361 .breakpoint_store()
23362 .as_ref()
23363 .unwrap()
23364 .read(cx)
23365 .all_source_breakpoints(cx)
23366 });
23367
23368 let disable_breakpoint = {
23369 let mut bp = Breakpoint::new_standard();
23370 bp.state = BreakpointState::Disabled;
23371 bp
23372 };
23373
23374 assert_eq!(1, breakpoints.len());
23375 assert_breakpoint(
23376 &breakpoints,
23377 &abs_path,
23378 vec![
23379 (0, disable_breakpoint.clone()),
23380 (2, Breakpoint::new_standard()),
23381 (3, disable_breakpoint.clone()),
23382 ],
23383 );
23384
23385 editor.update_in(cx, |editor, window, cx| {
23386 editor.move_to_beginning(&MoveToBeginning, window, cx);
23387 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23388 editor.move_to_end(&MoveToEnd, window, cx);
23389 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
23390 editor.move_up(&MoveUp, window, cx);
23391 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
23392 });
23393
23394 let breakpoints = editor.update(cx, |editor, cx| {
23395 editor
23396 .breakpoint_store()
23397 .as_ref()
23398 .unwrap()
23399 .read(cx)
23400 .all_source_breakpoints(cx)
23401 });
23402
23403 assert_eq!(1, breakpoints.len());
23404 assert_breakpoint(
23405 &breakpoints,
23406 &abs_path,
23407 vec![
23408 (0, Breakpoint::new_standard()),
23409 (2, disable_breakpoint),
23410 (3, Breakpoint::new_standard()),
23411 ],
23412 );
23413}
23414
23415#[gpui::test]
23416async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
23417 init_test(cx, |_| {});
23418 let capabilities = lsp::ServerCapabilities {
23419 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
23420 prepare_provider: Some(true),
23421 work_done_progress_options: Default::default(),
23422 })),
23423 ..Default::default()
23424 };
23425 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23426
23427 cx.set_state(indoc! {"
23428 struct Fˇoo {}
23429 "});
23430
23431 cx.update_editor(|editor, _, cx| {
23432 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23433 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23434 editor.highlight_background::<DocumentHighlightRead>(
23435 &[highlight_range],
23436 |theme| theme.colors().editor_document_highlight_read_background,
23437 cx,
23438 );
23439 });
23440
23441 let mut prepare_rename_handler = cx
23442 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
23443 move |_, _, _| async move {
23444 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
23445 start: lsp::Position {
23446 line: 0,
23447 character: 7,
23448 },
23449 end: lsp::Position {
23450 line: 0,
23451 character: 10,
23452 },
23453 })))
23454 },
23455 );
23456 let prepare_rename_task = cx
23457 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23458 .expect("Prepare rename was not started");
23459 prepare_rename_handler.next().await.unwrap();
23460 prepare_rename_task.await.expect("Prepare rename failed");
23461
23462 let mut rename_handler =
23463 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23464 let edit = lsp::TextEdit {
23465 range: lsp::Range {
23466 start: lsp::Position {
23467 line: 0,
23468 character: 7,
23469 },
23470 end: lsp::Position {
23471 line: 0,
23472 character: 10,
23473 },
23474 },
23475 new_text: "FooRenamed".to_string(),
23476 };
23477 Ok(Some(lsp::WorkspaceEdit::new(
23478 // Specify the same edit twice
23479 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
23480 )))
23481 });
23482 let rename_task = cx
23483 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23484 .expect("Confirm rename was not started");
23485 rename_handler.next().await.unwrap();
23486 rename_task.await.expect("Confirm rename failed");
23487 cx.run_until_parked();
23488
23489 // Despite two edits, only one is actually applied as those are identical
23490 cx.assert_editor_state(indoc! {"
23491 struct FooRenamedˇ {}
23492 "});
23493}
23494
23495#[gpui::test]
23496async fn test_rename_without_prepare(cx: &mut TestAppContext) {
23497 init_test(cx, |_| {});
23498 // These capabilities indicate that the server does not support prepare rename.
23499 let capabilities = lsp::ServerCapabilities {
23500 rename_provider: Some(lsp::OneOf::Left(true)),
23501 ..Default::default()
23502 };
23503 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
23504
23505 cx.set_state(indoc! {"
23506 struct Fˇoo {}
23507 "});
23508
23509 cx.update_editor(|editor, _window, cx| {
23510 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
23511 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
23512 editor.highlight_background::<DocumentHighlightRead>(
23513 &[highlight_range],
23514 |theme| theme.colors().editor_document_highlight_read_background,
23515 cx,
23516 );
23517 });
23518
23519 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
23520 .expect("Prepare rename was not started")
23521 .await
23522 .expect("Prepare rename failed");
23523
23524 let mut rename_handler =
23525 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
23526 let edit = lsp::TextEdit {
23527 range: lsp::Range {
23528 start: lsp::Position {
23529 line: 0,
23530 character: 7,
23531 },
23532 end: lsp::Position {
23533 line: 0,
23534 character: 10,
23535 },
23536 },
23537 new_text: "FooRenamed".to_string(),
23538 };
23539 Ok(Some(lsp::WorkspaceEdit::new(
23540 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
23541 )))
23542 });
23543 let rename_task = cx
23544 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
23545 .expect("Confirm rename was not started");
23546 rename_handler.next().await.unwrap();
23547 rename_task.await.expect("Confirm rename failed");
23548 cx.run_until_parked();
23549
23550 // Correct range is renamed, as `surrounding_word` is used to find it.
23551 cx.assert_editor_state(indoc! {"
23552 struct FooRenamedˇ {}
23553 "});
23554}
23555
23556#[gpui::test]
23557async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
23558 init_test(cx, |_| {});
23559 let mut cx = EditorTestContext::new(cx).await;
23560
23561 let language = Arc::new(
23562 Language::new(
23563 LanguageConfig::default(),
23564 Some(tree_sitter_html::LANGUAGE.into()),
23565 )
23566 .with_brackets_query(
23567 r#"
23568 ("<" @open "/>" @close)
23569 ("</" @open ">" @close)
23570 ("<" @open ">" @close)
23571 ("\"" @open "\"" @close)
23572 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
23573 "#,
23574 )
23575 .unwrap(),
23576 );
23577 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
23578
23579 cx.set_state(indoc! {"
23580 <span>ˇ</span>
23581 "});
23582 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23583 cx.assert_editor_state(indoc! {"
23584 <span>
23585 ˇ
23586 </span>
23587 "});
23588
23589 cx.set_state(indoc! {"
23590 <span><span></span>ˇ</span>
23591 "});
23592 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23593 cx.assert_editor_state(indoc! {"
23594 <span><span></span>
23595 ˇ</span>
23596 "});
23597
23598 cx.set_state(indoc! {"
23599 <span>ˇ
23600 </span>
23601 "});
23602 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
23603 cx.assert_editor_state(indoc! {"
23604 <span>
23605 ˇ
23606 </span>
23607 "});
23608}
23609
23610#[gpui::test(iterations = 10)]
23611async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
23612 init_test(cx, |_| {});
23613
23614 let fs = FakeFs::new(cx.executor());
23615 fs.insert_tree(
23616 path!("/dir"),
23617 json!({
23618 "a.ts": "a",
23619 }),
23620 )
23621 .await;
23622
23623 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
23624 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
23625 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
23626
23627 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
23628 language_registry.add(Arc::new(Language::new(
23629 LanguageConfig {
23630 name: "TypeScript".into(),
23631 matcher: LanguageMatcher {
23632 path_suffixes: vec!["ts".to_string()],
23633 ..Default::default()
23634 },
23635 ..Default::default()
23636 },
23637 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
23638 )));
23639 let mut fake_language_servers = language_registry.register_fake_lsp(
23640 "TypeScript",
23641 FakeLspAdapter {
23642 capabilities: lsp::ServerCapabilities {
23643 code_lens_provider: Some(lsp::CodeLensOptions {
23644 resolve_provider: Some(true),
23645 }),
23646 execute_command_provider: Some(lsp::ExecuteCommandOptions {
23647 commands: vec!["_the/command".to_string()],
23648 ..lsp::ExecuteCommandOptions::default()
23649 }),
23650 ..lsp::ServerCapabilities::default()
23651 },
23652 ..FakeLspAdapter::default()
23653 },
23654 );
23655
23656 let editor = workspace
23657 .update(cx, |workspace, window, cx| {
23658 workspace.open_abs_path(
23659 PathBuf::from(path!("/dir/a.ts")),
23660 OpenOptions::default(),
23661 window,
23662 cx,
23663 )
23664 })
23665 .unwrap()
23666 .await
23667 .unwrap()
23668 .downcast::<Editor>()
23669 .unwrap();
23670 cx.executor().run_until_parked();
23671
23672 let fake_server = fake_language_servers.next().await.unwrap();
23673
23674 let buffer = editor.update(cx, |editor, cx| {
23675 editor
23676 .buffer()
23677 .read(cx)
23678 .as_singleton()
23679 .expect("have opened a single file by path")
23680 });
23681
23682 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
23683 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
23684 drop(buffer_snapshot);
23685 let actions = cx
23686 .update_window(*workspace, |_, window, cx| {
23687 project.code_actions(&buffer, anchor..anchor, window, cx)
23688 })
23689 .unwrap();
23690
23691 fake_server
23692 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23693 Ok(Some(vec![
23694 lsp::CodeLens {
23695 range: lsp::Range::default(),
23696 command: Some(lsp::Command {
23697 title: "Code lens command".to_owned(),
23698 command: "_the/command".to_owned(),
23699 arguments: None,
23700 }),
23701 data: None,
23702 },
23703 lsp::CodeLens {
23704 range: lsp::Range::default(),
23705 command: Some(lsp::Command {
23706 title: "Command not in capabilities".to_owned(),
23707 command: "not in capabilities".to_owned(),
23708 arguments: None,
23709 }),
23710 data: None,
23711 },
23712 lsp::CodeLens {
23713 range: lsp::Range {
23714 start: lsp::Position {
23715 line: 1,
23716 character: 1,
23717 },
23718 end: lsp::Position {
23719 line: 1,
23720 character: 1,
23721 },
23722 },
23723 command: Some(lsp::Command {
23724 title: "Command not in range".to_owned(),
23725 command: "_the/command".to_owned(),
23726 arguments: None,
23727 }),
23728 data: None,
23729 },
23730 ]))
23731 })
23732 .next()
23733 .await;
23734
23735 let actions = actions.await.unwrap();
23736 assert_eq!(
23737 actions.len(),
23738 1,
23739 "Should have only one valid action for the 0..0 range, got: {actions:#?}"
23740 );
23741 let action = actions[0].clone();
23742 let apply = project.update(cx, |project, cx| {
23743 project.apply_code_action(buffer.clone(), action, true, cx)
23744 });
23745
23746 // Resolving the code action does not populate its edits. In absence of
23747 // edits, we must execute the given command.
23748 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
23749 |mut lens, _| async move {
23750 let lens_command = lens.command.as_mut().expect("should have a command");
23751 assert_eq!(lens_command.title, "Code lens command");
23752 lens_command.arguments = Some(vec![json!("the-argument")]);
23753 Ok(lens)
23754 },
23755 );
23756
23757 // While executing the command, the language server sends the editor
23758 // a `workspaceEdit` request.
23759 fake_server
23760 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
23761 let fake = fake_server.clone();
23762 move |params, _| {
23763 assert_eq!(params.command, "_the/command");
23764 let fake = fake.clone();
23765 async move {
23766 fake.server
23767 .request::<lsp::request::ApplyWorkspaceEdit>(
23768 lsp::ApplyWorkspaceEditParams {
23769 label: None,
23770 edit: lsp::WorkspaceEdit {
23771 changes: Some(
23772 [(
23773 lsp::Uri::from_file_path(path!("/dir/a.ts")).unwrap(),
23774 vec![lsp::TextEdit {
23775 range: lsp::Range::new(
23776 lsp::Position::new(0, 0),
23777 lsp::Position::new(0, 0),
23778 ),
23779 new_text: "X".into(),
23780 }],
23781 )]
23782 .into_iter()
23783 .collect(),
23784 ),
23785 ..lsp::WorkspaceEdit::default()
23786 },
23787 },
23788 )
23789 .await
23790 .into_response()
23791 .unwrap();
23792 Ok(Some(json!(null)))
23793 }
23794 }
23795 })
23796 .next()
23797 .await;
23798
23799 // Applying the code lens command returns a project transaction containing the edits
23800 // sent by the language server in its `workspaceEdit` request.
23801 let transaction = apply.await.unwrap();
23802 assert!(transaction.0.contains_key(&buffer));
23803 buffer.update(cx, |buffer, cx| {
23804 assert_eq!(buffer.text(), "Xa");
23805 buffer.undo(cx);
23806 assert_eq!(buffer.text(), "a");
23807 });
23808
23809 let actions_after_edits = cx
23810 .update_window(*workspace, |_, window, cx| {
23811 project.code_actions(&buffer, anchor..anchor, window, cx)
23812 })
23813 .unwrap()
23814 .await
23815 .unwrap();
23816 assert_eq!(
23817 actions, actions_after_edits,
23818 "For the same selection, same code lens actions should be returned"
23819 );
23820
23821 let _responses =
23822 fake_server.set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
23823 panic!("No more code lens requests are expected");
23824 });
23825 editor.update_in(cx, |editor, window, cx| {
23826 editor.select_all(&SelectAll, window, cx);
23827 });
23828 cx.executor().run_until_parked();
23829 let new_actions = cx
23830 .update_window(*workspace, |_, window, cx| {
23831 project.code_actions(&buffer, anchor..anchor, window, cx)
23832 })
23833 .unwrap()
23834 .await
23835 .unwrap();
23836 assert_eq!(
23837 actions, new_actions,
23838 "Code lens are queried for the same range and should get the same set back, but without additional LSP queries now"
23839 );
23840}
23841
23842#[gpui::test]
23843async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
23844 init_test(cx, |_| {});
23845
23846 let fs = FakeFs::new(cx.executor());
23847 let main_text = r#"fn main() {
23848println!("1");
23849println!("2");
23850println!("3");
23851println!("4");
23852println!("5");
23853}"#;
23854 let lib_text = "mod foo {}";
23855 fs.insert_tree(
23856 path!("/a"),
23857 json!({
23858 "lib.rs": lib_text,
23859 "main.rs": main_text,
23860 }),
23861 )
23862 .await;
23863
23864 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
23865 let (workspace, cx) =
23866 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
23867 let worktree_id = workspace.update(cx, |workspace, cx| {
23868 workspace.project().update(cx, |project, cx| {
23869 project.worktrees(cx).next().unwrap().read(cx).id()
23870 })
23871 });
23872
23873 let expected_ranges = vec![
23874 Point::new(0, 0)..Point::new(0, 0),
23875 Point::new(1, 0)..Point::new(1, 1),
23876 Point::new(2, 0)..Point::new(2, 2),
23877 Point::new(3, 0)..Point::new(3, 3),
23878 ];
23879
23880 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
23881 let editor_1 = workspace
23882 .update_in(cx, |workspace, window, cx| {
23883 workspace.open_path(
23884 (worktree_id, rel_path("main.rs")),
23885 Some(pane_1.downgrade()),
23886 true,
23887 window,
23888 cx,
23889 )
23890 })
23891 .unwrap()
23892 .await
23893 .downcast::<Editor>()
23894 .unwrap();
23895 pane_1.update(cx, |pane, cx| {
23896 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23897 open_editor.update(cx, |editor, cx| {
23898 assert_eq!(
23899 editor.display_text(cx),
23900 main_text,
23901 "Original main.rs text on initial open",
23902 );
23903 assert_eq!(
23904 editor
23905 .selections
23906 .all::<Point>(&editor.display_snapshot(cx))
23907 .into_iter()
23908 .map(|s| s.range())
23909 .collect::<Vec<_>>(),
23910 vec![Point::zero()..Point::zero()],
23911 "Default selections on initial open",
23912 );
23913 })
23914 });
23915 editor_1.update_in(cx, |editor, window, cx| {
23916 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
23917 s.select_ranges(expected_ranges.clone());
23918 });
23919 });
23920
23921 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
23922 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
23923 });
23924 let editor_2 = workspace
23925 .update_in(cx, |workspace, window, cx| {
23926 workspace.open_path(
23927 (worktree_id, rel_path("main.rs")),
23928 Some(pane_2.downgrade()),
23929 true,
23930 window,
23931 cx,
23932 )
23933 })
23934 .unwrap()
23935 .await
23936 .downcast::<Editor>()
23937 .unwrap();
23938 pane_2.update(cx, |pane, cx| {
23939 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
23940 open_editor.update(cx, |editor, cx| {
23941 assert_eq!(
23942 editor.display_text(cx),
23943 main_text,
23944 "Original main.rs text on initial open in another panel",
23945 );
23946 assert_eq!(
23947 editor
23948 .selections
23949 .all::<Point>(&editor.display_snapshot(cx))
23950 .into_iter()
23951 .map(|s| s.range())
23952 .collect::<Vec<_>>(),
23953 vec![Point::zero()..Point::zero()],
23954 "Default selections on initial open in another panel",
23955 );
23956 })
23957 });
23958
23959 editor_2.update_in(cx, |editor, window, cx| {
23960 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
23961 });
23962
23963 let _other_editor_1 = workspace
23964 .update_in(cx, |workspace, window, cx| {
23965 workspace.open_path(
23966 (worktree_id, rel_path("lib.rs")),
23967 Some(pane_1.downgrade()),
23968 true,
23969 window,
23970 cx,
23971 )
23972 })
23973 .unwrap()
23974 .await
23975 .downcast::<Editor>()
23976 .unwrap();
23977 pane_1
23978 .update_in(cx, |pane, window, cx| {
23979 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
23980 })
23981 .await
23982 .unwrap();
23983 drop(editor_1);
23984 pane_1.update(cx, |pane, cx| {
23985 pane.active_item()
23986 .unwrap()
23987 .downcast::<Editor>()
23988 .unwrap()
23989 .update(cx, |editor, cx| {
23990 assert_eq!(
23991 editor.display_text(cx),
23992 lib_text,
23993 "Other file should be open and active",
23994 );
23995 });
23996 assert_eq!(pane.items().count(), 1, "No other editors should be open");
23997 });
23998
23999 let _other_editor_2 = workspace
24000 .update_in(cx, |workspace, window, cx| {
24001 workspace.open_path(
24002 (worktree_id, rel_path("lib.rs")),
24003 Some(pane_2.downgrade()),
24004 true,
24005 window,
24006 cx,
24007 )
24008 })
24009 .unwrap()
24010 .await
24011 .downcast::<Editor>()
24012 .unwrap();
24013 pane_2
24014 .update_in(cx, |pane, window, cx| {
24015 pane.close_other_items(&CloseOtherItems::default(), None, window, cx)
24016 })
24017 .await
24018 .unwrap();
24019 drop(editor_2);
24020 pane_2.update(cx, |pane, cx| {
24021 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24022 open_editor.update(cx, |editor, cx| {
24023 assert_eq!(
24024 editor.display_text(cx),
24025 lib_text,
24026 "Other file should be open and active in another panel too",
24027 );
24028 });
24029 assert_eq!(
24030 pane.items().count(),
24031 1,
24032 "No other editors should be open in another pane",
24033 );
24034 });
24035
24036 let _editor_1_reopened = workspace
24037 .update_in(cx, |workspace, window, cx| {
24038 workspace.open_path(
24039 (worktree_id, rel_path("main.rs")),
24040 Some(pane_1.downgrade()),
24041 true,
24042 window,
24043 cx,
24044 )
24045 })
24046 .unwrap()
24047 .await
24048 .downcast::<Editor>()
24049 .unwrap();
24050 let _editor_2_reopened = workspace
24051 .update_in(cx, |workspace, window, cx| {
24052 workspace.open_path(
24053 (worktree_id, rel_path("main.rs")),
24054 Some(pane_2.downgrade()),
24055 true,
24056 window,
24057 cx,
24058 )
24059 })
24060 .unwrap()
24061 .await
24062 .downcast::<Editor>()
24063 .unwrap();
24064 pane_1.update(cx, |pane, cx| {
24065 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24066 open_editor.update(cx, |editor, cx| {
24067 assert_eq!(
24068 editor.display_text(cx),
24069 main_text,
24070 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
24071 );
24072 assert_eq!(
24073 editor
24074 .selections
24075 .all::<Point>(&editor.display_snapshot(cx))
24076 .into_iter()
24077 .map(|s| s.range())
24078 .collect::<Vec<_>>(),
24079 expected_ranges,
24080 "Previous editor in the 1st panel had selections and should get them restored on reopen",
24081 );
24082 })
24083 });
24084 pane_2.update(cx, |pane, cx| {
24085 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24086 open_editor.update(cx, |editor, cx| {
24087 assert_eq!(
24088 editor.display_text(cx),
24089 r#"fn main() {
24090⋯rintln!("1");
24091⋯intln!("2");
24092⋯ntln!("3");
24093println!("4");
24094println!("5");
24095}"#,
24096 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
24097 );
24098 assert_eq!(
24099 editor
24100 .selections
24101 .all::<Point>(&editor.display_snapshot(cx))
24102 .into_iter()
24103 .map(|s| s.range())
24104 .collect::<Vec<_>>(),
24105 vec![Point::zero()..Point::zero()],
24106 "Previous editor in the 2nd pane had no selections changed hence should restore none",
24107 );
24108 })
24109 });
24110}
24111
24112#[gpui::test]
24113async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
24114 init_test(cx, |_| {});
24115
24116 let fs = FakeFs::new(cx.executor());
24117 let main_text = r#"fn main() {
24118println!("1");
24119println!("2");
24120println!("3");
24121println!("4");
24122println!("5");
24123}"#;
24124 let lib_text = "mod foo {}";
24125 fs.insert_tree(
24126 path!("/a"),
24127 json!({
24128 "lib.rs": lib_text,
24129 "main.rs": main_text,
24130 }),
24131 )
24132 .await;
24133
24134 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
24135 let (workspace, cx) =
24136 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24137 let worktree_id = workspace.update(cx, |workspace, cx| {
24138 workspace.project().update(cx, |project, cx| {
24139 project.worktrees(cx).next().unwrap().read(cx).id()
24140 })
24141 });
24142
24143 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24144 let editor = workspace
24145 .update_in(cx, |workspace, window, cx| {
24146 workspace.open_path(
24147 (worktree_id, rel_path("main.rs")),
24148 Some(pane.downgrade()),
24149 true,
24150 window,
24151 cx,
24152 )
24153 })
24154 .unwrap()
24155 .await
24156 .downcast::<Editor>()
24157 .unwrap();
24158 pane.update(cx, |pane, cx| {
24159 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24160 open_editor.update(cx, |editor, cx| {
24161 assert_eq!(
24162 editor.display_text(cx),
24163 main_text,
24164 "Original main.rs text on initial open",
24165 );
24166 })
24167 });
24168 editor.update_in(cx, |editor, window, cx| {
24169 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
24170 });
24171
24172 cx.update_global(|store: &mut SettingsStore, cx| {
24173 store.update_user_settings(cx, |s| {
24174 s.workspace.restore_on_file_reopen = Some(false);
24175 });
24176 });
24177 editor.update_in(cx, |editor, window, cx| {
24178 editor.fold_ranges(
24179 vec![
24180 Point::new(1, 0)..Point::new(1, 1),
24181 Point::new(2, 0)..Point::new(2, 2),
24182 Point::new(3, 0)..Point::new(3, 3),
24183 ],
24184 false,
24185 window,
24186 cx,
24187 );
24188 });
24189 pane.update_in(cx, |pane, window, cx| {
24190 pane.close_all_items(&CloseAllItems::default(), window, cx)
24191 })
24192 .await
24193 .unwrap();
24194 pane.update(cx, |pane, _| {
24195 assert!(pane.active_item().is_none());
24196 });
24197 cx.update_global(|store: &mut SettingsStore, cx| {
24198 store.update_user_settings(cx, |s| {
24199 s.workspace.restore_on_file_reopen = Some(true);
24200 });
24201 });
24202
24203 let _editor_reopened = workspace
24204 .update_in(cx, |workspace, window, cx| {
24205 workspace.open_path(
24206 (worktree_id, rel_path("main.rs")),
24207 Some(pane.downgrade()),
24208 true,
24209 window,
24210 cx,
24211 )
24212 })
24213 .unwrap()
24214 .await
24215 .downcast::<Editor>()
24216 .unwrap();
24217 pane.update(cx, |pane, cx| {
24218 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24219 open_editor.update(cx, |editor, cx| {
24220 assert_eq!(
24221 editor.display_text(cx),
24222 main_text,
24223 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
24224 );
24225 })
24226 });
24227}
24228
24229#[gpui::test]
24230async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
24231 struct EmptyModalView {
24232 focus_handle: gpui::FocusHandle,
24233 }
24234 impl EventEmitter<DismissEvent> for EmptyModalView {}
24235 impl Render for EmptyModalView {
24236 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
24237 div()
24238 }
24239 }
24240 impl Focusable for EmptyModalView {
24241 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
24242 self.focus_handle.clone()
24243 }
24244 }
24245 impl workspace::ModalView for EmptyModalView {}
24246 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
24247 EmptyModalView {
24248 focus_handle: cx.focus_handle(),
24249 }
24250 }
24251
24252 init_test(cx, |_| {});
24253
24254 let fs = FakeFs::new(cx.executor());
24255 let project = Project::test(fs, [], cx).await;
24256 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24257 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
24258 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
24259 let editor = cx.new_window_entity(|window, cx| {
24260 Editor::new(
24261 EditorMode::full(),
24262 buffer,
24263 Some(project.clone()),
24264 window,
24265 cx,
24266 )
24267 });
24268 workspace
24269 .update(cx, |workspace, window, cx| {
24270 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
24271 })
24272 .unwrap();
24273 editor.update_in(cx, |editor, window, cx| {
24274 editor.open_context_menu(&OpenContextMenu, window, cx);
24275 assert!(editor.mouse_context_menu.is_some());
24276 });
24277 workspace
24278 .update(cx, |workspace, window, cx| {
24279 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
24280 })
24281 .unwrap();
24282 cx.read(|cx| {
24283 assert!(editor.read(cx).mouse_context_menu.is_none());
24284 });
24285}
24286
24287fn set_linked_edit_ranges(
24288 opening: (Point, Point),
24289 closing: (Point, Point),
24290 editor: &mut Editor,
24291 cx: &mut Context<Editor>,
24292) {
24293 let Some((buffer, _)) = editor
24294 .buffer
24295 .read(cx)
24296 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
24297 else {
24298 panic!("Failed to get buffer for selection position");
24299 };
24300 let buffer = buffer.read(cx);
24301 let buffer_id = buffer.remote_id();
24302 let opening_range = buffer.anchor_before(opening.0)..buffer.anchor_after(opening.1);
24303 let closing_range = buffer.anchor_before(closing.0)..buffer.anchor_after(closing.1);
24304 let mut linked_ranges = HashMap::default();
24305 linked_ranges.insert(buffer_id, vec![(opening_range, vec![closing_range])]);
24306 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
24307}
24308
24309#[gpui::test]
24310async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
24311 init_test(cx, |_| {});
24312
24313 let fs = FakeFs::new(cx.executor());
24314 fs.insert_file(path!("/file.html"), Default::default())
24315 .await;
24316
24317 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
24318
24319 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24320 let html_language = Arc::new(Language::new(
24321 LanguageConfig {
24322 name: "HTML".into(),
24323 matcher: LanguageMatcher {
24324 path_suffixes: vec!["html".to_string()],
24325 ..LanguageMatcher::default()
24326 },
24327 brackets: BracketPairConfig {
24328 pairs: vec![BracketPair {
24329 start: "<".into(),
24330 end: ">".into(),
24331 close: true,
24332 ..Default::default()
24333 }],
24334 ..Default::default()
24335 },
24336 ..Default::default()
24337 },
24338 Some(tree_sitter_html::LANGUAGE.into()),
24339 ));
24340 language_registry.add(html_language);
24341 let mut fake_servers = language_registry.register_fake_lsp(
24342 "HTML",
24343 FakeLspAdapter {
24344 capabilities: lsp::ServerCapabilities {
24345 completion_provider: Some(lsp::CompletionOptions {
24346 resolve_provider: Some(true),
24347 ..Default::default()
24348 }),
24349 ..Default::default()
24350 },
24351 ..Default::default()
24352 },
24353 );
24354
24355 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
24356 let cx = &mut VisualTestContext::from_window(*workspace, cx);
24357
24358 let worktree_id = workspace
24359 .update(cx, |workspace, _window, cx| {
24360 workspace.project().update(cx, |project, cx| {
24361 project.worktrees(cx).next().unwrap().read(cx).id()
24362 })
24363 })
24364 .unwrap();
24365 project
24366 .update(cx, |project, cx| {
24367 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
24368 })
24369 .await
24370 .unwrap();
24371 let editor = workspace
24372 .update(cx, |workspace, window, cx| {
24373 workspace.open_path((worktree_id, rel_path("file.html")), None, true, window, cx)
24374 })
24375 .unwrap()
24376 .await
24377 .unwrap()
24378 .downcast::<Editor>()
24379 .unwrap();
24380
24381 let fake_server = fake_servers.next().await.unwrap();
24382 editor.update_in(cx, |editor, window, cx| {
24383 editor.set_text("<ad></ad>", window, cx);
24384 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |selections| {
24385 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
24386 });
24387 set_linked_edit_ranges(
24388 (Point::new(0, 1), Point::new(0, 3)),
24389 (Point::new(0, 6), Point::new(0, 8)),
24390 editor,
24391 cx,
24392 );
24393 });
24394 let mut completion_handle =
24395 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
24396 Ok(Some(lsp::CompletionResponse::Array(vec![
24397 lsp::CompletionItem {
24398 label: "head".to_string(),
24399 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
24400 lsp::InsertReplaceEdit {
24401 new_text: "head".to_string(),
24402 insert: lsp::Range::new(
24403 lsp::Position::new(0, 1),
24404 lsp::Position::new(0, 3),
24405 ),
24406 replace: lsp::Range::new(
24407 lsp::Position::new(0, 1),
24408 lsp::Position::new(0, 3),
24409 ),
24410 },
24411 )),
24412 ..Default::default()
24413 },
24414 ])))
24415 });
24416 editor.update_in(cx, |editor, window, cx| {
24417 editor.show_completions(&ShowCompletions, window, cx);
24418 });
24419 cx.run_until_parked();
24420 completion_handle.next().await.unwrap();
24421 editor.update(cx, |editor, _| {
24422 assert!(
24423 editor.context_menu_visible(),
24424 "Completion menu should be visible"
24425 );
24426 });
24427 editor.update_in(cx, |editor, window, cx| {
24428 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
24429 });
24430 cx.executor().run_until_parked();
24431 editor.update(cx, |editor, cx| {
24432 assert_eq!(editor.text(cx), "<head></head>");
24433 });
24434}
24435
24436#[gpui::test]
24437async fn test_linked_edits_on_typing_punctuation(cx: &mut TestAppContext) {
24438 init_test(cx, |_| {});
24439
24440 let mut cx = EditorTestContext::new(cx).await;
24441 let language = Arc::new(Language::new(
24442 LanguageConfig {
24443 name: "TSX".into(),
24444 matcher: LanguageMatcher {
24445 path_suffixes: vec!["tsx".to_string()],
24446 ..LanguageMatcher::default()
24447 },
24448 brackets: BracketPairConfig {
24449 pairs: vec![BracketPair {
24450 start: "<".into(),
24451 end: ">".into(),
24452 close: true,
24453 ..Default::default()
24454 }],
24455 ..Default::default()
24456 },
24457 linked_edit_characters: HashSet::from_iter(['.']),
24458 ..Default::default()
24459 },
24460 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
24461 ));
24462 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24463
24464 // Test typing > does not extend linked pair
24465 cx.set_state("<divˇ<div></div>");
24466 cx.update_editor(|editor, _, cx| {
24467 set_linked_edit_ranges(
24468 (Point::new(0, 1), Point::new(0, 4)),
24469 (Point::new(0, 11), Point::new(0, 14)),
24470 editor,
24471 cx,
24472 );
24473 });
24474 cx.update_editor(|editor, window, cx| {
24475 editor.handle_input(">", window, cx);
24476 });
24477 cx.assert_editor_state("<div>ˇ<div></div>");
24478
24479 // Test typing . do extend linked pair
24480 cx.set_state("<Animatedˇ></Animated>");
24481 cx.update_editor(|editor, _, cx| {
24482 set_linked_edit_ranges(
24483 (Point::new(0, 1), Point::new(0, 9)),
24484 (Point::new(0, 12), Point::new(0, 20)),
24485 editor,
24486 cx,
24487 );
24488 });
24489 cx.update_editor(|editor, window, cx| {
24490 editor.handle_input(".", window, cx);
24491 });
24492 cx.assert_editor_state("<Animated.ˇ></Animated.>");
24493 cx.update_editor(|editor, _, cx| {
24494 set_linked_edit_ranges(
24495 (Point::new(0, 1), Point::new(0, 10)),
24496 (Point::new(0, 13), Point::new(0, 21)),
24497 editor,
24498 cx,
24499 );
24500 });
24501 cx.update_editor(|editor, window, cx| {
24502 editor.handle_input("V", window, cx);
24503 });
24504 cx.assert_editor_state("<Animated.Vˇ></Animated.V>");
24505}
24506
24507#[gpui::test]
24508async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
24509 init_test(cx, |_| {});
24510
24511 let fs = FakeFs::new(cx.executor());
24512 fs.insert_tree(
24513 path!("/root"),
24514 json!({
24515 "a": {
24516 "main.rs": "fn main() {}",
24517 },
24518 "foo": {
24519 "bar": {
24520 "external_file.rs": "pub mod external {}",
24521 }
24522 }
24523 }),
24524 )
24525 .await;
24526
24527 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
24528 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
24529 language_registry.add(rust_lang());
24530 let _fake_servers = language_registry.register_fake_lsp(
24531 "Rust",
24532 FakeLspAdapter {
24533 ..FakeLspAdapter::default()
24534 },
24535 );
24536 let (workspace, cx) =
24537 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
24538 let worktree_id = workspace.update(cx, |workspace, cx| {
24539 workspace.project().update(cx, |project, cx| {
24540 project.worktrees(cx).next().unwrap().read(cx).id()
24541 })
24542 });
24543
24544 let assert_language_servers_count =
24545 |expected: usize, context: &str, cx: &mut VisualTestContext| {
24546 project.update(cx, |project, cx| {
24547 let current = project
24548 .lsp_store()
24549 .read(cx)
24550 .as_local()
24551 .unwrap()
24552 .language_servers
24553 .len();
24554 assert_eq!(expected, current, "{context}");
24555 });
24556 };
24557
24558 assert_language_servers_count(
24559 0,
24560 "No servers should be running before any file is open",
24561 cx,
24562 );
24563 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
24564 let main_editor = workspace
24565 .update_in(cx, |workspace, window, cx| {
24566 workspace.open_path(
24567 (worktree_id, rel_path("main.rs")),
24568 Some(pane.downgrade()),
24569 true,
24570 window,
24571 cx,
24572 )
24573 })
24574 .unwrap()
24575 .await
24576 .downcast::<Editor>()
24577 .unwrap();
24578 pane.update(cx, |pane, cx| {
24579 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24580 open_editor.update(cx, |editor, cx| {
24581 assert_eq!(
24582 editor.display_text(cx),
24583 "fn main() {}",
24584 "Original main.rs text on initial open",
24585 );
24586 });
24587 assert_eq!(open_editor, main_editor);
24588 });
24589 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
24590
24591 let external_editor = workspace
24592 .update_in(cx, |workspace, window, cx| {
24593 workspace.open_abs_path(
24594 PathBuf::from("/root/foo/bar/external_file.rs"),
24595 OpenOptions::default(),
24596 window,
24597 cx,
24598 )
24599 })
24600 .await
24601 .expect("opening external file")
24602 .downcast::<Editor>()
24603 .expect("downcasted external file's open element to editor");
24604 pane.update(cx, |pane, cx| {
24605 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24606 open_editor.update(cx, |editor, cx| {
24607 assert_eq!(
24608 editor.display_text(cx),
24609 "pub mod external {}",
24610 "External file is open now",
24611 );
24612 });
24613 assert_eq!(open_editor, external_editor);
24614 });
24615 assert_language_servers_count(
24616 1,
24617 "Second, external, *.rs file should join the existing server",
24618 cx,
24619 );
24620
24621 pane.update_in(cx, |pane, window, cx| {
24622 pane.close_active_item(&CloseActiveItem::default(), window, cx)
24623 })
24624 .await
24625 .unwrap();
24626 pane.update_in(cx, |pane, window, cx| {
24627 pane.navigate_backward(&Default::default(), window, cx);
24628 });
24629 cx.run_until_parked();
24630 pane.update(cx, |pane, cx| {
24631 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
24632 open_editor.update(cx, |editor, cx| {
24633 assert_eq!(
24634 editor.display_text(cx),
24635 "pub mod external {}",
24636 "External file is open now",
24637 );
24638 });
24639 });
24640 assert_language_servers_count(
24641 1,
24642 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
24643 cx,
24644 );
24645
24646 cx.update(|_, cx| {
24647 workspace::reload(cx);
24648 });
24649 assert_language_servers_count(
24650 1,
24651 "After reloading the worktree with local and external files opened, only one project should be started",
24652 cx,
24653 );
24654}
24655
24656#[gpui::test]
24657async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
24658 init_test(cx, |_| {});
24659
24660 let mut cx = EditorTestContext::new(cx).await;
24661 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24662 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24663
24664 // test cursor move to start of each line on tab
24665 // for `if`, `elif`, `else`, `while`, `with` and `for`
24666 cx.set_state(indoc! {"
24667 def main():
24668 ˇ for item in items:
24669 ˇ while item.active:
24670 ˇ if item.value > 10:
24671 ˇ continue
24672 ˇ elif item.value < 0:
24673 ˇ break
24674 ˇ else:
24675 ˇ with item.context() as ctx:
24676 ˇ yield count
24677 ˇ else:
24678 ˇ log('while else')
24679 ˇ else:
24680 ˇ log('for else')
24681 "});
24682 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24683 cx.assert_editor_state(indoc! {"
24684 def main():
24685 ˇfor item in items:
24686 ˇwhile item.active:
24687 ˇif item.value > 10:
24688 ˇcontinue
24689 ˇelif item.value < 0:
24690 ˇbreak
24691 ˇelse:
24692 ˇwith item.context() as ctx:
24693 ˇyield count
24694 ˇelse:
24695 ˇlog('while else')
24696 ˇelse:
24697 ˇlog('for else')
24698 "});
24699 // test relative indent is preserved when tab
24700 // for `if`, `elif`, `else`, `while`, `with` and `for`
24701 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24702 cx.assert_editor_state(indoc! {"
24703 def main():
24704 ˇfor item in items:
24705 ˇwhile item.active:
24706 ˇif item.value > 10:
24707 ˇcontinue
24708 ˇelif item.value < 0:
24709 ˇbreak
24710 ˇelse:
24711 ˇwith item.context() as ctx:
24712 ˇyield count
24713 ˇelse:
24714 ˇlog('while else')
24715 ˇelse:
24716 ˇlog('for else')
24717 "});
24718
24719 // test cursor move to start of each line on tab
24720 // for `try`, `except`, `else`, `finally`, `match` and `def`
24721 cx.set_state(indoc! {"
24722 def main():
24723 ˇ try:
24724 ˇ fetch()
24725 ˇ except ValueError:
24726 ˇ handle_error()
24727 ˇ else:
24728 ˇ match value:
24729 ˇ case _:
24730 ˇ finally:
24731 ˇ def status():
24732 ˇ return 0
24733 "});
24734 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24735 cx.assert_editor_state(indoc! {"
24736 def main():
24737 ˇtry:
24738 ˇfetch()
24739 ˇexcept ValueError:
24740 ˇhandle_error()
24741 ˇelse:
24742 ˇmatch value:
24743 ˇcase _:
24744 ˇfinally:
24745 ˇdef status():
24746 ˇreturn 0
24747 "});
24748 // test relative indent is preserved when tab
24749 // for `try`, `except`, `else`, `finally`, `match` and `def`
24750 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
24751 cx.assert_editor_state(indoc! {"
24752 def main():
24753 ˇtry:
24754 ˇfetch()
24755 ˇexcept ValueError:
24756 ˇhandle_error()
24757 ˇelse:
24758 ˇmatch value:
24759 ˇcase _:
24760 ˇfinally:
24761 ˇdef status():
24762 ˇreturn 0
24763 "});
24764}
24765
24766#[gpui::test]
24767async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
24768 init_test(cx, |_| {});
24769
24770 let mut cx = EditorTestContext::new(cx).await;
24771 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
24772 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
24773
24774 // test `else` auto outdents when typed inside `if` block
24775 cx.set_state(indoc! {"
24776 def main():
24777 if i == 2:
24778 return
24779 ˇ
24780 "});
24781 cx.update_editor(|editor, window, cx| {
24782 editor.handle_input("else:", window, cx);
24783 });
24784 cx.assert_editor_state(indoc! {"
24785 def main():
24786 if i == 2:
24787 return
24788 else:ˇ
24789 "});
24790
24791 // test `except` auto outdents when typed inside `try` block
24792 cx.set_state(indoc! {"
24793 def main():
24794 try:
24795 i = 2
24796 ˇ
24797 "});
24798 cx.update_editor(|editor, window, cx| {
24799 editor.handle_input("except:", window, cx);
24800 });
24801 cx.assert_editor_state(indoc! {"
24802 def main():
24803 try:
24804 i = 2
24805 except:ˇ
24806 "});
24807
24808 // test `else` auto outdents when typed inside `except` block
24809 cx.set_state(indoc! {"
24810 def main():
24811 try:
24812 i = 2
24813 except:
24814 j = 2
24815 ˇ
24816 "});
24817 cx.update_editor(|editor, window, cx| {
24818 editor.handle_input("else:", window, cx);
24819 });
24820 cx.assert_editor_state(indoc! {"
24821 def main():
24822 try:
24823 i = 2
24824 except:
24825 j = 2
24826 else:ˇ
24827 "});
24828
24829 // test `finally` auto outdents when typed inside `else` block
24830 cx.set_state(indoc! {"
24831 def main():
24832 try:
24833 i = 2
24834 except:
24835 j = 2
24836 else:
24837 k = 2
24838 ˇ
24839 "});
24840 cx.update_editor(|editor, window, cx| {
24841 editor.handle_input("finally:", window, cx);
24842 });
24843 cx.assert_editor_state(indoc! {"
24844 def main():
24845 try:
24846 i = 2
24847 except:
24848 j = 2
24849 else:
24850 k = 2
24851 finally:ˇ
24852 "});
24853
24854 // test `else` does not outdents when typed inside `except` block right after for block
24855 cx.set_state(indoc! {"
24856 def main():
24857 try:
24858 i = 2
24859 except:
24860 for i in range(n):
24861 pass
24862 ˇ
24863 "});
24864 cx.update_editor(|editor, window, cx| {
24865 editor.handle_input("else:", window, cx);
24866 });
24867 cx.assert_editor_state(indoc! {"
24868 def main():
24869 try:
24870 i = 2
24871 except:
24872 for i in range(n):
24873 pass
24874 else:ˇ
24875 "});
24876
24877 // test `finally` auto outdents when typed inside `else` block right after for block
24878 cx.set_state(indoc! {"
24879 def main():
24880 try:
24881 i = 2
24882 except:
24883 j = 2
24884 else:
24885 for i in range(n):
24886 pass
24887 ˇ
24888 "});
24889 cx.update_editor(|editor, window, cx| {
24890 editor.handle_input("finally:", window, cx);
24891 });
24892 cx.assert_editor_state(indoc! {"
24893 def main():
24894 try:
24895 i = 2
24896 except:
24897 j = 2
24898 else:
24899 for i in range(n):
24900 pass
24901 finally:ˇ
24902 "});
24903
24904 // test `except` outdents to inner "try" block
24905 cx.set_state(indoc! {"
24906 def main():
24907 try:
24908 i = 2
24909 if i == 2:
24910 try:
24911 i = 3
24912 ˇ
24913 "});
24914 cx.update_editor(|editor, window, cx| {
24915 editor.handle_input("except:", window, cx);
24916 });
24917 cx.assert_editor_state(indoc! {"
24918 def main():
24919 try:
24920 i = 2
24921 if i == 2:
24922 try:
24923 i = 3
24924 except:ˇ
24925 "});
24926
24927 // test `except` outdents to outer "try" block
24928 cx.set_state(indoc! {"
24929 def main():
24930 try:
24931 i = 2
24932 if i == 2:
24933 try:
24934 i = 3
24935 ˇ
24936 "});
24937 cx.update_editor(|editor, window, cx| {
24938 editor.handle_input("except:", window, cx);
24939 });
24940 cx.assert_editor_state(indoc! {"
24941 def main():
24942 try:
24943 i = 2
24944 if i == 2:
24945 try:
24946 i = 3
24947 except:ˇ
24948 "});
24949
24950 // test `else` stays at correct indent when typed after `for` block
24951 cx.set_state(indoc! {"
24952 def main():
24953 for i in range(10):
24954 if i == 3:
24955 break
24956 ˇ
24957 "});
24958 cx.update_editor(|editor, window, cx| {
24959 editor.handle_input("else:", window, cx);
24960 });
24961 cx.assert_editor_state(indoc! {"
24962 def main():
24963 for i in range(10):
24964 if i == 3:
24965 break
24966 else:ˇ
24967 "});
24968
24969 // test does not outdent on typing after line with square brackets
24970 cx.set_state(indoc! {"
24971 def f() -> list[str]:
24972 ˇ
24973 "});
24974 cx.update_editor(|editor, window, cx| {
24975 editor.handle_input("a", window, cx);
24976 });
24977 cx.assert_editor_state(indoc! {"
24978 def f() -> list[str]:
24979 aˇ
24980 "});
24981
24982 // test does not outdent on typing : after case keyword
24983 cx.set_state(indoc! {"
24984 match 1:
24985 caseˇ
24986 "});
24987 cx.update_editor(|editor, window, cx| {
24988 editor.handle_input(":", window, cx);
24989 });
24990 cx.assert_editor_state(indoc! {"
24991 match 1:
24992 case:ˇ
24993 "});
24994}
24995
24996#[gpui::test]
24997async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
24998 init_test(cx, |_| {});
24999 update_test_language_settings(cx, |settings| {
25000 settings.defaults.extend_comment_on_newline = Some(false);
25001 });
25002 let mut cx = EditorTestContext::new(cx).await;
25003 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
25004 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25005
25006 // test correct indent after newline on comment
25007 cx.set_state(indoc! {"
25008 # COMMENT:ˇ
25009 "});
25010 cx.update_editor(|editor, window, cx| {
25011 editor.newline(&Newline, window, cx);
25012 });
25013 cx.assert_editor_state(indoc! {"
25014 # COMMENT:
25015 ˇ
25016 "});
25017
25018 // test correct indent after newline in brackets
25019 cx.set_state(indoc! {"
25020 {ˇ}
25021 "});
25022 cx.update_editor(|editor, window, cx| {
25023 editor.newline(&Newline, window, cx);
25024 });
25025 cx.run_until_parked();
25026 cx.assert_editor_state(indoc! {"
25027 {
25028 ˇ
25029 }
25030 "});
25031
25032 cx.set_state(indoc! {"
25033 (ˇ)
25034 "});
25035 cx.update_editor(|editor, window, cx| {
25036 editor.newline(&Newline, window, cx);
25037 });
25038 cx.run_until_parked();
25039 cx.assert_editor_state(indoc! {"
25040 (
25041 ˇ
25042 )
25043 "});
25044
25045 // do not indent after empty lists or dictionaries
25046 cx.set_state(indoc! {"
25047 a = []ˇ
25048 "});
25049 cx.update_editor(|editor, window, cx| {
25050 editor.newline(&Newline, window, cx);
25051 });
25052 cx.run_until_parked();
25053 cx.assert_editor_state(indoc! {"
25054 a = []
25055 ˇ
25056 "});
25057}
25058
25059#[gpui::test]
25060async fn test_tab_in_leading_whitespace_auto_indents_for_bash(cx: &mut TestAppContext) {
25061 init_test(cx, |_| {});
25062
25063 let mut cx = EditorTestContext::new(cx).await;
25064 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25065 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25066
25067 // test cursor move to start of each line on tab
25068 // for `if`, `elif`, `else`, `while`, `for`, `case` and `function`
25069 cx.set_state(indoc! {"
25070 function main() {
25071 ˇ for item in $items; do
25072 ˇ while [ -n \"$item\" ]; do
25073 ˇ if [ \"$value\" -gt 10 ]; then
25074 ˇ continue
25075 ˇ elif [ \"$value\" -lt 0 ]; then
25076 ˇ break
25077 ˇ else
25078 ˇ echo \"$item\"
25079 ˇ fi
25080 ˇ done
25081 ˇ done
25082 ˇ}
25083 "});
25084 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25085 cx.assert_editor_state(indoc! {"
25086 function main() {
25087 ˇfor item in $items; do
25088 ˇwhile [ -n \"$item\" ]; do
25089 ˇif [ \"$value\" -gt 10 ]; then
25090 ˇcontinue
25091 ˇelif [ \"$value\" -lt 0 ]; then
25092 ˇbreak
25093 ˇelse
25094 ˇecho \"$item\"
25095 ˇfi
25096 ˇdone
25097 ˇdone
25098 ˇ}
25099 "});
25100 // test relative indent is preserved when tab
25101 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25102 cx.assert_editor_state(indoc! {"
25103 function main() {
25104 ˇfor item in $items; do
25105 ˇwhile [ -n \"$item\" ]; do
25106 ˇif [ \"$value\" -gt 10 ]; then
25107 ˇcontinue
25108 ˇelif [ \"$value\" -lt 0 ]; then
25109 ˇbreak
25110 ˇelse
25111 ˇecho \"$item\"
25112 ˇfi
25113 ˇdone
25114 ˇdone
25115 ˇ}
25116 "});
25117
25118 // test cursor move to start of each line on tab
25119 // for `case` statement with patterns
25120 cx.set_state(indoc! {"
25121 function handle() {
25122 ˇ case \"$1\" in
25123 ˇ start)
25124 ˇ echo \"a\"
25125 ˇ ;;
25126 ˇ stop)
25127 ˇ echo \"b\"
25128 ˇ ;;
25129 ˇ *)
25130 ˇ echo \"c\"
25131 ˇ ;;
25132 ˇ esac
25133 ˇ}
25134 "});
25135 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
25136 cx.assert_editor_state(indoc! {"
25137 function handle() {
25138 ˇcase \"$1\" in
25139 ˇstart)
25140 ˇecho \"a\"
25141 ˇ;;
25142 ˇstop)
25143 ˇecho \"b\"
25144 ˇ;;
25145 ˇ*)
25146 ˇecho \"c\"
25147 ˇ;;
25148 ˇesac
25149 ˇ}
25150 "});
25151}
25152
25153#[gpui::test]
25154async fn test_indent_after_input_for_bash(cx: &mut TestAppContext) {
25155 init_test(cx, |_| {});
25156
25157 let mut cx = EditorTestContext::new(cx).await;
25158 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25159 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25160
25161 // test indents on comment insert
25162 cx.set_state(indoc! {"
25163 function main() {
25164 ˇ for item in $items; do
25165 ˇ while [ -n \"$item\" ]; do
25166 ˇ if [ \"$value\" -gt 10 ]; then
25167 ˇ continue
25168 ˇ elif [ \"$value\" -lt 0 ]; then
25169 ˇ break
25170 ˇ else
25171 ˇ echo \"$item\"
25172 ˇ fi
25173 ˇ done
25174 ˇ done
25175 ˇ}
25176 "});
25177 cx.update_editor(|e, window, cx| e.handle_input("#", window, cx));
25178 cx.assert_editor_state(indoc! {"
25179 function main() {
25180 #ˇ for item in $items; do
25181 #ˇ while [ -n \"$item\" ]; do
25182 #ˇ if [ \"$value\" -gt 10 ]; then
25183 #ˇ continue
25184 #ˇ elif [ \"$value\" -lt 0 ]; then
25185 #ˇ break
25186 #ˇ else
25187 #ˇ echo \"$item\"
25188 #ˇ fi
25189 #ˇ done
25190 #ˇ done
25191 #ˇ}
25192 "});
25193}
25194
25195#[gpui::test]
25196async fn test_outdent_after_input_for_bash(cx: &mut TestAppContext) {
25197 init_test(cx, |_| {});
25198
25199 let mut cx = EditorTestContext::new(cx).await;
25200 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25201 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25202
25203 // test `else` auto outdents when typed inside `if` block
25204 cx.set_state(indoc! {"
25205 if [ \"$1\" = \"test\" ]; then
25206 echo \"foo bar\"
25207 ˇ
25208 "});
25209 cx.update_editor(|editor, window, cx| {
25210 editor.handle_input("else", window, cx);
25211 });
25212 cx.assert_editor_state(indoc! {"
25213 if [ \"$1\" = \"test\" ]; then
25214 echo \"foo bar\"
25215 elseˇ
25216 "});
25217
25218 // test `elif` auto outdents when typed inside `if` block
25219 cx.set_state(indoc! {"
25220 if [ \"$1\" = \"test\" ]; then
25221 echo \"foo bar\"
25222 ˇ
25223 "});
25224 cx.update_editor(|editor, window, cx| {
25225 editor.handle_input("elif", window, cx);
25226 });
25227 cx.assert_editor_state(indoc! {"
25228 if [ \"$1\" = \"test\" ]; then
25229 echo \"foo bar\"
25230 elifˇ
25231 "});
25232
25233 // test `fi` auto outdents when typed inside `else` block
25234 cx.set_state(indoc! {"
25235 if [ \"$1\" = \"test\" ]; then
25236 echo \"foo bar\"
25237 else
25238 echo \"bar baz\"
25239 ˇ
25240 "});
25241 cx.update_editor(|editor, window, cx| {
25242 editor.handle_input("fi", window, cx);
25243 });
25244 cx.assert_editor_state(indoc! {"
25245 if [ \"$1\" = \"test\" ]; then
25246 echo \"foo bar\"
25247 else
25248 echo \"bar baz\"
25249 fiˇ
25250 "});
25251
25252 // test `done` auto outdents when typed inside `while` block
25253 cx.set_state(indoc! {"
25254 while read line; do
25255 echo \"$line\"
25256 ˇ
25257 "});
25258 cx.update_editor(|editor, window, cx| {
25259 editor.handle_input("done", window, cx);
25260 });
25261 cx.assert_editor_state(indoc! {"
25262 while read line; do
25263 echo \"$line\"
25264 doneˇ
25265 "});
25266
25267 // test `done` auto outdents when typed inside `for` block
25268 cx.set_state(indoc! {"
25269 for file in *.txt; do
25270 cat \"$file\"
25271 ˇ
25272 "});
25273 cx.update_editor(|editor, window, cx| {
25274 editor.handle_input("done", window, cx);
25275 });
25276 cx.assert_editor_state(indoc! {"
25277 for file in *.txt; do
25278 cat \"$file\"
25279 doneˇ
25280 "});
25281
25282 // test `esac` auto outdents when typed inside `case` block
25283 cx.set_state(indoc! {"
25284 case \"$1\" in
25285 start)
25286 echo \"foo bar\"
25287 ;;
25288 stop)
25289 echo \"bar baz\"
25290 ;;
25291 ˇ
25292 "});
25293 cx.update_editor(|editor, window, cx| {
25294 editor.handle_input("esac", window, cx);
25295 });
25296 cx.assert_editor_state(indoc! {"
25297 case \"$1\" in
25298 start)
25299 echo \"foo bar\"
25300 ;;
25301 stop)
25302 echo \"bar baz\"
25303 ;;
25304 esacˇ
25305 "});
25306
25307 // test `*)` auto outdents when typed inside `case` block
25308 cx.set_state(indoc! {"
25309 case \"$1\" in
25310 start)
25311 echo \"foo bar\"
25312 ;;
25313 ˇ
25314 "});
25315 cx.update_editor(|editor, window, cx| {
25316 editor.handle_input("*)", window, cx);
25317 });
25318 cx.assert_editor_state(indoc! {"
25319 case \"$1\" in
25320 start)
25321 echo \"foo bar\"
25322 ;;
25323 *)ˇ
25324 "});
25325
25326 // test `fi` outdents to correct level with nested if blocks
25327 cx.set_state(indoc! {"
25328 if [ \"$1\" = \"test\" ]; then
25329 echo \"outer if\"
25330 if [ \"$2\" = \"debug\" ]; then
25331 echo \"inner if\"
25332 ˇ
25333 "});
25334 cx.update_editor(|editor, window, cx| {
25335 editor.handle_input("fi", window, cx);
25336 });
25337 cx.assert_editor_state(indoc! {"
25338 if [ \"$1\" = \"test\" ]; then
25339 echo \"outer if\"
25340 if [ \"$2\" = \"debug\" ]; then
25341 echo \"inner if\"
25342 fiˇ
25343 "});
25344}
25345
25346#[gpui::test]
25347async fn test_indent_on_newline_for_bash(cx: &mut TestAppContext) {
25348 init_test(cx, |_| {});
25349 update_test_language_settings(cx, |settings| {
25350 settings.defaults.extend_comment_on_newline = Some(false);
25351 });
25352 let mut cx = EditorTestContext::new(cx).await;
25353 let language = languages::language("bash", tree_sitter_bash::LANGUAGE.into());
25354 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
25355
25356 // test correct indent after newline on comment
25357 cx.set_state(indoc! {"
25358 # COMMENT:ˇ
25359 "});
25360 cx.update_editor(|editor, window, cx| {
25361 editor.newline(&Newline, window, cx);
25362 });
25363 cx.assert_editor_state(indoc! {"
25364 # COMMENT:
25365 ˇ
25366 "});
25367
25368 // test correct indent after newline after `then`
25369 cx.set_state(indoc! {"
25370
25371 if [ \"$1\" = \"test\" ]; thenˇ
25372 "});
25373 cx.update_editor(|editor, window, cx| {
25374 editor.newline(&Newline, window, cx);
25375 });
25376 cx.run_until_parked();
25377 cx.assert_editor_state(indoc! {"
25378
25379 if [ \"$1\" = \"test\" ]; then
25380 ˇ
25381 "});
25382
25383 // test correct indent after newline after `else`
25384 cx.set_state(indoc! {"
25385 if [ \"$1\" = \"test\" ]; then
25386 elseˇ
25387 "});
25388 cx.update_editor(|editor, window, cx| {
25389 editor.newline(&Newline, window, cx);
25390 });
25391 cx.run_until_parked();
25392 cx.assert_editor_state(indoc! {"
25393 if [ \"$1\" = \"test\" ]; then
25394 else
25395 ˇ
25396 "});
25397
25398 // test correct indent after newline after `elif`
25399 cx.set_state(indoc! {"
25400 if [ \"$1\" = \"test\" ]; then
25401 elifˇ
25402 "});
25403 cx.update_editor(|editor, window, cx| {
25404 editor.newline(&Newline, window, cx);
25405 });
25406 cx.run_until_parked();
25407 cx.assert_editor_state(indoc! {"
25408 if [ \"$1\" = \"test\" ]; then
25409 elif
25410 ˇ
25411 "});
25412
25413 // test correct indent after newline after `do`
25414 cx.set_state(indoc! {"
25415 for file in *.txt; doˇ
25416 "});
25417 cx.update_editor(|editor, window, cx| {
25418 editor.newline(&Newline, window, cx);
25419 });
25420 cx.run_until_parked();
25421 cx.assert_editor_state(indoc! {"
25422 for file in *.txt; do
25423 ˇ
25424 "});
25425
25426 // test correct indent after newline after case pattern
25427 cx.set_state(indoc! {"
25428 case \"$1\" in
25429 start)ˇ
25430 "});
25431 cx.update_editor(|editor, window, cx| {
25432 editor.newline(&Newline, window, cx);
25433 });
25434 cx.run_until_parked();
25435 cx.assert_editor_state(indoc! {"
25436 case \"$1\" in
25437 start)
25438 ˇ
25439 "});
25440
25441 // test correct indent after newline after case pattern
25442 cx.set_state(indoc! {"
25443 case \"$1\" in
25444 start)
25445 ;;
25446 *)ˇ
25447 "});
25448 cx.update_editor(|editor, window, cx| {
25449 editor.newline(&Newline, window, cx);
25450 });
25451 cx.run_until_parked();
25452 cx.assert_editor_state(indoc! {"
25453 case \"$1\" in
25454 start)
25455 ;;
25456 *)
25457 ˇ
25458 "});
25459
25460 // test correct indent after newline after function opening brace
25461 cx.set_state(indoc! {"
25462 function test() {ˇ}
25463 "});
25464 cx.update_editor(|editor, window, cx| {
25465 editor.newline(&Newline, window, cx);
25466 });
25467 cx.run_until_parked();
25468 cx.assert_editor_state(indoc! {"
25469 function test() {
25470 ˇ
25471 }
25472 "});
25473
25474 // test no extra indent after semicolon on same line
25475 cx.set_state(indoc! {"
25476 echo \"test\";ˇ
25477 "});
25478 cx.update_editor(|editor, window, cx| {
25479 editor.newline(&Newline, window, cx);
25480 });
25481 cx.run_until_parked();
25482 cx.assert_editor_state(indoc! {"
25483 echo \"test\";
25484 ˇ
25485 "});
25486}
25487
25488fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
25489 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
25490 point..point
25491}
25492
25493#[track_caller]
25494fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
25495 let (text, ranges) = marked_text_ranges(marked_text, true);
25496 assert_eq!(editor.text(cx), text);
25497 assert_eq!(
25498 editor.selections.ranges(&editor.display_snapshot(cx)),
25499 ranges,
25500 "Assert selections are {}",
25501 marked_text
25502 );
25503}
25504
25505pub fn handle_signature_help_request(
25506 cx: &mut EditorLspTestContext,
25507 mocked_response: lsp::SignatureHelp,
25508) -> impl Future<Output = ()> + use<> {
25509 let mut request =
25510 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
25511 let mocked_response = mocked_response.clone();
25512 async move { Ok(Some(mocked_response)) }
25513 });
25514
25515 async move {
25516 request.next().await;
25517 }
25518}
25519
25520#[track_caller]
25521pub fn check_displayed_completions(expected: Vec<&'static str>, cx: &mut EditorLspTestContext) {
25522 cx.update_editor(|editor, _, _| {
25523 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow().as_ref() {
25524 let entries = menu.entries.borrow();
25525 let entries = entries
25526 .iter()
25527 .map(|entry| entry.string.as_str())
25528 .collect::<Vec<_>>();
25529 assert_eq!(entries, expected);
25530 } else {
25531 panic!("Expected completions menu");
25532 }
25533 });
25534}
25535
25536/// Handle completion request passing a marked string specifying where the completion
25537/// should be triggered from using '|' character, what range should be replaced, and what completions
25538/// should be returned using '<' and '>' to delimit the range.
25539///
25540/// Also see `handle_completion_request_with_insert_and_replace`.
25541#[track_caller]
25542pub fn handle_completion_request(
25543 marked_string: &str,
25544 completions: Vec<&'static str>,
25545 is_incomplete: bool,
25546 counter: Arc<AtomicUsize>,
25547 cx: &mut EditorLspTestContext,
25548) -> impl Future<Output = ()> {
25549 let complete_from_marker: TextRangeMarker = '|'.into();
25550 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25551 let (_, mut marked_ranges) = marked_text_ranges_by(
25552 marked_string,
25553 vec![complete_from_marker.clone(), replace_range_marker.clone()],
25554 );
25555
25556 let complete_from_position =
25557 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25558 let replace_range =
25559 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25560
25561 let mut request =
25562 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25563 let completions = completions.clone();
25564 counter.fetch_add(1, atomic::Ordering::Release);
25565 async move {
25566 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25567 assert_eq!(
25568 params.text_document_position.position,
25569 complete_from_position
25570 );
25571 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
25572 is_incomplete,
25573 item_defaults: None,
25574 items: completions
25575 .iter()
25576 .map(|completion_text| lsp::CompletionItem {
25577 label: completion_text.to_string(),
25578 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
25579 range: replace_range,
25580 new_text: completion_text.to_string(),
25581 })),
25582 ..Default::default()
25583 })
25584 .collect(),
25585 })))
25586 }
25587 });
25588
25589 async move {
25590 request.next().await;
25591 }
25592}
25593
25594/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
25595/// given instead, which also contains an `insert` range.
25596///
25597/// This function uses markers to define ranges:
25598/// - `|` marks the cursor position
25599/// - `<>` marks the replace range
25600/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
25601pub fn handle_completion_request_with_insert_and_replace(
25602 cx: &mut EditorLspTestContext,
25603 marked_string: &str,
25604 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
25605 counter: Arc<AtomicUsize>,
25606) -> impl Future<Output = ()> {
25607 let complete_from_marker: TextRangeMarker = '|'.into();
25608 let replace_range_marker: TextRangeMarker = ('<', '>').into();
25609 let insert_range_marker: TextRangeMarker = ('{', '}').into();
25610
25611 let (_, mut marked_ranges) = marked_text_ranges_by(
25612 marked_string,
25613 vec![
25614 complete_from_marker.clone(),
25615 replace_range_marker.clone(),
25616 insert_range_marker.clone(),
25617 ],
25618 );
25619
25620 let complete_from_position =
25621 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
25622 let replace_range =
25623 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
25624
25625 let insert_range = match marked_ranges.remove(&insert_range_marker) {
25626 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
25627 _ => lsp::Range {
25628 start: replace_range.start,
25629 end: complete_from_position,
25630 },
25631 };
25632
25633 let mut request =
25634 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
25635 let completions = completions.clone();
25636 counter.fetch_add(1, atomic::Ordering::Release);
25637 async move {
25638 assert_eq!(params.text_document_position.text_document.uri, url.clone());
25639 assert_eq!(
25640 params.text_document_position.position, complete_from_position,
25641 "marker `|` position doesn't match",
25642 );
25643 Ok(Some(lsp::CompletionResponse::Array(
25644 completions
25645 .iter()
25646 .map(|(label, new_text)| lsp::CompletionItem {
25647 label: label.to_string(),
25648 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
25649 lsp::InsertReplaceEdit {
25650 insert: insert_range,
25651 replace: replace_range,
25652 new_text: new_text.to_string(),
25653 },
25654 )),
25655 ..Default::default()
25656 })
25657 .collect(),
25658 )))
25659 }
25660 });
25661
25662 async move {
25663 request.next().await;
25664 }
25665}
25666
25667fn handle_resolve_completion_request(
25668 cx: &mut EditorLspTestContext,
25669 edits: Option<Vec<(&'static str, &'static str)>>,
25670) -> impl Future<Output = ()> {
25671 let edits = edits.map(|edits| {
25672 edits
25673 .iter()
25674 .map(|(marked_string, new_text)| {
25675 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
25676 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
25677 lsp::TextEdit::new(replace_range, new_text.to_string())
25678 })
25679 .collect::<Vec<_>>()
25680 });
25681
25682 let mut request =
25683 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
25684 let edits = edits.clone();
25685 async move {
25686 Ok(lsp::CompletionItem {
25687 additional_text_edits: edits,
25688 ..Default::default()
25689 })
25690 }
25691 });
25692
25693 async move {
25694 request.next().await;
25695 }
25696}
25697
25698pub(crate) fn update_test_language_settings(
25699 cx: &mut TestAppContext,
25700 f: impl Fn(&mut AllLanguageSettingsContent),
25701) {
25702 cx.update(|cx| {
25703 SettingsStore::update_global(cx, |store, cx| {
25704 store.update_user_settings(cx, |settings| f(&mut settings.project.all_languages));
25705 });
25706 });
25707}
25708
25709pub(crate) fn update_test_project_settings(
25710 cx: &mut TestAppContext,
25711 f: impl Fn(&mut ProjectSettingsContent),
25712) {
25713 cx.update(|cx| {
25714 SettingsStore::update_global(cx, |store, cx| {
25715 store.update_user_settings(cx, |settings| f(&mut settings.project));
25716 });
25717 });
25718}
25719
25720pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
25721 cx.update(|cx| {
25722 assets::Assets.load_test_fonts(cx);
25723 let store = SettingsStore::test(cx);
25724 cx.set_global(store);
25725 theme::init(theme::LoadThemes::JustBase, cx);
25726 release_channel::init(SemanticVersion::default(), cx);
25727 crate::init(cx);
25728 });
25729 zlog::init_test();
25730 update_test_language_settings(cx, f);
25731}
25732
25733#[track_caller]
25734fn assert_hunk_revert(
25735 not_reverted_text_with_selections: &str,
25736 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
25737 expected_reverted_text_with_selections: &str,
25738 base_text: &str,
25739 cx: &mut EditorLspTestContext,
25740) {
25741 cx.set_state(not_reverted_text_with_selections);
25742 cx.set_head_text(base_text);
25743 cx.executor().run_until_parked();
25744
25745 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
25746 let snapshot = editor.snapshot(window, cx);
25747 let reverted_hunk_statuses = snapshot
25748 .buffer_snapshot()
25749 .diff_hunks_in_range(0..snapshot.buffer_snapshot().len())
25750 .map(|hunk| hunk.status().kind)
25751 .collect::<Vec<_>>();
25752
25753 editor.git_restore(&Default::default(), window, cx);
25754 reverted_hunk_statuses
25755 });
25756 cx.executor().run_until_parked();
25757 cx.assert_editor_state(expected_reverted_text_with_selections);
25758 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
25759}
25760
25761#[gpui::test(iterations = 10)]
25762async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
25763 init_test(cx, |_| {});
25764
25765 let diagnostic_requests = Arc::new(AtomicUsize::new(0));
25766 let counter = diagnostic_requests.clone();
25767
25768 let fs = FakeFs::new(cx.executor());
25769 fs.insert_tree(
25770 path!("/a"),
25771 json!({
25772 "first.rs": "fn main() { let a = 5; }",
25773 "second.rs": "// Test file",
25774 }),
25775 )
25776 .await;
25777
25778 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
25779 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
25780 let cx = &mut VisualTestContext::from_window(*workspace, cx);
25781
25782 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
25783 language_registry.add(rust_lang());
25784 let mut fake_servers = language_registry.register_fake_lsp(
25785 "Rust",
25786 FakeLspAdapter {
25787 capabilities: lsp::ServerCapabilities {
25788 diagnostic_provider: Some(lsp::DiagnosticServerCapabilities::Options(
25789 lsp::DiagnosticOptions {
25790 identifier: None,
25791 inter_file_dependencies: true,
25792 workspace_diagnostics: true,
25793 work_done_progress_options: Default::default(),
25794 },
25795 )),
25796 ..Default::default()
25797 },
25798 ..Default::default()
25799 },
25800 );
25801
25802 let editor = workspace
25803 .update(cx, |workspace, window, cx| {
25804 workspace.open_abs_path(
25805 PathBuf::from(path!("/a/first.rs")),
25806 OpenOptions::default(),
25807 window,
25808 cx,
25809 )
25810 })
25811 .unwrap()
25812 .await
25813 .unwrap()
25814 .downcast::<Editor>()
25815 .unwrap();
25816 let fake_server = fake_servers.next().await.unwrap();
25817 let server_id = fake_server.server.server_id();
25818 let mut first_request = fake_server
25819 .set_request_handler::<lsp::request::DocumentDiagnosticRequest, _, _>(move |params, _| {
25820 let new_result_id = counter.fetch_add(1, atomic::Ordering::Release) + 1;
25821 let result_id = Some(new_result_id.to_string());
25822 assert_eq!(
25823 params.text_document.uri,
25824 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
25825 );
25826 async move {
25827 Ok(lsp::DocumentDiagnosticReportResult::Report(
25828 lsp::DocumentDiagnosticReport::Full(lsp::RelatedFullDocumentDiagnosticReport {
25829 related_documents: None,
25830 full_document_diagnostic_report: lsp::FullDocumentDiagnosticReport {
25831 items: Vec::new(),
25832 result_id,
25833 },
25834 }),
25835 ))
25836 }
25837 });
25838
25839 let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
25840 project.update(cx, |project, cx| {
25841 let buffer_id = editor
25842 .read(cx)
25843 .buffer()
25844 .read(cx)
25845 .as_singleton()
25846 .expect("created a singleton buffer")
25847 .read(cx)
25848 .remote_id();
25849 let buffer_result_id = project
25850 .lsp_store()
25851 .read(cx)
25852 .result_id(server_id, buffer_id, cx);
25853 assert_eq!(expected, buffer_result_id);
25854 });
25855 };
25856
25857 ensure_result_id(None, cx);
25858 cx.executor().advance_clock(Duration::from_millis(60));
25859 cx.executor().run_until_parked();
25860 assert_eq!(
25861 diagnostic_requests.load(atomic::Ordering::Acquire),
25862 1,
25863 "Opening file should trigger diagnostic request"
25864 );
25865 first_request
25866 .next()
25867 .await
25868 .expect("should have sent the first diagnostics pull request");
25869 ensure_result_id(Some("1".to_string()), cx);
25870
25871 // Editing should trigger diagnostics
25872 editor.update_in(cx, |editor, window, cx| {
25873 editor.handle_input("2", window, cx)
25874 });
25875 cx.executor().advance_clock(Duration::from_millis(60));
25876 cx.executor().run_until_parked();
25877 assert_eq!(
25878 diagnostic_requests.load(atomic::Ordering::Acquire),
25879 2,
25880 "Editing should trigger diagnostic request"
25881 );
25882 ensure_result_id(Some("2".to_string()), cx);
25883
25884 // Moving cursor should not trigger diagnostic request
25885 editor.update_in(cx, |editor, window, cx| {
25886 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
25887 s.select_ranges([Point::new(0, 0)..Point::new(0, 0)])
25888 });
25889 });
25890 cx.executor().advance_clock(Duration::from_millis(60));
25891 cx.executor().run_until_parked();
25892 assert_eq!(
25893 diagnostic_requests.load(atomic::Ordering::Acquire),
25894 2,
25895 "Cursor movement should not trigger diagnostic request"
25896 );
25897 ensure_result_id(Some("2".to_string()), cx);
25898 // Multiple rapid edits should be debounced
25899 for _ in 0..5 {
25900 editor.update_in(cx, |editor, window, cx| {
25901 editor.handle_input("x", window, cx)
25902 });
25903 }
25904 cx.executor().advance_clock(Duration::from_millis(60));
25905 cx.executor().run_until_parked();
25906
25907 let final_requests = diagnostic_requests.load(atomic::Ordering::Acquire);
25908 assert!(
25909 final_requests <= 4,
25910 "Multiple rapid edits should be debounced (got {final_requests} requests)",
25911 );
25912 ensure_result_id(Some(final_requests.to_string()), cx);
25913}
25914
25915#[gpui::test]
25916async fn test_add_selection_after_moving_with_multiple_cursors(cx: &mut TestAppContext) {
25917 // Regression test for issue #11671
25918 // Previously, adding a cursor after moving multiple cursors would reset
25919 // the cursor count instead of adding to the existing cursors.
25920 init_test(cx, |_| {});
25921 let mut cx = EditorTestContext::new(cx).await;
25922
25923 // Create a simple buffer with cursor at start
25924 cx.set_state(indoc! {"
25925 ˇaaaa
25926 bbbb
25927 cccc
25928 dddd
25929 eeee
25930 ffff
25931 gggg
25932 hhhh"});
25933
25934 // Add 2 cursors below (so we have 3 total)
25935 cx.update_editor(|editor, window, cx| {
25936 editor.add_selection_below(&Default::default(), window, cx);
25937 editor.add_selection_below(&Default::default(), window, cx);
25938 });
25939
25940 // Verify we have 3 cursors
25941 let initial_count = cx.update_editor(|editor, _, _| editor.selections.count());
25942 assert_eq!(
25943 initial_count, 3,
25944 "Should have 3 cursors after adding 2 below"
25945 );
25946
25947 // Move down one line
25948 cx.update_editor(|editor, window, cx| {
25949 editor.move_down(&MoveDown, window, cx);
25950 });
25951
25952 // Add another cursor below
25953 cx.update_editor(|editor, window, cx| {
25954 editor.add_selection_below(&Default::default(), window, cx);
25955 });
25956
25957 // Should now have 4 cursors (3 original + 1 new)
25958 let final_count = cx.update_editor(|editor, _, _| editor.selections.count());
25959 assert_eq!(
25960 final_count, 4,
25961 "Should have 4 cursors after moving and adding another"
25962 );
25963}
25964
25965#[gpui::test]
25966async fn test_add_selection_skip_soft_wrap_option(cx: &mut TestAppContext) {
25967 init_test(cx, |_| {});
25968
25969 let mut cx = EditorTestContext::new(cx).await;
25970
25971 cx.set_state(indoc!(
25972 r#"ˇThis is a very long line that will be wrapped when soft wrapping is enabled
25973 Second line here"#
25974 ));
25975
25976 cx.update_editor(|editor, window, cx| {
25977 // Enable soft wrapping with a narrow width to force soft wrapping and
25978 // confirm that more than 2 rows are being displayed.
25979 editor.set_wrap_width(Some(100.0.into()), cx);
25980 assert!(editor.display_text(cx).lines().count() > 2);
25981
25982 editor.add_selection_below(
25983 &AddSelectionBelow {
25984 skip_soft_wrap: true,
25985 },
25986 window,
25987 cx,
25988 );
25989
25990 assert_eq!(
25991 display_ranges(editor, cx),
25992 &[
25993 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
25994 DisplayPoint::new(DisplayRow(8), 0)..DisplayPoint::new(DisplayRow(8), 0),
25995 ]
25996 );
25997
25998 editor.add_selection_above(
25999 &AddSelectionAbove {
26000 skip_soft_wrap: true,
26001 },
26002 window,
26003 cx,
26004 );
26005
26006 assert_eq!(
26007 display_ranges(editor, cx),
26008 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26009 );
26010
26011 editor.add_selection_below(
26012 &AddSelectionBelow {
26013 skip_soft_wrap: false,
26014 },
26015 window,
26016 cx,
26017 );
26018
26019 assert_eq!(
26020 display_ranges(editor, cx),
26021 &[
26022 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
26023 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
26024 ]
26025 );
26026
26027 editor.add_selection_above(
26028 &AddSelectionAbove {
26029 skip_soft_wrap: false,
26030 },
26031 window,
26032 cx,
26033 );
26034
26035 assert_eq!(
26036 display_ranges(editor, cx),
26037 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
26038 );
26039 });
26040}
26041
26042#[gpui::test(iterations = 10)]
26043async fn test_document_colors(cx: &mut TestAppContext) {
26044 let expected_color = Rgba {
26045 r: 0.33,
26046 g: 0.33,
26047 b: 0.33,
26048 a: 0.33,
26049 };
26050
26051 init_test(cx, |_| {});
26052
26053 let fs = FakeFs::new(cx.executor());
26054 fs.insert_tree(
26055 path!("/a"),
26056 json!({
26057 "first.rs": "fn main() { let a = 5; }",
26058 }),
26059 )
26060 .await;
26061
26062 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
26063 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26064 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26065
26066 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26067 language_registry.add(rust_lang());
26068 let mut fake_servers = language_registry.register_fake_lsp(
26069 "Rust",
26070 FakeLspAdapter {
26071 capabilities: lsp::ServerCapabilities {
26072 color_provider: Some(lsp::ColorProviderCapability::Simple(true)),
26073 ..lsp::ServerCapabilities::default()
26074 },
26075 name: "rust-analyzer",
26076 ..FakeLspAdapter::default()
26077 },
26078 );
26079 let mut fake_servers_without_capabilities = language_registry.register_fake_lsp(
26080 "Rust",
26081 FakeLspAdapter {
26082 capabilities: lsp::ServerCapabilities {
26083 color_provider: Some(lsp::ColorProviderCapability::Simple(false)),
26084 ..lsp::ServerCapabilities::default()
26085 },
26086 name: "not-rust-analyzer",
26087 ..FakeLspAdapter::default()
26088 },
26089 );
26090
26091 let editor = workspace
26092 .update(cx, |workspace, window, cx| {
26093 workspace.open_abs_path(
26094 PathBuf::from(path!("/a/first.rs")),
26095 OpenOptions::default(),
26096 window,
26097 cx,
26098 )
26099 })
26100 .unwrap()
26101 .await
26102 .unwrap()
26103 .downcast::<Editor>()
26104 .unwrap();
26105 let fake_language_server = fake_servers.next().await.unwrap();
26106 let fake_language_server_without_capabilities =
26107 fake_servers_without_capabilities.next().await.unwrap();
26108 let requests_made = Arc::new(AtomicUsize::new(0));
26109 let closure_requests_made = Arc::clone(&requests_made);
26110 let mut color_request_handle = fake_language_server
26111 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26112 let requests_made = Arc::clone(&closure_requests_made);
26113 async move {
26114 assert_eq!(
26115 params.text_document.uri,
26116 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26117 );
26118 requests_made.fetch_add(1, atomic::Ordering::Release);
26119 Ok(vec![
26120 lsp::ColorInformation {
26121 range: lsp::Range {
26122 start: lsp::Position {
26123 line: 0,
26124 character: 0,
26125 },
26126 end: lsp::Position {
26127 line: 0,
26128 character: 1,
26129 },
26130 },
26131 color: lsp::Color {
26132 red: 0.33,
26133 green: 0.33,
26134 blue: 0.33,
26135 alpha: 0.33,
26136 },
26137 },
26138 lsp::ColorInformation {
26139 range: lsp::Range {
26140 start: lsp::Position {
26141 line: 0,
26142 character: 0,
26143 },
26144 end: lsp::Position {
26145 line: 0,
26146 character: 1,
26147 },
26148 },
26149 color: lsp::Color {
26150 red: 0.33,
26151 green: 0.33,
26152 blue: 0.33,
26153 alpha: 0.33,
26154 },
26155 },
26156 ])
26157 }
26158 });
26159
26160 let _handle = fake_language_server_without_capabilities
26161 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |_, _| async move {
26162 panic!("Should not be called");
26163 });
26164 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26165 color_request_handle.next().await.unwrap();
26166 cx.run_until_parked();
26167 assert_eq!(
26168 1,
26169 requests_made.load(atomic::Ordering::Acquire),
26170 "Should query for colors once per editor open"
26171 );
26172 editor.update_in(cx, |editor, _, cx| {
26173 assert_eq!(
26174 vec![expected_color],
26175 extract_color_inlays(editor, cx),
26176 "Should have an initial inlay"
26177 );
26178 });
26179
26180 // opening another file in a split should not influence the LSP query counter
26181 workspace
26182 .update(cx, |workspace, window, cx| {
26183 assert_eq!(
26184 workspace.panes().len(),
26185 1,
26186 "Should have one pane with one editor"
26187 );
26188 workspace.move_item_to_pane_in_direction(
26189 &MoveItemToPaneInDirection {
26190 direction: SplitDirection::Right,
26191 focus: false,
26192 clone: true,
26193 },
26194 window,
26195 cx,
26196 );
26197 })
26198 .unwrap();
26199 cx.run_until_parked();
26200 workspace
26201 .update(cx, |workspace, _, cx| {
26202 let panes = workspace.panes();
26203 assert_eq!(panes.len(), 2, "Should have two panes after splitting");
26204 for pane in panes {
26205 let editor = pane
26206 .read(cx)
26207 .active_item()
26208 .and_then(|item| item.downcast::<Editor>())
26209 .expect("Should have opened an editor in each split");
26210 let editor_file = editor
26211 .read(cx)
26212 .buffer()
26213 .read(cx)
26214 .as_singleton()
26215 .expect("test deals with singleton buffers")
26216 .read(cx)
26217 .file()
26218 .expect("test buffese should have a file")
26219 .path();
26220 assert_eq!(
26221 editor_file.as_ref(),
26222 rel_path("first.rs"),
26223 "Both editors should be opened for the same file"
26224 )
26225 }
26226 })
26227 .unwrap();
26228
26229 cx.executor().advance_clock(Duration::from_millis(500));
26230 let save = editor.update_in(cx, |editor, window, cx| {
26231 editor.move_to_end(&MoveToEnd, window, cx);
26232 editor.handle_input("dirty", window, cx);
26233 editor.save(
26234 SaveOptions {
26235 format: true,
26236 autosave: true,
26237 },
26238 project.clone(),
26239 window,
26240 cx,
26241 )
26242 });
26243 save.await.unwrap();
26244
26245 color_request_handle.next().await.unwrap();
26246 cx.run_until_parked();
26247 assert_eq!(
26248 2,
26249 requests_made.load(atomic::Ordering::Acquire),
26250 "Should query for colors once per save (deduplicated) and once per formatting after save"
26251 );
26252
26253 drop(editor);
26254 let close = workspace
26255 .update(cx, |workspace, window, cx| {
26256 workspace.active_pane().update(cx, |pane, cx| {
26257 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26258 })
26259 })
26260 .unwrap();
26261 close.await.unwrap();
26262 let close = workspace
26263 .update(cx, |workspace, window, cx| {
26264 workspace.active_pane().update(cx, |pane, cx| {
26265 pane.close_active_item(&CloseActiveItem::default(), window, cx)
26266 })
26267 })
26268 .unwrap();
26269 close.await.unwrap();
26270 assert_eq!(
26271 2,
26272 requests_made.load(atomic::Ordering::Acquire),
26273 "After saving and closing all editors, no extra requests should be made"
26274 );
26275 workspace
26276 .update(cx, |workspace, _, cx| {
26277 assert!(
26278 workspace.active_item(cx).is_none(),
26279 "Should close all editors"
26280 )
26281 })
26282 .unwrap();
26283
26284 workspace
26285 .update(cx, |workspace, window, cx| {
26286 workspace.active_pane().update(cx, |pane, cx| {
26287 pane.navigate_backward(&workspace::GoBack, window, cx);
26288 })
26289 })
26290 .unwrap();
26291 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26292 cx.run_until_parked();
26293 let editor = workspace
26294 .update(cx, |workspace, _, cx| {
26295 workspace
26296 .active_item(cx)
26297 .expect("Should have reopened the editor again after navigating back")
26298 .downcast::<Editor>()
26299 .expect("Should be an editor")
26300 })
26301 .unwrap();
26302
26303 assert_eq!(
26304 2,
26305 requests_made.load(atomic::Ordering::Acquire),
26306 "Cache should be reused on buffer close and reopen"
26307 );
26308 editor.update(cx, |editor, cx| {
26309 assert_eq!(
26310 vec![expected_color],
26311 extract_color_inlays(editor, cx),
26312 "Should have an initial inlay"
26313 );
26314 });
26315
26316 drop(color_request_handle);
26317 let closure_requests_made = Arc::clone(&requests_made);
26318 let mut empty_color_request_handle = fake_language_server
26319 .set_request_handler::<lsp::request::DocumentColor, _, _>(move |params, _| {
26320 let requests_made = Arc::clone(&closure_requests_made);
26321 async move {
26322 assert_eq!(
26323 params.text_document.uri,
26324 lsp::Uri::from_file_path(path!("/a/first.rs")).unwrap()
26325 );
26326 requests_made.fetch_add(1, atomic::Ordering::Release);
26327 Ok(Vec::new())
26328 }
26329 });
26330 let save = editor.update_in(cx, |editor, window, cx| {
26331 editor.move_to_end(&MoveToEnd, window, cx);
26332 editor.handle_input("dirty_again", window, cx);
26333 editor.save(
26334 SaveOptions {
26335 format: false,
26336 autosave: true,
26337 },
26338 project.clone(),
26339 window,
26340 cx,
26341 )
26342 });
26343 save.await.unwrap();
26344
26345 cx.executor().advance_clock(FETCH_COLORS_DEBOUNCE_TIMEOUT);
26346 empty_color_request_handle.next().await.unwrap();
26347 cx.run_until_parked();
26348 assert_eq!(
26349 3,
26350 requests_made.load(atomic::Ordering::Acquire),
26351 "Should query for colors once per save only, as formatting was not requested"
26352 );
26353 editor.update(cx, |editor, cx| {
26354 assert_eq!(
26355 Vec::<Rgba>::new(),
26356 extract_color_inlays(editor, cx),
26357 "Should clear all colors when the server returns an empty response"
26358 );
26359 });
26360}
26361
26362#[gpui::test]
26363async fn test_newline_replacement_in_single_line(cx: &mut TestAppContext) {
26364 init_test(cx, |_| {});
26365 let (editor, cx) = cx.add_window_view(Editor::single_line);
26366 editor.update_in(cx, |editor, window, cx| {
26367 editor.set_text("oops\n\nwow\n", window, cx)
26368 });
26369 cx.run_until_parked();
26370 editor.update(cx, |editor, cx| {
26371 assert_eq!(editor.display_text(cx), "oops⋯⋯wow⋯");
26372 });
26373 editor.update(cx, |editor, cx| editor.edit([(3..5, "")], cx));
26374 cx.run_until_parked();
26375 editor.update(cx, |editor, cx| {
26376 assert_eq!(editor.display_text(cx), "oop⋯wow⋯");
26377 });
26378}
26379
26380#[gpui::test]
26381async fn test_non_utf_8_opens(cx: &mut TestAppContext) {
26382 init_test(cx, |_| {});
26383
26384 cx.update(|cx| {
26385 register_project_item::<Editor>(cx);
26386 });
26387
26388 let fs = FakeFs::new(cx.executor());
26389 fs.insert_tree("/root1", json!({})).await;
26390 fs.insert_file("/root1/one.pdf", vec![0xff, 0xfe, 0xfd])
26391 .await;
26392
26393 let project = Project::test(fs, ["/root1".as_ref()], cx).await;
26394 let (workspace, cx) =
26395 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
26396
26397 let worktree_id = project.update(cx, |project, cx| {
26398 project.worktrees(cx).next().unwrap().read(cx).id()
26399 });
26400
26401 let handle = workspace
26402 .update_in(cx, |workspace, window, cx| {
26403 let project_path = (worktree_id, rel_path("one.pdf"));
26404 workspace.open_path(project_path, None, true, window, cx)
26405 })
26406 .await
26407 .unwrap();
26408
26409 assert_eq!(
26410 handle.to_any().entity_type(),
26411 TypeId::of::<InvalidItemView>()
26412 );
26413}
26414
26415#[gpui::test]
26416async fn test_select_next_prev_syntax_node(cx: &mut TestAppContext) {
26417 init_test(cx, |_| {});
26418
26419 let language = Arc::new(Language::new(
26420 LanguageConfig::default(),
26421 Some(tree_sitter_rust::LANGUAGE.into()),
26422 ));
26423
26424 // Test hierarchical sibling navigation
26425 let text = r#"
26426 fn outer() {
26427 if condition {
26428 let a = 1;
26429 }
26430 let b = 2;
26431 }
26432
26433 fn another() {
26434 let c = 3;
26435 }
26436 "#;
26437
26438 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
26439 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
26440 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
26441
26442 // Wait for parsing to complete
26443 editor
26444 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
26445 .await;
26446
26447 editor.update_in(cx, |editor, window, cx| {
26448 // Start by selecting "let a = 1;" inside the if block
26449 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26450 s.select_display_ranges([
26451 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 26)
26452 ]);
26453 });
26454
26455 let initial_selection = editor
26456 .selections
26457 .display_ranges(&editor.display_snapshot(cx));
26458 assert_eq!(initial_selection.len(), 1, "Should have one selection");
26459
26460 // Test select next sibling - should move up levels to find the next sibling
26461 // Since "let a = 1;" has no siblings in the if block, it should move up
26462 // to find "let b = 2;" which is a sibling of the if block
26463 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26464 let next_selection = editor
26465 .selections
26466 .display_ranges(&editor.display_snapshot(cx));
26467
26468 // Should have a selection and it should be different from the initial
26469 assert_eq!(
26470 next_selection.len(),
26471 1,
26472 "Should have one selection after next"
26473 );
26474 assert_ne!(
26475 next_selection[0], initial_selection[0],
26476 "Next sibling selection should be different"
26477 );
26478
26479 // Test hierarchical navigation by going to the end of the current function
26480 // and trying to navigate to the next function
26481 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26482 s.select_display_ranges([
26483 DisplayPoint::new(DisplayRow(5), 12)..DisplayPoint::new(DisplayRow(5), 22)
26484 ]);
26485 });
26486
26487 editor.select_next_syntax_node(&SelectNextSyntaxNode, window, cx);
26488 let function_next_selection = editor
26489 .selections
26490 .display_ranges(&editor.display_snapshot(cx));
26491
26492 // Should move to the next function
26493 assert_eq!(
26494 function_next_selection.len(),
26495 1,
26496 "Should have one selection after function next"
26497 );
26498
26499 // Test select previous sibling navigation
26500 editor.select_prev_syntax_node(&SelectPreviousSyntaxNode, window, cx);
26501 let prev_selection = editor
26502 .selections
26503 .display_ranges(&editor.display_snapshot(cx));
26504
26505 // Should have a selection and it should be different
26506 assert_eq!(
26507 prev_selection.len(),
26508 1,
26509 "Should have one selection after prev"
26510 );
26511 assert_ne!(
26512 prev_selection[0], function_next_selection[0],
26513 "Previous sibling selection should be different from next"
26514 );
26515 });
26516}
26517
26518#[gpui::test]
26519async fn test_next_prev_document_highlight(cx: &mut TestAppContext) {
26520 init_test(cx, |_| {});
26521
26522 let mut cx = EditorTestContext::new(cx).await;
26523 cx.set_state(
26524 "let ˇvariable = 42;
26525let another = variable + 1;
26526let result = variable * 2;",
26527 );
26528
26529 // Set up document highlights manually (simulating LSP response)
26530 cx.update_editor(|editor, _window, cx| {
26531 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
26532
26533 // Create highlights for "variable" occurrences
26534 let highlight_ranges = [
26535 Point::new(0, 4)..Point::new(0, 12), // First "variable"
26536 Point::new(1, 14)..Point::new(1, 22), // Second "variable"
26537 Point::new(2, 13)..Point::new(2, 21), // Third "variable"
26538 ];
26539
26540 let anchor_ranges: Vec<_> = highlight_ranges
26541 .iter()
26542 .map(|range| range.clone().to_anchors(&buffer_snapshot))
26543 .collect();
26544
26545 editor.highlight_background::<DocumentHighlightRead>(
26546 &anchor_ranges,
26547 |theme| theme.colors().editor_document_highlight_read_background,
26548 cx,
26549 );
26550 });
26551
26552 // Go to next highlight - should move to second "variable"
26553 cx.update_editor(|editor, window, cx| {
26554 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26555 });
26556 cx.assert_editor_state(
26557 "let variable = 42;
26558let another = ˇvariable + 1;
26559let result = variable * 2;",
26560 );
26561
26562 // Go to next highlight - should move to third "variable"
26563 cx.update_editor(|editor, window, cx| {
26564 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26565 });
26566 cx.assert_editor_state(
26567 "let variable = 42;
26568let another = variable + 1;
26569let result = ˇvariable * 2;",
26570 );
26571
26572 // Go to next highlight - should stay at third "variable" (no wrap-around)
26573 cx.update_editor(|editor, window, cx| {
26574 editor.go_to_next_document_highlight(&GoToNextDocumentHighlight, window, cx);
26575 });
26576 cx.assert_editor_state(
26577 "let variable = 42;
26578let another = variable + 1;
26579let result = ˇvariable * 2;",
26580 );
26581
26582 // Now test going backwards from third position
26583 cx.update_editor(|editor, window, cx| {
26584 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26585 });
26586 cx.assert_editor_state(
26587 "let variable = 42;
26588let another = ˇvariable + 1;
26589let result = variable * 2;",
26590 );
26591
26592 // Go to previous highlight - should move to first "variable"
26593 cx.update_editor(|editor, window, cx| {
26594 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26595 });
26596 cx.assert_editor_state(
26597 "let ˇvariable = 42;
26598let another = variable + 1;
26599let result = variable * 2;",
26600 );
26601
26602 // Go to previous highlight - should stay on first "variable"
26603 cx.update_editor(|editor, window, cx| {
26604 editor.go_to_prev_document_highlight(&GoToPreviousDocumentHighlight, window, cx);
26605 });
26606 cx.assert_editor_state(
26607 "let ˇvariable = 42;
26608let another = variable + 1;
26609let result = variable * 2;",
26610 );
26611}
26612
26613#[gpui::test]
26614async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
26615 cx: &mut gpui::TestAppContext,
26616) {
26617 init_test(cx, |_| {});
26618
26619 let url = "https://zed.dev";
26620
26621 let markdown_language = Arc::new(Language::new(
26622 LanguageConfig {
26623 name: "Markdown".into(),
26624 ..LanguageConfig::default()
26625 },
26626 None,
26627 ));
26628
26629 let mut cx = EditorTestContext::new(cx).await;
26630 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26631 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)");
26632
26633 cx.update_editor(|editor, window, cx| {
26634 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26635 editor.paste(&Paste, window, cx);
26636 });
26637
26638 cx.assert_editor_state(&format!(
26639 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)"
26640 ));
26641}
26642
26643#[gpui::test]
26644async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
26645 cx: &mut gpui::TestAppContext,
26646) {
26647 init_test(cx, |_| {});
26648
26649 let url = "https://zed.dev";
26650
26651 let markdown_language = Arc::new(Language::new(
26652 LanguageConfig {
26653 name: "Markdown".into(),
26654 ..LanguageConfig::default()
26655 },
26656 None,
26657 ));
26658
26659 let mut cx = EditorTestContext::new(cx).await;
26660 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26661 cx.set_state(&format!(
26662 "Hello, editor.\nZed is great (see this link: )\n«{url}ˇ»"
26663 ));
26664
26665 cx.update_editor(|editor, window, cx| {
26666 editor.copy(&Copy, window, cx);
26667 });
26668
26669 cx.set_state(&format!(
26670 "Hello, «editorˇ».\nZed is «ˇgreat» (see this link: ˇ)\n{url}"
26671 ));
26672
26673 cx.update_editor(|editor, window, cx| {
26674 editor.paste(&Paste, window, cx);
26675 });
26676
26677 cx.assert_editor_state(&format!(
26678 "Hello, [editor]({url})ˇ.\nZed is [great]({url})ˇ (see this link: {url}ˇ)\n{url}"
26679 ));
26680}
26681
26682#[gpui::test]
26683async fn test_paste_url_from_other_app_replaces_existing_url_without_creating_markdown_link(
26684 cx: &mut gpui::TestAppContext,
26685) {
26686 init_test(cx, |_| {});
26687
26688 let url = "https://zed.dev";
26689
26690 let markdown_language = Arc::new(Language::new(
26691 LanguageConfig {
26692 name: "Markdown".into(),
26693 ..LanguageConfig::default()
26694 },
26695 None,
26696 ));
26697
26698 let mut cx = EditorTestContext::new(cx).await;
26699 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26700 cx.set_state("Please visit zed's homepage: «https://www.apple.comˇ»");
26701
26702 cx.update_editor(|editor, window, cx| {
26703 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26704 editor.paste(&Paste, window, cx);
26705 });
26706
26707 cx.assert_editor_state(&format!("Please visit zed's homepage: {url}ˇ"));
26708}
26709
26710#[gpui::test]
26711async fn test_paste_plain_text_from_other_app_replaces_selection_without_creating_markdown_link(
26712 cx: &mut gpui::TestAppContext,
26713) {
26714 init_test(cx, |_| {});
26715
26716 let text = "Awesome";
26717
26718 let markdown_language = Arc::new(Language::new(
26719 LanguageConfig {
26720 name: "Markdown".into(),
26721 ..LanguageConfig::default()
26722 },
26723 None,
26724 ));
26725
26726 let mut cx = EditorTestContext::new(cx).await;
26727 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26728 cx.set_state("Hello, «editorˇ».\nZed is «ˇgreat»");
26729
26730 cx.update_editor(|editor, window, cx| {
26731 cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
26732 editor.paste(&Paste, window, cx);
26733 });
26734
26735 cx.assert_editor_state(&format!("Hello, {text}ˇ.\nZed is {text}ˇ"));
26736}
26737
26738#[gpui::test]
26739async fn test_paste_url_from_other_app_without_creating_markdown_link_in_non_markdown_language(
26740 cx: &mut gpui::TestAppContext,
26741) {
26742 init_test(cx, |_| {});
26743
26744 let url = "https://zed.dev";
26745
26746 let markdown_language = Arc::new(Language::new(
26747 LanguageConfig {
26748 name: "Rust".into(),
26749 ..LanguageConfig::default()
26750 },
26751 None,
26752 ));
26753
26754 let mut cx = EditorTestContext::new(cx).await;
26755 cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
26756 cx.set_state("// Hello, «editorˇ».\n// Zed is «ˇgreat» (see this link: ˇ)");
26757
26758 cx.update_editor(|editor, window, cx| {
26759 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26760 editor.paste(&Paste, window, cx);
26761 });
26762
26763 cx.assert_editor_state(&format!(
26764 "// Hello, {url}ˇ.\n// Zed is {url}ˇ (see this link: {url}ˇ)"
26765 ));
26766}
26767
26768#[gpui::test]
26769async fn test_paste_url_from_other_app_creates_markdown_link_selectively_in_multi_buffer(
26770 cx: &mut TestAppContext,
26771) {
26772 init_test(cx, |_| {});
26773
26774 let url = "https://zed.dev";
26775
26776 let markdown_language = Arc::new(Language::new(
26777 LanguageConfig {
26778 name: "Markdown".into(),
26779 ..LanguageConfig::default()
26780 },
26781 None,
26782 ));
26783
26784 let (editor, cx) = cx.add_window_view(|window, cx| {
26785 let multi_buffer = MultiBuffer::build_multi(
26786 [
26787 ("this will embed -> link", vec![Point::row_range(0..1)]),
26788 ("this will replace -> link", vec![Point::row_range(0..1)]),
26789 ],
26790 cx,
26791 );
26792 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
26793 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26794 s.select_ranges(vec![
26795 Point::new(0, 19)..Point::new(0, 23),
26796 Point::new(1, 21)..Point::new(1, 25),
26797 ])
26798 });
26799 let first_buffer_id = multi_buffer
26800 .read(cx)
26801 .excerpt_buffer_ids()
26802 .into_iter()
26803 .next()
26804 .unwrap();
26805 let first_buffer = multi_buffer.read(cx).buffer(first_buffer_id).unwrap();
26806 first_buffer.update(cx, |buffer, cx| {
26807 buffer.set_language(Some(markdown_language.clone()), cx);
26808 });
26809
26810 editor
26811 });
26812 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
26813
26814 cx.update_editor(|editor, window, cx| {
26815 cx.write_to_clipboard(ClipboardItem::new_string(url.to_string()));
26816 editor.paste(&Paste, window, cx);
26817 });
26818
26819 cx.assert_editor_state(&format!(
26820 "this will embed -> [link]({url})ˇ\nthis will replace -> {url}ˇ"
26821 ));
26822}
26823
26824#[gpui::test]
26825async fn test_race_in_multibuffer_save(cx: &mut TestAppContext) {
26826 init_test(cx, |_| {});
26827
26828 let fs = FakeFs::new(cx.executor());
26829 fs.insert_tree(
26830 path!("/project"),
26831 json!({
26832 "first.rs": "# First Document\nSome content here.",
26833 "second.rs": "Plain text content for second file.",
26834 }),
26835 )
26836 .await;
26837
26838 let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
26839 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
26840 let cx = &mut VisualTestContext::from_window(*workspace, cx);
26841
26842 let language = rust_lang();
26843 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
26844 language_registry.add(language.clone());
26845 let mut fake_servers = language_registry.register_fake_lsp(
26846 "Rust",
26847 FakeLspAdapter {
26848 ..FakeLspAdapter::default()
26849 },
26850 );
26851
26852 let buffer1 = project
26853 .update(cx, |project, cx| {
26854 project.open_local_buffer(PathBuf::from(path!("/project/first.rs")), cx)
26855 })
26856 .await
26857 .unwrap();
26858 let buffer2 = project
26859 .update(cx, |project, cx| {
26860 project.open_local_buffer(PathBuf::from(path!("/project/second.rs")), cx)
26861 })
26862 .await
26863 .unwrap();
26864
26865 let multi_buffer = cx.new(|cx| {
26866 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
26867 multi_buffer.set_excerpts_for_path(
26868 PathKey::for_buffer(&buffer1, cx),
26869 buffer1.clone(),
26870 [Point::zero()..buffer1.read(cx).max_point()],
26871 3,
26872 cx,
26873 );
26874 multi_buffer.set_excerpts_for_path(
26875 PathKey::for_buffer(&buffer2, cx),
26876 buffer2.clone(),
26877 [Point::zero()..buffer1.read(cx).max_point()],
26878 3,
26879 cx,
26880 );
26881 multi_buffer
26882 });
26883
26884 let (editor, cx) = cx.add_window_view(|window, cx| {
26885 Editor::new(
26886 EditorMode::full(),
26887 multi_buffer,
26888 Some(project.clone()),
26889 window,
26890 cx,
26891 )
26892 });
26893
26894 let fake_language_server = fake_servers.next().await.unwrap();
26895
26896 buffer1.update(cx, |buffer, cx| buffer.edit([(0..0, "hello!")], None, cx));
26897
26898 let save = editor.update_in(cx, |editor, window, cx| {
26899 assert!(editor.is_dirty(cx));
26900
26901 editor.save(
26902 SaveOptions {
26903 format: true,
26904 autosave: true,
26905 },
26906 project,
26907 window,
26908 cx,
26909 )
26910 });
26911 let (start_edit_tx, start_edit_rx) = oneshot::channel();
26912 let (done_edit_tx, done_edit_rx) = oneshot::channel();
26913 let mut done_edit_rx = Some(done_edit_rx);
26914 let mut start_edit_tx = Some(start_edit_tx);
26915
26916 fake_language_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| {
26917 start_edit_tx.take().unwrap().send(()).unwrap();
26918 let done_edit_rx = done_edit_rx.take().unwrap();
26919 async move {
26920 done_edit_rx.await.unwrap();
26921 Ok(None)
26922 }
26923 });
26924
26925 start_edit_rx.await.unwrap();
26926 buffer2
26927 .update(cx, |buffer, cx| buffer.edit([(0..0, "world!")], None, cx))
26928 .unwrap();
26929
26930 done_edit_tx.send(()).unwrap();
26931
26932 save.await.unwrap();
26933 cx.update(|_, cx| assert!(editor.is_dirty(cx)));
26934}
26935
26936#[track_caller]
26937fn extract_color_inlays(editor: &Editor, cx: &App) -> Vec<Rgba> {
26938 editor
26939 .all_inlays(cx)
26940 .into_iter()
26941 .filter_map(|inlay| inlay.get_color())
26942 .map(Rgba::from)
26943 .collect()
26944}
26945
26946#[gpui::test]
26947fn test_duplicate_line_up_on_last_line_without_newline(cx: &mut TestAppContext) {
26948 init_test(cx, |_| {});
26949
26950 let editor = cx.add_window(|window, cx| {
26951 let buffer = MultiBuffer::build_simple("line1\nline2", cx);
26952 build_editor(buffer, window, cx)
26953 });
26954
26955 editor
26956 .update(cx, |editor, window, cx| {
26957 editor.change_selections(SelectionEffects::no_scroll(), window, cx, |s| {
26958 s.select_display_ranges([
26959 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
26960 ])
26961 });
26962
26963 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
26964
26965 assert_eq!(
26966 editor.display_text(cx),
26967 "line1\nline2\nline2",
26968 "Duplicating last line upward should create duplicate above, not on same line"
26969 );
26970
26971 assert_eq!(
26972 editor
26973 .selections
26974 .display_ranges(&editor.display_snapshot(cx)),
26975 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)],
26976 "Selection should move to the duplicated line"
26977 );
26978 })
26979 .unwrap();
26980}
26981
26982#[gpui::test]
26983async fn test_copy_line_without_trailing_newline(cx: &mut TestAppContext) {
26984 init_test(cx, |_| {});
26985
26986 let mut cx = EditorTestContext::new(cx).await;
26987
26988 cx.set_state("line1\nline2ˇ");
26989
26990 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
26991
26992 let clipboard_text = cx
26993 .read_from_clipboard()
26994 .and_then(|item| item.text().as_deref().map(str::to_string));
26995
26996 assert_eq!(
26997 clipboard_text,
26998 Some("line2\n".to_string()),
26999 "Copying a line without trailing newline should include a newline"
27000 );
27001
27002 cx.set_state("line1\nˇ");
27003
27004 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
27005
27006 cx.assert_editor_state("line1\nline2\nˇ");
27007}
27008
27009#[gpui::test]
27010async fn test_end_of_editor_context(cx: &mut TestAppContext) {
27011 init_test(cx, |_| {});
27012
27013 let mut cx = EditorTestContext::new(cx).await;
27014
27015 cx.set_state("line1\nline2ˇ");
27016 cx.update_editor(|e, window, cx| {
27017 e.set_mode(EditorMode::SingleLine);
27018 assert!(e.key_context(window, cx).contains("end_of_input"));
27019 });
27020 cx.set_state("ˇline1\nline2");
27021 cx.update_editor(|e, window, cx| {
27022 assert!(!e.key_context(window, cx).contains("end_of_input"));
27023 });
27024 cx.set_state("line1ˇ\nline2");
27025 cx.update_editor(|e, window, cx| {
27026 assert!(!e.key_context(window, cx).contains("end_of_input"));
27027 });
27028}
27029
27030#[gpui::test]
27031async fn test_sticky_scroll(cx: &mut TestAppContext) {
27032 init_test(cx, |_| {});
27033 let mut cx = EditorTestContext::new(cx).await;
27034
27035 let buffer = indoc! {"
27036 ˇfn foo() {
27037 let abc = 123;
27038 }
27039 struct Bar;
27040 impl Bar {
27041 fn new() -> Self {
27042 Self
27043 }
27044 }
27045 fn baz() {
27046 }
27047 "};
27048 cx.set_state(&buffer);
27049
27050 cx.update_editor(|e, _, cx| {
27051 e.buffer()
27052 .read(cx)
27053 .as_singleton()
27054 .unwrap()
27055 .update(cx, |buffer, cx| {
27056 buffer.set_language(Some(rust_lang()), cx);
27057 })
27058 });
27059
27060 let mut sticky_headers = |offset: ScrollOffset| {
27061 cx.update_editor(|e, window, cx| {
27062 e.scroll(gpui::Point { x: 0., y: offset }, None, window, cx);
27063 EditorElement::sticky_headers(&e, &e.snapshot(window, cx), cx)
27064 .into_iter()
27065 .map(
27066 |StickyHeader {
27067 start_point,
27068 offset,
27069 ..
27070 }| { (start_point, offset) },
27071 )
27072 .collect::<Vec<_>>()
27073 })
27074 };
27075
27076 let fn_foo = Point { row: 0, column: 0 };
27077 let impl_bar = Point { row: 4, column: 0 };
27078 let fn_new = Point { row: 5, column: 4 };
27079
27080 assert_eq!(sticky_headers(0.0), vec![]);
27081 assert_eq!(sticky_headers(0.5), vec![(fn_foo, 0.0)]);
27082 assert_eq!(sticky_headers(1.0), vec![(fn_foo, 0.0)]);
27083 assert_eq!(sticky_headers(1.5), vec![(fn_foo, -0.5)]);
27084 assert_eq!(sticky_headers(2.0), vec![]);
27085 assert_eq!(sticky_headers(2.5), vec![]);
27086 assert_eq!(sticky_headers(3.0), vec![]);
27087 assert_eq!(sticky_headers(3.5), vec![]);
27088 assert_eq!(sticky_headers(4.0), vec![]);
27089 assert_eq!(sticky_headers(4.5), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27090 assert_eq!(sticky_headers(5.0), vec![(impl_bar, 0.0), (fn_new, 1.0)]);
27091 assert_eq!(sticky_headers(5.5), vec![(impl_bar, 0.0), (fn_new, 0.5)]);
27092 assert_eq!(sticky_headers(6.0), vec![(impl_bar, 0.0)]);
27093 assert_eq!(sticky_headers(6.5), vec![(impl_bar, 0.0)]);
27094 assert_eq!(sticky_headers(7.0), vec![(impl_bar, 0.0)]);
27095 assert_eq!(sticky_headers(7.5), vec![(impl_bar, -0.5)]);
27096 assert_eq!(sticky_headers(8.0), vec![]);
27097 assert_eq!(sticky_headers(8.5), vec![]);
27098 assert_eq!(sticky_headers(9.0), vec![]);
27099 assert_eq!(sticky_headers(9.5), vec![]);
27100 assert_eq!(sticky_headers(10.0), vec![]);
27101}
27102
27103#[gpui::test]
27104async fn test_scroll_by_clicking_sticky_header(cx: &mut TestAppContext) {
27105 init_test(cx, |_| {});
27106 cx.update(|cx| {
27107 SettingsStore::update_global(cx, |store, cx| {
27108 store.update_user_settings(cx, |settings| {
27109 settings.editor.sticky_scroll = Some(settings::StickyScrollContent {
27110 enabled: Some(true),
27111 })
27112 });
27113 });
27114 });
27115 let mut cx = EditorTestContext::new(cx).await;
27116
27117 let line_height = cx.editor(|editor, window, _cx| {
27118 editor
27119 .style()
27120 .unwrap()
27121 .text
27122 .line_height_in_pixels(window.rem_size())
27123 });
27124
27125 let buffer = indoc! {"
27126 ˇfn foo() {
27127 let abc = 123;
27128 }
27129 struct Bar;
27130 impl Bar {
27131 fn new() -> Self {
27132 Self
27133 }
27134 }
27135 fn baz() {
27136 }
27137 "};
27138 cx.set_state(&buffer);
27139
27140 cx.update_editor(|e, _, cx| {
27141 e.buffer()
27142 .read(cx)
27143 .as_singleton()
27144 .unwrap()
27145 .update(cx, |buffer, cx| {
27146 buffer.set_language(Some(rust_lang()), cx);
27147 })
27148 });
27149
27150 let fn_foo = || empty_range(0, 0);
27151 let impl_bar = || empty_range(4, 0);
27152 let fn_new = || empty_range(5, 4);
27153
27154 let mut scroll_and_click = |scroll_offset: ScrollOffset, click_offset: ScrollOffset| {
27155 cx.update_editor(|e, window, cx| {
27156 e.scroll(
27157 gpui::Point {
27158 x: 0.,
27159 y: scroll_offset,
27160 },
27161 None,
27162 window,
27163 cx,
27164 );
27165 });
27166 cx.simulate_click(
27167 gpui::Point {
27168 x: px(0.),
27169 y: click_offset as f32 * line_height,
27170 },
27171 Modifiers::none(),
27172 );
27173 cx.update_editor(|e, _, cx| (e.scroll_position(cx), display_ranges(e, cx)))
27174 };
27175
27176 assert_eq!(
27177 scroll_and_click(
27178 4.5, // impl Bar is halfway off the screen
27179 0.0 // click top of screen
27180 ),
27181 // scrolled to impl Bar
27182 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27183 );
27184
27185 assert_eq!(
27186 scroll_and_click(
27187 4.5, // impl Bar is halfway off the screen
27188 0.25 // click middle of impl Bar
27189 ),
27190 // scrolled to impl Bar
27191 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27192 );
27193
27194 assert_eq!(
27195 scroll_and_click(
27196 4.5, // impl Bar is halfway off the screen
27197 1.5 // click below impl Bar (e.g. fn new())
27198 ),
27199 // scrolled to fn new() - this is below the impl Bar header which has persisted
27200 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27201 );
27202
27203 assert_eq!(
27204 scroll_and_click(
27205 5.5, // fn new is halfway underneath impl Bar
27206 0.75 // click on the overlap of impl Bar and fn new()
27207 ),
27208 (gpui::Point { x: 0., y: 4. }, vec![impl_bar()])
27209 );
27210
27211 assert_eq!(
27212 scroll_and_click(
27213 5.5, // fn new is halfway underneath impl Bar
27214 1.25 // click on the visible part of fn new()
27215 ),
27216 (gpui::Point { x: 0., y: 4. }, vec![fn_new()])
27217 );
27218
27219 assert_eq!(
27220 scroll_and_click(
27221 1.5, // fn foo is halfway off the screen
27222 0.0 // click top of screen
27223 ),
27224 (gpui::Point { x: 0., y: 0. }, vec![fn_foo()])
27225 );
27226
27227 assert_eq!(
27228 scroll_and_click(
27229 1.5, // fn foo is halfway off the screen
27230 0.75 // click visible part of let abc...
27231 )
27232 .0,
27233 // no change in scroll
27234 // we don't assert on the visible_range because if we clicked the gutter, our line is fully selected
27235 (gpui::Point { x: 0., y: 1.5 })
27236 );
27237}
27238
27239#[gpui::test]
27240async fn test_next_prev_reference(cx: &mut TestAppContext) {
27241 const CYCLE_POSITIONS: &[&'static str] = &[
27242 indoc! {"
27243 fn foo() {
27244 let ˇabc = 123;
27245 let x = abc + 1;
27246 let y = abc + 2;
27247 let z = abc + 2;
27248 }
27249 "},
27250 indoc! {"
27251 fn foo() {
27252 let abc = 123;
27253 let x = ˇabc + 1;
27254 let y = abc + 2;
27255 let z = abc + 2;
27256 }
27257 "},
27258 indoc! {"
27259 fn foo() {
27260 let abc = 123;
27261 let x = abc + 1;
27262 let y = ˇabc + 2;
27263 let z = abc + 2;
27264 }
27265 "},
27266 indoc! {"
27267 fn foo() {
27268 let abc = 123;
27269 let x = abc + 1;
27270 let y = abc + 2;
27271 let z = ˇabc + 2;
27272 }
27273 "},
27274 ];
27275
27276 init_test(cx, |_| {});
27277
27278 let mut cx = EditorLspTestContext::new_rust(
27279 lsp::ServerCapabilities {
27280 references_provider: Some(lsp::OneOf::Left(true)),
27281 ..Default::default()
27282 },
27283 cx,
27284 )
27285 .await;
27286
27287 // importantly, the cursor is in the middle
27288 cx.set_state(indoc! {"
27289 fn foo() {
27290 let aˇbc = 123;
27291 let x = abc + 1;
27292 let y = abc + 2;
27293 let z = abc + 2;
27294 }
27295 "});
27296
27297 let reference_ranges = [
27298 lsp::Position::new(1, 8),
27299 lsp::Position::new(2, 12),
27300 lsp::Position::new(3, 12),
27301 lsp::Position::new(4, 12),
27302 ]
27303 .map(|start| lsp::Range::new(start, lsp::Position::new(start.line, start.character + 3)));
27304
27305 cx.lsp
27306 .set_request_handler::<lsp::request::References, _, _>(move |params, _cx| async move {
27307 Ok(Some(
27308 reference_ranges
27309 .map(|range| lsp::Location {
27310 uri: params.text_document_position.text_document.uri.clone(),
27311 range,
27312 })
27313 .to_vec(),
27314 ))
27315 });
27316
27317 let _move = async |direction, count, cx: &mut EditorLspTestContext| {
27318 cx.update_editor(|editor, window, cx| {
27319 editor.go_to_reference_before_or_after_position(direction, count, window, cx)
27320 })
27321 .unwrap()
27322 .await
27323 .unwrap()
27324 };
27325
27326 _move(Direction::Next, 1, &mut cx).await;
27327 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27328
27329 _move(Direction::Next, 1, &mut cx).await;
27330 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27331
27332 _move(Direction::Next, 1, &mut cx).await;
27333 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27334
27335 // loops back to the start
27336 _move(Direction::Next, 1, &mut cx).await;
27337 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27338
27339 // loops back to the end
27340 _move(Direction::Prev, 1, &mut cx).await;
27341 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27342
27343 _move(Direction::Prev, 1, &mut cx).await;
27344 cx.assert_editor_state(CYCLE_POSITIONS[2]);
27345
27346 _move(Direction::Prev, 1, &mut cx).await;
27347 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27348
27349 _move(Direction::Prev, 1, &mut cx).await;
27350 cx.assert_editor_state(CYCLE_POSITIONS[0]);
27351
27352 _move(Direction::Next, 3, &mut cx).await;
27353 cx.assert_editor_state(CYCLE_POSITIONS[3]);
27354
27355 _move(Direction::Prev, 2, &mut cx).await;
27356 cx.assert_editor_state(CYCLE_POSITIONS[1]);
27357}