1use super::*;
2use crate::{
3 JoinLines,
4 inline_completion_tests::FakeInlineCompletionProvider,
5 linked_editing_ranges::LinkedEditingRanges,
6 scroll::scroll_amount::ScrollAmount,
7 test::{
8 assert_text_with_selections, build_editor,
9 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
10 editor_test_context::EditorTestContext,
11 select_ranges,
12 },
13};
14use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
15use futures::StreamExt;
16use gpui::{
17 BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
18 VisualTestContext, WindowBounds, WindowOptions, div,
19};
20use indoc::indoc;
21use language::{
22 BracketPairConfig,
23 Capability::ReadWrite,
24 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
25 Override, Point,
26 language_settings::{
27 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
28 LanguageSettingsContent, LspInsertMode, PrettierSettings,
29 },
30 tree_sitter_python,
31};
32use language_settings::{Formatter, FormatterList, IndentGuideSettings};
33use lsp::CompletionParams;
34use multi_buffer::{IndentGuide, PathKey};
35use parking_lot::Mutex;
36use pretty_assertions::{assert_eq, assert_ne};
37use project::{
38 FakeFs,
39 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
40 project_settings::{LspSettings, ProjectSettings},
41};
42use serde_json::{self, json};
43use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
44use std::{
45 iter,
46 sync::atomic::{self, AtomicUsize},
47};
48use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
49use text::ToPoint as _;
50use unindent::Unindent;
51use util::{
52 assert_set_eq, path,
53 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
54 uri,
55};
56use workspace::{
57 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
58 item::{FollowEvent, FollowableItem, Item, ItemHandle},
59};
60
61#[gpui::test]
62fn test_edit_events(cx: &mut TestAppContext) {
63 init_test(cx, |_| {});
64
65 let buffer = cx.new(|cx| {
66 let mut buffer = language::Buffer::local("123456", cx);
67 buffer.set_group_interval(Duration::from_secs(1));
68 buffer
69 });
70
71 let events = Rc::new(RefCell::new(Vec::new()));
72 let editor1 = cx.add_window({
73 let events = events.clone();
74 |window, cx| {
75 let entity = cx.entity().clone();
76 cx.subscribe_in(
77 &entity,
78 window,
79 move |_, _, event: &EditorEvent, _, _| match event {
80 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
81 EditorEvent::BufferEdited => {
82 events.borrow_mut().push(("editor1", "buffer edited"))
83 }
84 _ => {}
85 },
86 )
87 .detach();
88 Editor::for_buffer(buffer.clone(), None, window, cx)
89 }
90 });
91
92 let editor2 = cx.add_window({
93 let events = events.clone();
94 |window, cx| {
95 cx.subscribe_in(
96 &cx.entity().clone(),
97 window,
98 move |_, _, event: &EditorEvent, _, _| match event {
99 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
100 EditorEvent::BufferEdited => {
101 events.borrow_mut().push(("editor2", "buffer edited"))
102 }
103 _ => {}
104 },
105 )
106 .detach();
107 Editor::for_buffer(buffer.clone(), None, window, cx)
108 }
109 });
110
111 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
112
113 // Mutating editor 1 will emit an `Edited` event only for that editor.
114 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
115 assert_eq!(
116 mem::take(&mut *events.borrow_mut()),
117 [
118 ("editor1", "edited"),
119 ("editor1", "buffer edited"),
120 ("editor2", "buffer edited"),
121 ]
122 );
123
124 // Mutating editor 2 will emit an `Edited` event only for that editor.
125 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
126 assert_eq!(
127 mem::take(&mut *events.borrow_mut()),
128 [
129 ("editor2", "edited"),
130 ("editor1", "buffer edited"),
131 ("editor2", "buffer edited"),
132 ]
133 );
134
135 // Undoing on editor 1 will emit an `Edited` event only for that editor.
136 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
137 assert_eq!(
138 mem::take(&mut *events.borrow_mut()),
139 [
140 ("editor1", "edited"),
141 ("editor1", "buffer edited"),
142 ("editor2", "buffer edited"),
143 ]
144 );
145
146 // Redoing on editor 1 will emit an `Edited` event only for that editor.
147 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
148 assert_eq!(
149 mem::take(&mut *events.borrow_mut()),
150 [
151 ("editor1", "edited"),
152 ("editor1", "buffer edited"),
153 ("editor2", "buffer edited"),
154 ]
155 );
156
157 // Undoing on editor 2 will emit an `Edited` event only for that editor.
158 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
159 assert_eq!(
160 mem::take(&mut *events.borrow_mut()),
161 [
162 ("editor2", "edited"),
163 ("editor1", "buffer edited"),
164 ("editor2", "buffer edited"),
165 ]
166 );
167
168 // Redoing on editor 2 will emit an `Edited` event only for that editor.
169 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
170 assert_eq!(
171 mem::take(&mut *events.borrow_mut()),
172 [
173 ("editor2", "edited"),
174 ("editor1", "buffer edited"),
175 ("editor2", "buffer edited"),
176 ]
177 );
178
179 // No event is emitted when the mutation is a no-op.
180 _ = editor2.update(cx, |editor, window, cx| {
181 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
182
183 editor.backspace(&Backspace, window, cx);
184 });
185 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
186}
187
188#[gpui::test]
189fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
190 init_test(cx, |_| {});
191
192 let mut now = Instant::now();
193 let group_interval = Duration::from_millis(1);
194 let buffer = cx.new(|cx| {
195 let mut buf = language::Buffer::local("123456", cx);
196 buf.set_group_interval(group_interval);
197 buf
198 });
199 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
200 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
201
202 _ = editor.update(cx, |editor, window, cx| {
203 editor.start_transaction_at(now, window, cx);
204 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
205
206 editor.insert("cd", window, cx);
207 editor.end_transaction_at(now, cx);
208 assert_eq!(editor.text(cx), "12cd56");
209 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
210
211 editor.start_transaction_at(now, window, cx);
212 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
213 editor.insert("e", window, cx);
214 editor.end_transaction_at(now, cx);
215 assert_eq!(editor.text(cx), "12cde6");
216 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
217
218 now += group_interval + Duration::from_millis(1);
219 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
220
221 // Simulate an edit in another editor
222 buffer.update(cx, |buffer, cx| {
223 buffer.start_transaction_at(now, cx);
224 buffer.edit([(0..1, "a")], None, cx);
225 buffer.edit([(1..1, "b")], None, cx);
226 buffer.end_transaction_at(now, cx);
227 });
228
229 assert_eq!(editor.text(cx), "ab2cde6");
230 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
231
232 // Last transaction happened past the group interval in a different editor.
233 // Undo it individually and don't restore selections.
234 editor.undo(&Undo, window, cx);
235 assert_eq!(editor.text(cx), "12cde6");
236 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
237
238 // First two transactions happened within the group interval in this editor.
239 // Undo them together and restore selections.
240 editor.undo(&Undo, window, cx);
241 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
242 assert_eq!(editor.text(cx), "123456");
243 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
244
245 // Redo the first two transactions together.
246 editor.redo(&Redo, window, cx);
247 assert_eq!(editor.text(cx), "12cde6");
248 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
249
250 // Redo the last transaction on its own.
251 editor.redo(&Redo, window, cx);
252 assert_eq!(editor.text(cx), "ab2cde6");
253 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
254
255 // Test empty transactions.
256 editor.start_transaction_at(now, window, cx);
257 editor.end_transaction_at(now, cx);
258 editor.undo(&Undo, window, cx);
259 assert_eq!(editor.text(cx), "12cde6");
260 });
261}
262
263#[gpui::test]
264fn test_ime_composition(cx: &mut TestAppContext) {
265 init_test(cx, |_| {});
266
267 let buffer = cx.new(|cx| {
268 let mut buffer = language::Buffer::local("abcde", cx);
269 // Ensure automatic grouping doesn't occur.
270 buffer.set_group_interval(Duration::ZERO);
271 buffer
272 });
273
274 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
275 cx.add_window(|window, cx| {
276 let mut editor = build_editor(buffer.clone(), window, cx);
277
278 // Start a new IME composition.
279 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
280 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
281 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
282 assert_eq!(editor.text(cx), "äbcde");
283 assert_eq!(
284 editor.marked_text_ranges(cx),
285 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
286 );
287
288 // Finalize IME composition.
289 editor.replace_text_in_range(None, "ā", window, cx);
290 assert_eq!(editor.text(cx), "ābcde");
291 assert_eq!(editor.marked_text_ranges(cx), None);
292
293 // IME composition edits are grouped and are undone/redone at once.
294 editor.undo(&Default::default(), window, cx);
295 assert_eq!(editor.text(cx), "abcde");
296 assert_eq!(editor.marked_text_ranges(cx), None);
297 editor.redo(&Default::default(), window, cx);
298 assert_eq!(editor.text(cx), "ābcde");
299 assert_eq!(editor.marked_text_ranges(cx), None);
300
301 // Start a new IME composition.
302 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
303 assert_eq!(
304 editor.marked_text_ranges(cx),
305 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
306 );
307
308 // Undoing during an IME composition cancels it.
309 editor.undo(&Default::default(), window, cx);
310 assert_eq!(editor.text(cx), "ābcde");
311 assert_eq!(editor.marked_text_ranges(cx), None);
312
313 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
314 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
315 assert_eq!(editor.text(cx), "ābcdè");
316 assert_eq!(
317 editor.marked_text_ranges(cx),
318 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
319 );
320
321 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
322 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
323 assert_eq!(editor.text(cx), "ābcdę");
324 assert_eq!(editor.marked_text_ranges(cx), None);
325
326 // Start a new IME composition with multiple cursors.
327 editor.change_selections(None, window, cx, |s| {
328 s.select_ranges([
329 OffsetUtf16(1)..OffsetUtf16(1),
330 OffsetUtf16(3)..OffsetUtf16(3),
331 OffsetUtf16(5)..OffsetUtf16(5),
332 ])
333 });
334 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
335 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
336 assert_eq!(
337 editor.marked_text_ranges(cx),
338 Some(vec![
339 OffsetUtf16(0)..OffsetUtf16(3),
340 OffsetUtf16(4)..OffsetUtf16(7),
341 OffsetUtf16(8)..OffsetUtf16(11)
342 ])
343 );
344
345 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
346 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
347 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
348 assert_eq!(
349 editor.marked_text_ranges(cx),
350 Some(vec![
351 OffsetUtf16(1)..OffsetUtf16(2),
352 OffsetUtf16(5)..OffsetUtf16(6),
353 OffsetUtf16(9)..OffsetUtf16(10)
354 ])
355 );
356
357 // Finalize IME composition with multiple cursors.
358 editor.replace_text_in_range(Some(9..10), "2", window, cx);
359 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
360 assert_eq!(editor.marked_text_ranges(cx), None);
361
362 editor
363 });
364}
365
366#[gpui::test]
367fn test_selection_with_mouse(cx: &mut TestAppContext) {
368 init_test(cx, |_| {});
369
370 let editor = cx.add_window(|window, cx| {
371 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
372 build_editor(buffer, window, cx)
373 });
374
375 _ = editor.update(cx, |editor, window, cx| {
376 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
377 });
378 assert_eq!(
379 editor
380 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
381 .unwrap(),
382 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
383 );
384
385 _ = editor.update(cx, |editor, window, cx| {
386 editor.update_selection(
387 DisplayPoint::new(DisplayRow(3), 3),
388 0,
389 gpui::Point::<f32>::default(),
390 window,
391 cx,
392 );
393 });
394
395 assert_eq!(
396 editor
397 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
398 .unwrap(),
399 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
400 );
401
402 _ = editor.update(cx, |editor, window, cx| {
403 editor.update_selection(
404 DisplayPoint::new(DisplayRow(1), 1),
405 0,
406 gpui::Point::<f32>::default(),
407 window,
408 cx,
409 );
410 });
411
412 assert_eq!(
413 editor
414 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
415 .unwrap(),
416 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
417 );
418
419 _ = editor.update(cx, |editor, window, cx| {
420 editor.end_selection(window, cx);
421 editor.update_selection(
422 DisplayPoint::new(DisplayRow(3), 3),
423 0,
424 gpui::Point::<f32>::default(),
425 window,
426 cx,
427 );
428 });
429
430 assert_eq!(
431 editor
432 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
433 .unwrap(),
434 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
435 );
436
437 _ = editor.update(cx, |editor, window, cx| {
438 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
439 editor.update_selection(
440 DisplayPoint::new(DisplayRow(0), 0),
441 0,
442 gpui::Point::<f32>::default(),
443 window,
444 cx,
445 );
446 });
447
448 assert_eq!(
449 editor
450 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
451 .unwrap(),
452 [
453 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
454 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
455 ]
456 );
457
458 _ = editor.update(cx, |editor, window, cx| {
459 editor.end_selection(window, cx);
460 });
461
462 assert_eq!(
463 editor
464 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
465 .unwrap(),
466 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
467 );
468}
469
470#[gpui::test]
471fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
472 init_test(cx, |_| {});
473
474 let editor = cx.add_window(|window, cx| {
475 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
476 build_editor(buffer, window, cx)
477 });
478
479 _ = editor.update(cx, |editor, window, cx| {
480 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
481 });
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.end_selection(window, cx);
485 });
486
487 _ = editor.update(cx, |editor, window, cx| {
488 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
489 });
490
491 _ = editor.update(cx, |editor, window, cx| {
492 editor.end_selection(window, cx);
493 });
494
495 assert_eq!(
496 editor
497 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
498 .unwrap(),
499 [
500 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
501 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
502 ]
503 );
504
505 _ = editor.update(cx, |editor, window, cx| {
506 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
507 });
508
509 _ = editor.update(cx, |editor, window, cx| {
510 editor.end_selection(window, cx);
511 });
512
513 assert_eq!(
514 editor
515 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
516 .unwrap(),
517 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
518 );
519}
520
521#[gpui::test]
522fn test_canceling_pending_selection(cx: &mut TestAppContext) {
523 init_test(cx, |_| {});
524
525 let editor = cx.add_window(|window, cx| {
526 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
527 build_editor(buffer, window, cx)
528 });
529
530 _ = editor.update(cx, |editor, window, cx| {
531 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
532 assert_eq!(
533 editor.selections.display_ranges(cx),
534 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
535 );
536 });
537
538 _ = editor.update(cx, |editor, window, cx| {
539 editor.update_selection(
540 DisplayPoint::new(DisplayRow(3), 3),
541 0,
542 gpui::Point::<f32>::default(),
543 window,
544 cx,
545 );
546 assert_eq!(
547 editor.selections.display_ranges(cx),
548 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
549 );
550 });
551
552 _ = editor.update(cx, |editor, window, cx| {
553 editor.cancel(&Cancel, window, cx);
554 editor.update_selection(
555 DisplayPoint::new(DisplayRow(1), 1),
556 0,
557 gpui::Point::<f32>::default(),
558 window,
559 cx,
560 );
561 assert_eq!(
562 editor.selections.display_ranges(cx),
563 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
564 );
565 });
566}
567
568#[gpui::test]
569fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
570 init_test(cx, |_| {});
571
572 let editor = cx.add_window(|window, cx| {
573 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
574 build_editor(buffer, window, cx)
575 });
576
577 _ = editor.update(cx, |editor, window, cx| {
578 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
579 assert_eq!(
580 editor.selections.display_ranges(cx),
581 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
582 );
583
584 editor.move_down(&Default::default(), window, cx);
585 assert_eq!(
586 editor.selections.display_ranges(cx),
587 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
588 );
589
590 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
591 assert_eq!(
592 editor.selections.display_ranges(cx),
593 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
594 );
595
596 editor.move_up(&Default::default(), window, cx);
597 assert_eq!(
598 editor.selections.display_ranges(cx),
599 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
600 );
601 });
602}
603
604#[gpui::test]
605fn test_clone(cx: &mut TestAppContext) {
606 init_test(cx, |_| {});
607
608 let (text, selection_ranges) = marked_text_ranges(
609 indoc! {"
610 one
611 two
612 threeˇ
613 four
614 fiveˇ
615 "},
616 true,
617 );
618
619 let editor = cx.add_window(|window, cx| {
620 let buffer = MultiBuffer::build_simple(&text, cx);
621 build_editor(buffer, window, cx)
622 });
623
624 _ = editor.update(cx, |editor, window, cx| {
625 editor.change_selections(None, window, cx, |s| {
626 s.select_ranges(selection_ranges.clone())
627 });
628 editor.fold_creases(
629 vec![
630 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
631 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
632 ],
633 true,
634 window,
635 cx,
636 );
637 });
638
639 let cloned_editor = editor
640 .update(cx, |editor, _, cx| {
641 cx.open_window(Default::default(), |window, cx| {
642 cx.new(|cx| editor.clone(window, cx))
643 })
644 })
645 .unwrap()
646 .unwrap();
647
648 let snapshot = editor
649 .update(cx, |e, window, cx| e.snapshot(window, cx))
650 .unwrap();
651 let cloned_snapshot = cloned_editor
652 .update(cx, |e, window, cx| e.snapshot(window, cx))
653 .unwrap();
654
655 assert_eq!(
656 cloned_editor
657 .update(cx, |e, _, cx| e.display_text(cx))
658 .unwrap(),
659 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
660 );
661 assert_eq!(
662 cloned_snapshot
663 .folds_in_range(0..text.len())
664 .collect::<Vec<_>>(),
665 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
666 );
667 assert_set_eq!(
668 cloned_editor
669 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
670 .unwrap(),
671 editor
672 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
673 .unwrap()
674 );
675 assert_set_eq!(
676 cloned_editor
677 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
678 .unwrap(),
679 editor
680 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
681 .unwrap()
682 );
683}
684
685#[gpui::test]
686async fn test_navigation_history(cx: &mut TestAppContext) {
687 init_test(cx, |_| {});
688
689 use workspace::item::Item;
690
691 let fs = FakeFs::new(cx.executor());
692 let project = Project::test(fs, [], cx).await;
693 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
694 let pane = workspace
695 .update(cx, |workspace, _, _| workspace.active_pane().clone())
696 .unwrap();
697
698 _ = workspace.update(cx, |_v, window, cx| {
699 cx.new(|cx| {
700 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
701 let mut editor = build_editor(buffer.clone(), window, cx);
702 let handle = cx.entity();
703 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
704
705 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
706 editor.nav_history.as_mut().unwrap().pop_backward(cx)
707 }
708
709 // Move the cursor a small distance.
710 // Nothing is added to the navigation history.
711 editor.change_selections(None, window, cx, |s| {
712 s.select_display_ranges([
713 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
714 ])
715 });
716 editor.change_selections(None, window, cx, |s| {
717 s.select_display_ranges([
718 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
719 ])
720 });
721 assert!(pop_history(&mut editor, cx).is_none());
722
723 // Move the cursor a large distance.
724 // The history can jump back to the previous position.
725 editor.change_selections(None, window, cx, |s| {
726 s.select_display_ranges([
727 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
728 ])
729 });
730 let nav_entry = pop_history(&mut editor, cx).unwrap();
731 editor.navigate(nav_entry.data.unwrap(), window, cx);
732 assert_eq!(nav_entry.item.id(), cx.entity_id());
733 assert_eq!(
734 editor.selections.display_ranges(cx),
735 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
736 );
737 assert!(pop_history(&mut editor, cx).is_none());
738
739 // Move the cursor a small distance via the mouse.
740 // Nothing is added to the navigation history.
741 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
742 editor.end_selection(window, cx);
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
746 );
747 assert!(pop_history(&mut editor, cx).is_none());
748
749 // Move the cursor a large distance via the mouse.
750 // The history can jump back to the previous position.
751 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
752 editor.end_selection(window, cx);
753 assert_eq!(
754 editor.selections.display_ranges(cx),
755 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
756 );
757 let nav_entry = pop_history(&mut editor, cx).unwrap();
758 editor.navigate(nav_entry.data.unwrap(), window, cx);
759 assert_eq!(nav_entry.item.id(), cx.entity_id());
760 assert_eq!(
761 editor.selections.display_ranges(cx),
762 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
763 );
764 assert!(pop_history(&mut editor, cx).is_none());
765
766 // Set scroll position to check later
767 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
768 let original_scroll_position = editor.scroll_manager.anchor();
769
770 // Jump to the end of the document and adjust scroll
771 editor.move_to_end(&MoveToEnd, window, cx);
772 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
773 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
774
775 let nav_entry = pop_history(&mut editor, cx).unwrap();
776 editor.navigate(nav_entry.data.unwrap(), window, cx);
777 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
778
779 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
780 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
781 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
782 let invalid_point = Point::new(9999, 0);
783 editor.navigate(
784 Box::new(NavigationData {
785 cursor_anchor: invalid_anchor,
786 cursor_position: invalid_point,
787 scroll_anchor: ScrollAnchor {
788 anchor: invalid_anchor,
789 offset: Default::default(),
790 },
791 scroll_top_row: invalid_point.row,
792 }),
793 window,
794 cx,
795 );
796 assert_eq!(
797 editor.selections.display_ranges(cx),
798 &[editor.max_point(cx)..editor.max_point(cx)]
799 );
800 assert_eq!(
801 editor.scroll_position(cx),
802 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
803 );
804
805 editor
806 })
807 });
808}
809
810#[gpui::test]
811fn test_cancel(cx: &mut TestAppContext) {
812 init_test(cx, |_| {});
813
814 let editor = cx.add_window(|window, cx| {
815 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
816 build_editor(buffer, window, cx)
817 });
818
819 _ = editor.update(cx, |editor, window, cx| {
820 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
821 editor.update_selection(
822 DisplayPoint::new(DisplayRow(1), 1),
823 0,
824 gpui::Point::<f32>::default(),
825 window,
826 cx,
827 );
828 editor.end_selection(window, cx);
829
830 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
831 editor.update_selection(
832 DisplayPoint::new(DisplayRow(0), 3),
833 0,
834 gpui::Point::<f32>::default(),
835 window,
836 cx,
837 );
838 editor.end_selection(window, cx);
839 assert_eq!(
840 editor.selections.display_ranges(cx),
841 [
842 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
843 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
844 ]
845 );
846 });
847
848 _ = editor.update(cx, |editor, window, cx| {
849 editor.cancel(&Cancel, window, cx);
850 assert_eq!(
851 editor.selections.display_ranges(cx),
852 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
853 );
854 });
855
856 _ = editor.update(cx, |editor, window, cx| {
857 editor.cancel(&Cancel, window, cx);
858 assert_eq!(
859 editor.selections.display_ranges(cx),
860 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
861 );
862 });
863}
864
865#[gpui::test]
866fn test_fold_action(cx: &mut TestAppContext) {
867 init_test(cx, |_| {});
868
869 let editor = cx.add_window(|window, cx| {
870 let buffer = MultiBuffer::build_simple(
871 &"
872 impl Foo {
873 // Hello!
874
875 fn a() {
876 1
877 }
878
879 fn b() {
880 2
881 }
882
883 fn c() {
884 3
885 }
886 }
887 "
888 .unindent(),
889 cx,
890 );
891 build_editor(buffer.clone(), window, cx)
892 });
893
894 _ = editor.update(cx, |editor, window, cx| {
895 editor.change_selections(None, window, cx, |s| {
896 s.select_display_ranges([
897 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
898 ]);
899 });
900 editor.fold(&Fold, window, cx);
901 assert_eq!(
902 editor.display_text(cx),
903 "
904 impl Foo {
905 // Hello!
906
907 fn a() {
908 1
909 }
910
911 fn b() {⋯
912 }
913
914 fn c() {⋯
915 }
916 }
917 "
918 .unindent(),
919 );
920
921 editor.fold(&Fold, window, cx);
922 assert_eq!(
923 editor.display_text(cx),
924 "
925 impl Foo {⋯
926 }
927 "
928 .unindent(),
929 );
930
931 editor.unfold_lines(&UnfoldLines, window, cx);
932 assert_eq!(
933 editor.display_text(cx),
934 "
935 impl Foo {
936 // Hello!
937
938 fn a() {
939 1
940 }
941
942 fn b() {⋯
943 }
944
945 fn c() {⋯
946 }
947 }
948 "
949 .unindent(),
950 );
951
952 editor.unfold_lines(&UnfoldLines, window, cx);
953 assert_eq!(
954 editor.display_text(cx),
955 editor.buffer.read(cx).read(cx).text()
956 );
957 });
958}
959
960#[gpui::test]
961fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
962 init_test(cx, |_| {});
963
964 let editor = cx.add_window(|window, cx| {
965 let buffer = MultiBuffer::build_simple(
966 &"
967 class Foo:
968 # Hello!
969
970 def a():
971 print(1)
972
973 def b():
974 print(2)
975
976 def c():
977 print(3)
978 "
979 .unindent(),
980 cx,
981 );
982 build_editor(buffer.clone(), window, cx)
983 });
984
985 _ = editor.update(cx, |editor, window, cx| {
986 editor.change_selections(None, window, cx, |s| {
987 s.select_display_ranges([
988 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
989 ]);
990 });
991 editor.fold(&Fold, window, cx);
992 assert_eq!(
993 editor.display_text(cx),
994 "
995 class Foo:
996 # Hello!
997
998 def a():
999 print(1)
1000
1001 def b():⋯
1002
1003 def c():⋯
1004 "
1005 .unindent(),
1006 );
1007
1008 editor.fold(&Fold, window, cx);
1009 assert_eq!(
1010 editor.display_text(cx),
1011 "
1012 class Foo:⋯
1013 "
1014 .unindent(),
1015 );
1016
1017 editor.unfold_lines(&UnfoldLines, window, cx);
1018 assert_eq!(
1019 editor.display_text(cx),
1020 "
1021 class Foo:
1022 # Hello!
1023
1024 def a():
1025 print(1)
1026
1027 def b():⋯
1028
1029 def c():⋯
1030 "
1031 .unindent(),
1032 );
1033
1034 editor.unfold_lines(&UnfoldLines, window, cx);
1035 assert_eq!(
1036 editor.display_text(cx),
1037 editor.buffer.read(cx).read(cx).text()
1038 );
1039 });
1040}
1041
1042#[gpui::test]
1043fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1044 init_test(cx, |_| {});
1045
1046 let editor = cx.add_window(|window, cx| {
1047 let buffer = MultiBuffer::build_simple(
1048 &"
1049 class Foo:
1050 # Hello!
1051
1052 def a():
1053 print(1)
1054
1055 def b():
1056 print(2)
1057
1058
1059 def c():
1060 print(3)
1061
1062
1063 "
1064 .unindent(),
1065 cx,
1066 );
1067 build_editor(buffer.clone(), window, cx)
1068 });
1069
1070 _ = editor.update(cx, |editor, window, cx| {
1071 editor.change_selections(None, window, cx, |s| {
1072 s.select_display_ranges([
1073 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1074 ]);
1075 });
1076 editor.fold(&Fold, window, cx);
1077 assert_eq!(
1078 editor.display_text(cx),
1079 "
1080 class Foo:
1081 # Hello!
1082
1083 def a():
1084 print(1)
1085
1086 def b():⋯
1087
1088
1089 def c():⋯
1090
1091
1092 "
1093 .unindent(),
1094 );
1095
1096 editor.fold(&Fold, window, cx);
1097 assert_eq!(
1098 editor.display_text(cx),
1099 "
1100 class Foo:⋯
1101
1102
1103 "
1104 .unindent(),
1105 );
1106
1107 editor.unfold_lines(&UnfoldLines, window, cx);
1108 assert_eq!(
1109 editor.display_text(cx),
1110 "
1111 class Foo:
1112 # Hello!
1113
1114 def a():
1115 print(1)
1116
1117 def b():⋯
1118
1119
1120 def c():⋯
1121
1122
1123 "
1124 .unindent(),
1125 );
1126
1127 editor.unfold_lines(&UnfoldLines, window, cx);
1128 assert_eq!(
1129 editor.display_text(cx),
1130 editor.buffer.read(cx).read(cx).text()
1131 );
1132 });
1133}
1134
1135#[gpui::test]
1136fn test_fold_at_level(cx: &mut TestAppContext) {
1137 init_test(cx, |_| {});
1138
1139 let editor = cx.add_window(|window, cx| {
1140 let buffer = MultiBuffer::build_simple(
1141 &"
1142 class Foo:
1143 # Hello!
1144
1145 def a():
1146 print(1)
1147
1148 def b():
1149 print(2)
1150
1151
1152 class Bar:
1153 # World!
1154
1155 def a():
1156 print(1)
1157
1158 def b():
1159 print(2)
1160
1161
1162 "
1163 .unindent(),
1164 cx,
1165 );
1166 build_editor(buffer.clone(), window, cx)
1167 });
1168
1169 _ = editor.update(cx, |editor, window, cx| {
1170 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1171 assert_eq!(
1172 editor.display_text(cx),
1173 "
1174 class Foo:
1175 # Hello!
1176
1177 def a():⋯
1178
1179 def b():⋯
1180
1181
1182 class Bar:
1183 # World!
1184
1185 def a():⋯
1186
1187 def b():⋯
1188
1189
1190 "
1191 .unindent(),
1192 );
1193
1194 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1195 assert_eq!(
1196 editor.display_text(cx),
1197 "
1198 class Foo:⋯
1199
1200
1201 class Bar:⋯
1202
1203
1204 "
1205 .unindent(),
1206 );
1207
1208 editor.unfold_all(&UnfoldAll, window, cx);
1209 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1210 assert_eq!(
1211 editor.display_text(cx),
1212 "
1213 class Foo:
1214 # Hello!
1215
1216 def a():
1217 print(1)
1218
1219 def b():
1220 print(2)
1221
1222
1223 class Bar:
1224 # World!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 "
1234 .unindent(),
1235 );
1236
1237 assert_eq!(
1238 editor.display_text(cx),
1239 editor.buffer.read(cx).read(cx).text()
1240 );
1241 });
1242}
1243
1244#[gpui::test]
1245fn test_move_cursor(cx: &mut TestAppContext) {
1246 init_test(cx, |_| {});
1247
1248 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1249 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1250
1251 buffer.update(cx, |buffer, cx| {
1252 buffer.edit(
1253 vec![
1254 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1255 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1256 ],
1257 None,
1258 cx,
1259 );
1260 });
1261 _ = editor.update(cx, |editor, window, cx| {
1262 assert_eq!(
1263 editor.selections.display_ranges(cx),
1264 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1265 );
1266
1267 editor.move_down(&MoveDown, window, cx);
1268 assert_eq!(
1269 editor.selections.display_ranges(cx),
1270 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1271 );
1272
1273 editor.move_right(&MoveRight, window, cx);
1274 assert_eq!(
1275 editor.selections.display_ranges(cx),
1276 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1277 );
1278
1279 editor.move_left(&MoveLeft, window, cx);
1280 assert_eq!(
1281 editor.selections.display_ranges(cx),
1282 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1283 );
1284
1285 editor.move_up(&MoveUp, window, cx);
1286 assert_eq!(
1287 editor.selections.display_ranges(cx),
1288 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1289 );
1290
1291 editor.move_to_end(&MoveToEnd, window, cx);
1292 assert_eq!(
1293 editor.selections.display_ranges(cx),
1294 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1295 );
1296
1297 editor.move_to_beginning(&MoveToBeginning, window, cx);
1298 assert_eq!(
1299 editor.selections.display_ranges(cx),
1300 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1301 );
1302
1303 editor.change_selections(None, window, cx, |s| {
1304 s.select_display_ranges([
1305 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1306 ]);
1307 });
1308 editor.select_to_beginning(&SelectToBeginning, window, cx);
1309 assert_eq!(
1310 editor.selections.display_ranges(cx),
1311 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1312 );
1313
1314 editor.select_to_end(&SelectToEnd, window, cx);
1315 assert_eq!(
1316 editor.selections.display_ranges(cx),
1317 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1318 );
1319 });
1320}
1321
1322#[gpui::test]
1323fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1324 init_test(cx, |_| {});
1325
1326 let editor = cx.add_window(|window, cx| {
1327 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1328 build_editor(buffer.clone(), window, cx)
1329 });
1330
1331 assert_eq!('🟥'.len_utf8(), 4);
1332 assert_eq!('α'.len_utf8(), 2);
1333
1334 _ = editor.update(cx, |editor, window, cx| {
1335 editor.fold_creases(
1336 vec![
1337 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1338 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1339 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1340 ],
1341 true,
1342 window,
1343 cx,
1344 );
1345 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1346
1347 editor.move_right(&MoveRight, window, cx);
1348 assert_eq!(
1349 editor.selections.display_ranges(cx),
1350 &[empty_range(0, "🟥".len())]
1351 );
1352 editor.move_right(&MoveRight, window, cx);
1353 assert_eq!(
1354 editor.selections.display_ranges(cx),
1355 &[empty_range(0, "🟥🟧".len())]
1356 );
1357 editor.move_right(&MoveRight, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(0, "🟥🟧⋯".len())]
1361 );
1362
1363 editor.move_down(&MoveDown, window, cx);
1364 assert_eq!(
1365 editor.selections.display_ranges(cx),
1366 &[empty_range(1, "ab⋯e".len())]
1367 );
1368 editor.move_left(&MoveLeft, window, cx);
1369 assert_eq!(
1370 editor.selections.display_ranges(cx),
1371 &[empty_range(1, "ab⋯".len())]
1372 );
1373 editor.move_left(&MoveLeft, window, cx);
1374 assert_eq!(
1375 editor.selections.display_ranges(cx),
1376 &[empty_range(1, "ab".len())]
1377 );
1378 editor.move_left(&MoveLeft, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(1, "a".len())]
1382 );
1383
1384 editor.move_down(&MoveDown, window, cx);
1385 assert_eq!(
1386 editor.selections.display_ranges(cx),
1387 &[empty_range(2, "α".len())]
1388 );
1389 editor.move_right(&MoveRight, window, cx);
1390 assert_eq!(
1391 editor.selections.display_ranges(cx),
1392 &[empty_range(2, "αβ".len())]
1393 );
1394 editor.move_right(&MoveRight, window, cx);
1395 assert_eq!(
1396 editor.selections.display_ranges(cx),
1397 &[empty_range(2, "αβ⋯".len())]
1398 );
1399 editor.move_right(&MoveRight, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(2, "αβ⋯ε".len())]
1403 );
1404
1405 editor.move_up(&MoveUp, window, cx);
1406 assert_eq!(
1407 editor.selections.display_ranges(cx),
1408 &[empty_range(1, "ab⋯e".len())]
1409 );
1410 editor.move_down(&MoveDown, window, cx);
1411 assert_eq!(
1412 editor.selections.display_ranges(cx),
1413 &[empty_range(2, "αβ⋯ε".len())]
1414 );
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(1, "ab⋯e".len())]
1419 );
1420
1421 editor.move_up(&MoveUp, window, cx);
1422 assert_eq!(
1423 editor.selections.display_ranges(cx),
1424 &[empty_range(0, "🟥🟧".len())]
1425 );
1426 editor.move_left(&MoveLeft, window, cx);
1427 assert_eq!(
1428 editor.selections.display_ranges(cx),
1429 &[empty_range(0, "🟥".len())]
1430 );
1431 editor.move_left(&MoveLeft, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(0, "".len())]
1435 );
1436 });
1437}
1438
1439#[gpui::test]
1440fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1441 init_test(cx, |_| {});
1442
1443 let editor = cx.add_window(|window, cx| {
1444 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1445 build_editor(buffer.clone(), window, cx)
1446 });
1447 _ = editor.update(cx, |editor, window, cx| {
1448 editor.change_selections(None, window, cx, |s| {
1449 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1450 });
1451
1452 // moving above start of document should move selection to start of document,
1453 // but the next move down should still be at the original goal_x
1454 editor.move_up(&MoveUp, window, cx);
1455 assert_eq!(
1456 editor.selections.display_ranges(cx),
1457 &[empty_range(0, "".len())]
1458 );
1459
1460 editor.move_down(&MoveDown, window, cx);
1461 assert_eq!(
1462 editor.selections.display_ranges(cx),
1463 &[empty_range(1, "abcd".len())]
1464 );
1465
1466 editor.move_down(&MoveDown, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[empty_range(2, "αβγ".len())]
1470 );
1471
1472 editor.move_down(&MoveDown, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(3, "abcd".len())]
1476 );
1477
1478 editor.move_down(&MoveDown, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1482 );
1483
1484 // moving past end of document should not change goal_x
1485 editor.move_down(&MoveDown, window, cx);
1486 assert_eq!(
1487 editor.selections.display_ranges(cx),
1488 &[empty_range(5, "".len())]
1489 );
1490
1491 editor.move_down(&MoveDown, window, cx);
1492 assert_eq!(
1493 editor.selections.display_ranges(cx),
1494 &[empty_range(5, "".len())]
1495 );
1496
1497 editor.move_up(&MoveUp, window, cx);
1498 assert_eq!(
1499 editor.selections.display_ranges(cx),
1500 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1501 );
1502
1503 editor.move_up(&MoveUp, window, cx);
1504 assert_eq!(
1505 editor.selections.display_ranges(cx),
1506 &[empty_range(3, "abcd".len())]
1507 );
1508
1509 editor.move_up(&MoveUp, window, cx);
1510 assert_eq!(
1511 editor.selections.display_ranges(cx),
1512 &[empty_range(2, "αβγ".len())]
1513 );
1514 });
1515}
1516
1517#[gpui::test]
1518fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1519 init_test(cx, |_| {});
1520 let move_to_beg = MoveToBeginningOfLine {
1521 stop_at_soft_wraps: true,
1522 stop_at_indent: true,
1523 };
1524
1525 let delete_to_beg = DeleteToBeginningOfLine {
1526 stop_at_indent: false,
1527 };
1528
1529 let move_to_end = MoveToEndOfLine {
1530 stop_at_soft_wraps: true,
1531 };
1532
1533 let editor = cx.add_window(|window, cx| {
1534 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1535 build_editor(buffer, window, cx)
1536 });
1537 _ = editor.update(cx, |editor, window, cx| {
1538 editor.change_selections(None, window, cx, |s| {
1539 s.select_display_ranges([
1540 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1541 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1542 ]);
1543 });
1544 });
1545
1546 _ = editor.update(cx, |editor, window, cx| {
1547 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1548 assert_eq!(
1549 editor.selections.display_ranges(cx),
1550 &[
1551 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1552 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1553 ]
1554 );
1555 });
1556
1557 _ = editor.update(cx, |editor, window, cx| {
1558 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1559 assert_eq!(
1560 editor.selections.display_ranges(cx),
1561 &[
1562 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1563 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1564 ]
1565 );
1566 });
1567
1568 _ = editor.update(cx, |editor, window, cx| {
1569 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1570 assert_eq!(
1571 editor.selections.display_ranges(cx),
1572 &[
1573 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1574 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1575 ]
1576 );
1577 });
1578
1579 _ = editor.update(cx, |editor, window, cx| {
1580 editor.move_to_end_of_line(&move_to_end, window, cx);
1581 assert_eq!(
1582 editor.selections.display_ranges(cx),
1583 &[
1584 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1585 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1586 ]
1587 );
1588 });
1589
1590 // Moving to the end of line again is a no-op.
1591 _ = editor.update(cx, |editor, window, cx| {
1592 editor.move_to_end_of_line(&move_to_end, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[
1596 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1597 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1598 ]
1599 );
1600 });
1601
1602 _ = editor.update(cx, |editor, window, cx| {
1603 editor.move_left(&MoveLeft, window, cx);
1604 editor.select_to_beginning_of_line(
1605 &SelectToBeginningOfLine {
1606 stop_at_soft_wraps: true,
1607 stop_at_indent: true,
1608 },
1609 window,
1610 cx,
1611 );
1612 assert_eq!(
1613 editor.selections.display_ranges(cx),
1614 &[
1615 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1616 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1617 ]
1618 );
1619 });
1620
1621 _ = editor.update(cx, |editor, window, cx| {
1622 editor.select_to_beginning_of_line(
1623 &SelectToBeginningOfLine {
1624 stop_at_soft_wraps: true,
1625 stop_at_indent: true,
1626 },
1627 window,
1628 cx,
1629 );
1630 assert_eq!(
1631 editor.selections.display_ranges(cx),
1632 &[
1633 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1634 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1635 ]
1636 );
1637 });
1638
1639 _ = editor.update(cx, |editor, window, cx| {
1640 editor.select_to_beginning_of_line(
1641 &SelectToBeginningOfLine {
1642 stop_at_soft_wraps: true,
1643 stop_at_indent: true,
1644 },
1645 window,
1646 cx,
1647 );
1648 assert_eq!(
1649 editor.selections.display_ranges(cx),
1650 &[
1651 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1652 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1653 ]
1654 );
1655 });
1656
1657 _ = editor.update(cx, |editor, window, cx| {
1658 editor.select_to_end_of_line(
1659 &SelectToEndOfLine {
1660 stop_at_soft_wraps: true,
1661 },
1662 window,
1663 cx,
1664 );
1665 assert_eq!(
1666 editor.selections.display_ranges(cx),
1667 &[
1668 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1669 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1670 ]
1671 );
1672 });
1673
1674 _ = editor.update(cx, |editor, window, cx| {
1675 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1676 assert_eq!(editor.display_text(cx), "ab\n de");
1677 assert_eq!(
1678 editor.selections.display_ranges(cx),
1679 &[
1680 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1681 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1682 ]
1683 );
1684 });
1685
1686 _ = editor.update(cx, |editor, window, cx| {
1687 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1688 assert_eq!(editor.display_text(cx), "\n");
1689 assert_eq!(
1690 editor.selections.display_ranges(cx),
1691 &[
1692 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1693 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1694 ]
1695 );
1696 });
1697}
1698
1699#[gpui::test]
1700fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1701 init_test(cx, |_| {});
1702 let move_to_beg = MoveToBeginningOfLine {
1703 stop_at_soft_wraps: false,
1704 stop_at_indent: false,
1705 };
1706
1707 let move_to_end = MoveToEndOfLine {
1708 stop_at_soft_wraps: false,
1709 };
1710
1711 let editor = cx.add_window(|window, cx| {
1712 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1713 build_editor(buffer, window, cx)
1714 });
1715
1716 _ = editor.update(cx, |editor, window, cx| {
1717 editor.set_wrap_width(Some(140.0.into()), cx);
1718
1719 // We expect the following lines after wrapping
1720 // ```
1721 // thequickbrownfox
1722 // jumpedoverthelazydo
1723 // gs
1724 // ```
1725 // The final `gs` was soft-wrapped onto a new line.
1726 assert_eq!(
1727 "thequickbrownfox\njumpedoverthelaz\nydogs",
1728 editor.display_text(cx),
1729 );
1730
1731 // First, let's assert behavior on the first line, that was not soft-wrapped.
1732 // Start the cursor at the `k` on the first line
1733 editor.change_selections(None, window, cx, |s| {
1734 s.select_display_ranges([
1735 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1736 ]);
1737 });
1738
1739 // Moving to the beginning of the line should put us at the beginning of the line.
1740 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1741 assert_eq!(
1742 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1743 editor.selections.display_ranges(cx)
1744 );
1745
1746 // Moving to the end of the line should put us at the end of the line.
1747 editor.move_to_end_of_line(&move_to_end, window, cx);
1748 assert_eq!(
1749 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1750 editor.selections.display_ranges(cx)
1751 );
1752
1753 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1754 // Start the cursor at the last line (`y` that was wrapped to a new line)
1755 editor.change_selections(None, window, cx, |s| {
1756 s.select_display_ranges([
1757 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1758 ]);
1759 });
1760
1761 // Moving to the beginning of the line should put us at the start of the second line of
1762 // display text, i.e., the `j`.
1763 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1764 assert_eq!(
1765 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1766 editor.selections.display_ranges(cx)
1767 );
1768
1769 // Moving to the beginning of the line again should be a no-op.
1770 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1771 assert_eq!(
1772 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1773 editor.selections.display_ranges(cx)
1774 );
1775
1776 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1777 // next display line.
1778 editor.move_to_end_of_line(&move_to_end, window, cx);
1779 assert_eq!(
1780 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1781 editor.selections.display_ranges(cx)
1782 );
1783
1784 // Moving to the end of the line again should be a no-op.
1785 editor.move_to_end_of_line(&move_to_end, window, cx);
1786 assert_eq!(
1787 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1788 editor.selections.display_ranges(cx)
1789 );
1790 });
1791}
1792
1793#[gpui::test]
1794fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1795 init_test(cx, |_| {});
1796
1797 let move_to_beg = MoveToBeginningOfLine {
1798 stop_at_soft_wraps: true,
1799 stop_at_indent: true,
1800 };
1801
1802 let select_to_beg = SelectToBeginningOfLine {
1803 stop_at_soft_wraps: true,
1804 stop_at_indent: true,
1805 };
1806
1807 let delete_to_beg = DeleteToBeginningOfLine {
1808 stop_at_indent: true,
1809 };
1810
1811 let move_to_end = MoveToEndOfLine {
1812 stop_at_soft_wraps: false,
1813 };
1814
1815 let editor = cx.add_window(|window, cx| {
1816 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1817 build_editor(buffer, window, cx)
1818 });
1819
1820 _ = editor.update(cx, |editor, window, cx| {
1821 editor.change_selections(None, window, cx, |s| {
1822 s.select_display_ranges([
1823 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1824 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1825 ]);
1826 });
1827
1828 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1829 // and the second cursor at the first non-whitespace character in the line.
1830 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1831 assert_eq!(
1832 editor.selections.display_ranges(cx),
1833 &[
1834 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1835 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1836 ]
1837 );
1838
1839 // Moving to the beginning of the line again should be a no-op for the first cursor,
1840 // and should move the second cursor to the beginning of the line.
1841 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1842 assert_eq!(
1843 editor.selections.display_ranges(cx),
1844 &[
1845 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1846 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1847 ]
1848 );
1849
1850 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1851 // and should move the second cursor back to the first non-whitespace character in the line.
1852 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1853 assert_eq!(
1854 editor.selections.display_ranges(cx),
1855 &[
1856 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1857 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1858 ]
1859 );
1860
1861 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1862 // and to the first non-whitespace character in the line for the second cursor.
1863 editor.move_to_end_of_line(&move_to_end, window, cx);
1864 editor.move_left(&MoveLeft, window, cx);
1865 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1866 assert_eq!(
1867 editor.selections.display_ranges(cx),
1868 &[
1869 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1870 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1871 ]
1872 );
1873
1874 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1875 // and should select to the beginning of the line for the second cursor.
1876 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1877 assert_eq!(
1878 editor.selections.display_ranges(cx),
1879 &[
1880 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1881 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1882 ]
1883 );
1884
1885 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1886 // and should delete to the first non-whitespace character in the line for the second cursor.
1887 editor.move_to_end_of_line(&move_to_end, window, cx);
1888 editor.move_left(&MoveLeft, window, cx);
1889 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1890 assert_eq!(editor.text(cx), "c\n f");
1891 });
1892}
1893
1894#[gpui::test]
1895fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1896 init_test(cx, |_| {});
1897
1898 let editor = cx.add_window(|window, cx| {
1899 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1900 build_editor(buffer, window, cx)
1901 });
1902 _ = editor.update(cx, |editor, window, cx| {
1903 editor.change_selections(None, window, cx, |s| {
1904 s.select_display_ranges([
1905 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1906 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1907 ])
1908 });
1909
1910 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1911 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1912
1913 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1914 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1915
1916 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1917 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1918
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1924
1925 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1926 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1929 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1932 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1933
1934 editor.move_right(&MoveRight, window, cx);
1935 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1936 assert_selection_ranges(
1937 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1938 editor,
1939 cx,
1940 );
1941
1942 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1943 assert_selection_ranges(
1944 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1945 editor,
1946 cx,
1947 );
1948
1949 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1950 assert_selection_ranges(
1951 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1952 editor,
1953 cx,
1954 );
1955 });
1956}
1957
1958#[gpui::test]
1959fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1960 init_test(cx, |_| {});
1961
1962 let editor = cx.add_window(|window, cx| {
1963 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1964 build_editor(buffer, window, cx)
1965 });
1966
1967 _ = editor.update(cx, |editor, window, cx| {
1968 editor.set_wrap_width(Some(140.0.into()), cx);
1969 assert_eq!(
1970 editor.display_text(cx),
1971 "use one::{\n two::three::\n four::five\n};"
1972 );
1973
1974 editor.change_selections(None, window, cx, |s| {
1975 s.select_display_ranges([
1976 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1977 ]);
1978 });
1979
1980 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1981 assert_eq!(
1982 editor.selections.display_ranges(cx),
1983 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1984 );
1985
1986 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1987 assert_eq!(
1988 editor.selections.display_ranges(cx),
1989 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1990 );
1991
1992 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1993 assert_eq!(
1994 editor.selections.display_ranges(cx),
1995 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1996 );
1997
1998 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1999 assert_eq!(
2000 editor.selections.display_ranges(cx),
2001 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2002 );
2003
2004 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2005 assert_eq!(
2006 editor.selections.display_ranges(cx),
2007 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2008 );
2009
2010 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2011 assert_eq!(
2012 editor.selections.display_ranges(cx),
2013 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2014 );
2015 });
2016}
2017
2018#[gpui::test]
2019async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2020 init_test(cx, |_| {});
2021 let mut cx = EditorTestContext::new(cx).await;
2022
2023 let line_height = cx.editor(|editor, window, _| {
2024 editor
2025 .style()
2026 .unwrap()
2027 .text
2028 .line_height_in_pixels(window.rem_size())
2029 });
2030 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2031
2032 cx.set_state(
2033 &r#"ˇone
2034 two
2035
2036 three
2037 fourˇ
2038 five
2039
2040 six"#
2041 .unindent(),
2042 );
2043
2044 cx.update_editor(|editor, window, cx| {
2045 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2046 });
2047 cx.assert_editor_state(
2048 &r#"one
2049 two
2050 ˇ
2051 three
2052 four
2053 five
2054 ˇ
2055 six"#
2056 .unindent(),
2057 );
2058
2059 cx.update_editor(|editor, window, cx| {
2060 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2061 });
2062 cx.assert_editor_state(
2063 &r#"one
2064 two
2065
2066 three
2067 four
2068 five
2069 ˇ
2070 sixˇ"#
2071 .unindent(),
2072 );
2073
2074 cx.update_editor(|editor, window, cx| {
2075 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2076 });
2077 cx.assert_editor_state(
2078 &r#"one
2079 two
2080
2081 three
2082 four
2083 five
2084
2085 sixˇ"#
2086 .unindent(),
2087 );
2088
2089 cx.update_editor(|editor, window, cx| {
2090 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2091 });
2092 cx.assert_editor_state(
2093 &r#"one
2094 two
2095
2096 three
2097 four
2098 five
2099 ˇ
2100 six"#
2101 .unindent(),
2102 );
2103
2104 cx.update_editor(|editor, window, cx| {
2105 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2106 });
2107 cx.assert_editor_state(
2108 &r#"one
2109 two
2110 ˇ
2111 three
2112 four
2113 five
2114
2115 six"#
2116 .unindent(),
2117 );
2118
2119 cx.update_editor(|editor, window, cx| {
2120 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2121 });
2122 cx.assert_editor_state(
2123 &r#"ˇone
2124 two
2125
2126 three
2127 four
2128 five
2129
2130 six"#
2131 .unindent(),
2132 );
2133}
2134
2135#[gpui::test]
2136async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2137 init_test(cx, |_| {});
2138 let mut cx = EditorTestContext::new(cx).await;
2139 let line_height = cx.editor(|editor, window, _| {
2140 editor
2141 .style()
2142 .unwrap()
2143 .text
2144 .line_height_in_pixels(window.rem_size())
2145 });
2146 let window = cx.window;
2147 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2148
2149 cx.set_state(
2150 r#"ˇone
2151 two
2152 three
2153 four
2154 five
2155 six
2156 seven
2157 eight
2158 nine
2159 ten
2160 "#,
2161 );
2162
2163 cx.update_editor(|editor, window, cx| {
2164 assert_eq!(
2165 editor.snapshot(window, cx).scroll_position(),
2166 gpui::Point::new(0., 0.)
2167 );
2168 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2169 assert_eq!(
2170 editor.snapshot(window, cx).scroll_position(),
2171 gpui::Point::new(0., 3.)
2172 );
2173 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2174 assert_eq!(
2175 editor.snapshot(window, cx).scroll_position(),
2176 gpui::Point::new(0., 6.)
2177 );
2178 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2179 assert_eq!(
2180 editor.snapshot(window, cx).scroll_position(),
2181 gpui::Point::new(0., 3.)
2182 );
2183
2184 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2185 assert_eq!(
2186 editor.snapshot(window, cx).scroll_position(),
2187 gpui::Point::new(0., 1.)
2188 );
2189 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2190 assert_eq!(
2191 editor.snapshot(window, cx).scroll_position(),
2192 gpui::Point::new(0., 3.)
2193 );
2194 });
2195}
2196
2197#[gpui::test]
2198async fn test_autoscroll(cx: &mut TestAppContext) {
2199 init_test(cx, |_| {});
2200 let mut cx = EditorTestContext::new(cx).await;
2201
2202 let line_height = cx.update_editor(|editor, window, cx| {
2203 editor.set_vertical_scroll_margin(2, cx);
2204 editor
2205 .style()
2206 .unwrap()
2207 .text
2208 .line_height_in_pixels(window.rem_size())
2209 });
2210 let window = cx.window;
2211 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2212
2213 cx.set_state(
2214 r#"ˇone
2215 two
2216 three
2217 four
2218 five
2219 six
2220 seven
2221 eight
2222 nine
2223 ten
2224 "#,
2225 );
2226 cx.update_editor(|editor, window, cx| {
2227 assert_eq!(
2228 editor.snapshot(window, cx).scroll_position(),
2229 gpui::Point::new(0., 0.0)
2230 );
2231 });
2232
2233 // Add a cursor below the visible area. Since both cursors cannot fit
2234 // on screen, the editor autoscrolls to reveal the newest cursor, and
2235 // allows the vertical scroll margin below that cursor.
2236 cx.update_editor(|editor, window, cx| {
2237 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2238 selections.select_ranges([
2239 Point::new(0, 0)..Point::new(0, 0),
2240 Point::new(6, 0)..Point::new(6, 0),
2241 ]);
2242 })
2243 });
2244 cx.update_editor(|editor, window, cx| {
2245 assert_eq!(
2246 editor.snapshot(window, cx).scroll_position(),
2247 gpui::Point::new(0., 3.0)
2248 );
2249 });
2250
2251 // Move down. The editor cursor scrolls down to track the newest cursor.
2252 cx.update_editor(|editor, window, cx| {
2253 editor.move_down(&Default::default(), window, cx);
2254 });
2255 cx.update_editor(|editor, window, cx| {
2256 assert_eq!(
2257 editor.snapshot(window, cx).scroll_position(),
2258 gpui::Point::new(0., 4.0)
2259 );
2260 });
2261
2262 // Add a cursor above the visible area. Since both cursors fit on screen,
2263 // the editor scrolls to show both.
2264 cx.update_editor(|editor, window, cx| {
2265 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2266 selections.select_ranges([
2267 Point::new(1, 0)..Point::new(1, 0),
2268 Point::new(6, 0)..Point::new(6, 0),
2269 ]);
2270 })
2271 });
2272 cx.update_editor(|editor, window, cx| {
2273 assert_eq!(
2274 editor.snapshot(window, cx).scroll_position(),
2275 gpui::Point::new(0., 1.0)
2276 );
2277 });
2278}
2279
2280#[gpui::test]
2281async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2282 init_test(cx, |_| {});
2283 let mut cx = EditorTestContext::new(cx).await;
2284
2285 let line_height = cx.editor(|editor, window, _cx| {
2286 editor
2287 .style()
2288 .unwrap()
2289 .text
2290 .line_height_in_pixels(window.rem_size())
2291 });
2292 let window = cx.window;
2293 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2294 cx.set_state(
2295 &r#"
2296 ˇone
2297 two
2298 threeˇ
2299 four
2300 five
2301 six
2302 seven
2303 eight
2304 nine
2305 ten
2306 "#
2307 .unindent(),
2308 );
2309
2310 cx.update_editor(|editor, window, cx| {
2311 editor.move_page_down(&MovePageDown::default(), window, cx)
2312 });
2313 cx.assert_editor_state(
2314 &r#"
2315 one
2316 two
2317 three
2318 ˇfour
2319 five
2320 sixˇ
2321 seven
2322 eight
2323 nine
2324 ten
2325 "#
2326 .unindent(),
2327 );
2328
2329 cx.update_editor(|editor, window, cx| {
2330 editor.move_page_down(&MovePageDown::default(), window, cx)
2331 });
2332 cx.assert_editor_state(
2333 &r#"
2334 one
2335 two
2336 three
2337 four
2338 five
2339 six
2340 ˇseven
2341 eight
2342 nineˇ
2343 ten
2344 "#
2345 .unindent(),
2346 );
2347
2348 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2349 cx.assert_editor_state(
2350 &r#"
2351 one
2352 two
2353 three
2354 ˇfour
2355 five
2356 sixˇ
2357 seven
2358 eight
2359 nine
2360 ten
2361 "#
2362 .unindent(),
2363 );
2364
2365 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2366 cx.assert_editor_state(
2367 &r#"
2368 ˇone
2369 two
2370 threeˇ
2371 four
2372 five
2373 six
2374 seven
2375 eight
2376 nine
2377 ten
2378 "#
2379 .unindent(),
2380 );
2381
2382 // Test select collapsing
2383 cx.update_editor(|editor, window, cx| {
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 editor.move_page_down(&MovePageDown::default(), window, cx);
2387 });
2388 cx.assert_editor_state(
2389 &r#"
2390 one
2391 two
2392 three
2393 four
2394 five
2395 six
2396 seven
2397 eight
2398 nine
2399 ˇten
2400 ˇ"#
2401 .unindent(),
2402 );
2403}
2404
2405#[gpui::test]
2406async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2407 init_test(cx, |_| {});
2408 let mut cx = EditorTestContext::new(cx).await;
2409 cx.set_state("one «two threeˇ» four");
2410 cx.update_editor(|editor, window, cx| {
2411 editor.delete_to_beginning_of_line(
2412 &DeleteToBeginningOfLine {
2413 stop_at_indent: false,
2414 },
2415 window,
2416 cx,
2417 );
2418 assert_eq!(editor.text(cx), " four");
2419 });
2420}
2421
2422#[gpui::test]
2423fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2424 init_test(cx, |_| {});
2425
2426 let editor = cx.add_window(|window, cx| {
2427 let buffer = MultiBuffer::build_simple("one two three four", cx);
2428 build_editor(buffer.clone(), window, cx)
2429 });
2430
2431 _ = editor.update(cx, |editor, window, cx| {
2432 editor.change_selections(None, window, cx, |s| {
2433 s.select_display_ranges([
2434 // an empty selection - the preceding word fragment is deleted
2435 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2436 // characters selected - they are deleted
2437 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2438 ])
2439 });
2440 editor.delete_to_previous_word_start(
2441 &DeleteToPreviousWordStart {
2442 ignore_newlines: false,
2443 },
2444 window,
2445 cx,
2446 );
2447 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2448 });
2449
2450 _ = editor.update(cx, |editor, window, cx| {
2451 editor.change_selections(None, window, cx, |s| {
2452 s.select_display_ranges([
2453 // an empty selection - the following word fragment is deleted
2454 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2455 // characters selected - they are deleted
2456 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2457 ])
2458 });
2459 editor.delete_to_next_word_end(
2460 &DeleteToNextWordEnd {
2461 ignore_newlines: false,
2462 },
2463 window,
2464 cx,
2465 );
2466 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2467 });
2468}
2469
2470#[gpui::test]
2471fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2472 init_test(cx, |_| {});
2473
2474 let editor = cx.add_window(|window, cx| {
2475 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2476 build_editor(buffer.clone(), window, cx)
2477 });
2478 let del_to_prev_word_start = DeleteToPreviousWordStart {
2479 ignore_newlines: false,
2480 };
2481 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2482 ignore_newlines: true,
2483 };
2484
2485 _ = editor.update(cx, |editor, window, cx| {
2486 editor.change_selections(None, window, cx, |s| {
2487 s.select_display_ranges([
2488 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2489 ])
2490 });
2491 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2492 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2493 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2494 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2495 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2496 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2497 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2498 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2499 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2500 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2501 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2502 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2503 });
2504}
2505
2506#[gpui::test]
2507fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2508 init_test(cx, |_| {});
2509
2510 let editor = cx.add_window(|window, cx| {
2511 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2512 build_editor(buffer.clone(), window, cx)
2513 });
2514 let del_to_next_word_end = DeleteToNextWordEnd {
2515 ignore_newlines: false,
2516 };
2517 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2518 ignore_newlines: true,
2519 };
2520
2521 _ = editor.update(cx, |editor, window, cx| {
2522 editor.change_selections(None, window, cx, |s| {
2523 s.select_display_ranges([
2524 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2525 ])
2526 });
2527 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2528 assert_eq!(
2529 editor.buffer.read(cx).read(cx).text(),
2530 "one\n two\nthree\n four"
2531 );
2532 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2533 assert_eq!(
2534 editor.buffer.read(cx).read(cx).text(),
2535 "\n two\nthree\n four"
2536 );
2537 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2538 assert_eq!(
2539 editor.buffer.read(cx).read(cx).text(),
2540 "two\nthree\n four"
2541 );
2542 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2543 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2544 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2545 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2546 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2547 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2548 });
2549}
2550
2551#[gpui::test]
2552fn test_newline(cx: &mut TestAppContext) {
2553 init_test(cx, |_| {});
2554
2555 let editor = cx.add_window(|window, cx| {
2556 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2557 build_editor(buffer.clone(), window, cx)
2558 });
2559
2560 _ = editor.update(cx, |editor, window, cx| {
2561 editor.change_selections(None, window, cx, |s| {
2562 s.select_display_ranges([
2563 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2565 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2566 ])
2567 });
2568
2569 editor.newline(&Newline, window, cx);
2570 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2571 });
2572}
2573
2574#[gpui::test]
2575fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2576 init_test(cx, |_| {});
2577
2578 let editor = cx.add_window(|window, cx| {
2579 let buffer = MultiBuffer::build_simple(
2580 "
2581 a
2582 b(
2583 X
2584 )
2585 c(
2586 X
2587 )
2588 "
2589 .unindent()
2590 .as_str(),
2591 cx,
2592 );
2593 let mut editor = build_editor(buffer.clone(), window, cx);
2594 editor.change_selections(None, window, cx, |s| {
2595 s.select_ranges([
2596 Point::new(2, 4)..Point::new(2, 5),
2597 Point::new(5, 4)..Point::new(5, 5),
2598 ])
2599 });
2600 editor
2601 });
2602
2603 _ = editor.update(cx, |editor, window, cx| {
2604 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2605 editor.buffer.update(cx, |buffer, cx| {
2606 buffer.edit(
2607 [
2608 (Point::new(1, 2)..Point::new(3, 0), ""),
2609 (Point::new(4, 2)..Point::new(6, 0), ""),
2610 ],
2611 None,
2612 cx,
2613 );
2614 assert_eq!(
2615 buffer.read(cx).text(),
2616 "
2617 a
2618 b()
2619 c()
2620 "
2621 .unindent()
2622 );
2623 });
2624 assert_eq!(
2625 editor.selections.ranges(cx),
2626 &[
2627 Point::new(1, 2)..Point::new(1, 2),
2628 Point::new(2, 2)..Point::new(2, 2),
2629 ],
2630 );
2631
2632 editor.newline(&Newline, window, cx);
2633 assert_eq!(
2634 editor.text(cx),
2635 "
2636 a
2637 b(
2638 )
2639 c(
2640 )
2641 "
2642 .unindent()
2643 );
2644
2645 // The selections are moved after the inserted newlines
2646 assert_eq!(
2647 editor.selections.ranges(cx),
2648 &[
2649 Point::new(2, 0)..Point::new(2, 0),
2650 Point::new(4, 0)..Point::new(4, 0),
2651 ],
2652 );
2653 });
2654}
2655
2656#[gpui::test]
2657async fn test_newline_above(cx: &mut TestAppContext) {
2658 init_test(cx, |settings| {
2659 settings.defaults.tab_size = NonZeroU32::new(4)
2660 });
2661
2662 let language = Arc::new(
2663 Language::new(
2664 LanguageConfig::default(),
2665 Some(tree_sitter_rust::LANGUAGE.into()),
2666 )
2667 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2668 .unwrap(),
2669 );
2670
2671 let mut cx = EditorTestContext::new(cx).await;
2672 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2673 cx.set_state(indoc! {"
2674 const a: ˇA = (
2675 (ˇ
2676 «const_functionˇ»(ˇ),
2677 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2678 )ˇ
2679 ˇ);ˇ
2680 "});
2681
2682 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2683 cx.assert_editor_state(indoc! {"
2684 ˇ
2685 const a: A = (
2686 ˇ
2687 (
2688 ˇ
2689 ˇ
2690 const_function(),
2691 ˇ
2692 ˇ
2693 ˇ
2694 ˇ
2695 something_else,
2696 ˇ
2697 )
2698 ˇ
2699 ˇ
2700 );
2701 "});
2702}
2703
2704#[gpui::test]
2705async fn test_newline_below(cx: &mut TestAppContext) {
2706 init_test(cx, |settings| {
2707 settings.defaults.tab_size = NonZeroU32::new(4)
2708 });
2709
2710 let language = Arc::new(
2711 Language::new(
2712 LanguageConfig::default(),
2713 Some(tree_sitter_rust::LANGUAGE.into()),
2714 )
2715 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2716 .unwrap(),
2717 );
2718
2719 let mut cx = EditorTestContext::new(cx).await;
2720 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2721 cx.set_state(indoc! {"
2722 const a: ˇA = (
2723 (ˇ
2724 «const_functionˇ»(ˇ),
2725 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2726 )ˇ
2727 ˇ);ˇ
2728 "});
2729
2730 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2731 cx.assert_editor_state(indoc! {"
2732 const a: A = (
2733 ˇ
2734 (
2735 ˇ
2736 const_function(),
2737 ˇ
2738 ˇ
2739 something_else,
2740 ˇ
2741 ˇ
2742 ˇ
2743 ˇ
2744 )
2745 ˇ
2746 );
2747 ˇ
2748 ˇ
2749 "});
2750}
2751
2752#[gpui::test]
2753async fn test_newline_comments(cx: &mut TestAppContext) {
2754 init_test(cx, |settings| {
2755 settings.defaults.tab_size = NonZeroU32::new(4)
2756 });
2757
2758 let language = Arc::new(Language::new(
2759 LanguageConfig {
2760 line_comments: vec!["// ".into()],
2761 ..LanguageConfig::default()
2762 },
2763 None,
2764 ));
2765 {
2766 let mut cx = EditorTestContext::new(cx).await;
2767 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2768 cx.set_state(indoc! {"
2769 // Fooˇ
2770 "});
2771
2772 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2773 cx.assert_editor_state(indoc! {"
2774 // Foo
2775 // ˇ
2776 "});
2777 // Ensure that we add comment prefix when existing line contains space
2778 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2779 cx.assert_editor_state(
2780 indoc! {"
2781 // Foo
2782 //s
2783 // ˇ
2784 "}
2785 .replace("s", " ") // s is used as space placeholder to prevent format on save
2786 .as_str(),
2787 );
2788 // Ensure that we add comment prefix when existing line does not contain space
2789 cx.set_state(indoc! {"
2790 // Foo
2791 //ˇ
2792 "});
2793 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2794 cx.assert_editor_state(indoc! {"
2795 // Foo
2796 //
2797 // ˇ
2798 "});
2799 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2800 cx.set_state(indoc! {"
2801 ˇ// Foo
2802 "});
2803 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2804 cx.assert_editor_state(indoc! {"
2805
2806 ˇ// Foo
2807 "});
2808 }
2809 // Ensure that comment continuations can be disabled.
2810 update_test_language_settings(cx, |settings| {
2811 settings.defaults.extend_comment_on_newline = Some(false);
2812 });
2813 let mut cx = EditorTestContext::new(cx).await;
2814 cx.set_state(indoc! {"
2815 // Fooˇ
2816 "});
2817 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2818 cx.assert_editor_state(indoc! {"
2819 // Foo
2820 ˇ
2821 "});
2822}
2823
2824#[gpui::test]
2825async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2826 init_test(cx, |settings| {
2827 settings.defaults.tab_size = NonZeroU32::new(4)
2828 });
2829
2830 let language = Arc::new(Language::new(
2831 LanguageConfig {
2832 line_comments: vec!["// ".into(), "/// ".into()],
2833 ..LanguageConfig::default()
2834 },
2835 None,
2836 ));
2837 {
2838 let mut cx = EditorTestContext::new(cx).await;
2839 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2840 cx.set_state(indoc! {"
2841 //ˇ
2842 "});
2843 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2844 cx.assert_editor_state(indoc! {"
2845 //
2846 // ˇ
2847 "});
2848
2849 cx.set_state(indoc! {"
2850 ///ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 ///
2855 /// ˇ
2856 "});
2857 }
2858}
2859
2860#[gpui::test]
2861async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2862 init_test(cx, |settings| {
2863 settings.defaults.tab_size = NonZeroU32::new(4)
2864 });
2865
2866 let language = Arc::new(
2867 Language::new(
2868 LanguageConfig {
2869 documentation: Some(language::DocumentationConfig {
2870 start: "/**".into(),
2871 end: "*/".into(),
2872 prefix: "* ".into(),
2873 tab_size: NonZeroU32::new(1).unwrap(),
2874 }),
2875
2876 ..LanguageConfig::default()
2877 },
2878 Some(tree_sitter_rust::LANGUAGE.into()),
2879 )
2880 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2881 .unwrap(),
2882 );
2883
2884 {
2885 let mut cx = EditorTestContext::new(cx).await;
2886 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2887 cx.set_state(indoc! {"
2888 /**ˇ
2889 "});
2890
2891 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2892 cx.assert_editor_state(indoc! {"
2893 /**
2894 * ˇ
2895 "});
2896 // Ensure that if cursor is before the comment start,
2897 // we do not actually insert a comment prefix.
2898 cx.set_state(indoc! {"
2899 ˇ/**
2900 "});
2901 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2902 cx.assert_editor_state(indoc! {"
2903
2904 ˇ/**
2905 "});
2906 // Ensure that if cursor is between it doesn't add comment prefix.
2907 cx.set_state(indoc! {"
2908 /*ˇ*
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912 /*
2913 ˇ*
2914 "});
2915 // Ensure that if suffix exists on same line after cursor it adds new line.
2916 cx.set_state(indoc! {"
2917 /**ˇ*/
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /**
2922 * ˇ
2923 */
2924 "});
2925 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2926 cx.set_state(indoc! {"
2927 /**ˇ */
2928 "});
2929 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2930 cx.assert_editor_state(indoc! {"
2931 /**
2932 * ˇ
2933 */
2934 "});
2935 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2936 cx.set_state(indoc! {"
2937 /** ˇ*/
2938 "});
2939 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2940 cx.assert_editor_state(
2941 indoc! {"
2942 /**s
2943 * ˇ
2944 */
2945 "}
2946 .replace("s", " ") // s is used as space placeholder to prevent format on save
2947 .as_str(),
2948 );
2949 // Ensure that delimiter space is preserved when newline on already
2950 // spaced delimiter.
2951 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2952 cx.assert_editor_state(
2953 indoc! {"
2954 /**s
2955 *s
2956 * ˇ
2957 */
2958 "}
2959 .replace("s", " ") // s is used as space placeholder to prevent format on save
2960 .as_str(),
2961 );
2962 // Ensure that delimiter space is preserved when space is not
2963 // on existing delimiter.
2964 cx.set_state(indoc! {"
2965 /**
2966 *ˇ
2967 */
2968 "});
2969 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2970 cx.assert_editor_state(indoc! {"
2971 /**
2972 *
2973 * ˇ
2974 */
2975 "});
2976 // Ensure that if suffix exists on same line after cursor it
2977 // doesn't add extra new line if prefix is not on same line.
2978 cx.set_state(indoc! {"
2979 /**
2980 ˇ*/
2981 "});
2982 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2983 cx.assert_editor_state(indoc! {"
2984 /**
2985
2986 ˇ*/
2987 "});
2988 // Ensure that it detects suffix after existing prefix.
2989 cx.set_state(indoc! {"
2990 /**ˇ/
2991 "});
2992 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2993 cx.assert_editor_state(indoc! {"
2994 /**
2995 ˇ/
2996 "});
2997 // Ensure that if suffix exists on same line before
2998 // cursor it does not add comment prefix.
2999 cx.set_state(indoc! {"
3000 /** */ˇ
3001 "});
3002 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3003 cx.assert_editor_state(indoc! {"
3004 /** */
3005 ˇ
3006 "});
3007 // Ensure that if suffix exists on same line before
3008 // cursor it does not add comment prefix.
3009 cx.set_state(indoc! {"
3010 /**
3011 *
3012 */ˇ
3013 "});
3014 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3015 cx.assert_editor_state(indoc! {"
3016 /**
3017 *
3018 */
3019 ˇ
3020 "});
3021
3022 // Ensure that inline comment followed by code
3023 // doesn't add comment prefix on newline
3024 cx.set_state(indoc! {"
3025 /** */ textˇ
3026 "});
3027 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3028 cx.assert_editor_state(indoc! {"
3029 /** */ text
3030 ˇ
3031 "});
3032
3033 // Ensure that text after comment end tag
3034 // doesn't add comment prefix on newline
3035 cx.set_state(indoc! {"
3036 /**
3037 *
3038 */ˇtext
3039 "});
3040 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3041 cx.assert_editor_state(indoc! {"
3042 /**
3043 *
3044 */
3045 ˇtext
3046 "});
3047
3048 // Ensure if not comment block it doesn't
3049 // add comment prefix on newline
3050 cx.set_state(indoc! {"
3051 * textˇ
3052 "});
3053 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3054 cx.assert_editor_state(indoc! {"
3055 * text
3056 ˇ
3057 "});
3058 }
3059 // Ensure that comment continuations can be disabled.
3060 update_test_language_settings(cx, |settings| {
3061 settings.defaults.extend_comment_on_newline = Some(false);
3062 });
3063 let mut cx = EditorTestContext::new(cx).await;
3064 cx.set_state(indoc! {"
3065 /**ˇ
3066 "});
3067 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3068 cx.assert_editor_state(indoc! {"
3069 /**
3070 ˇ
3071 "});
3072}
3073
3074#[gpui::test]
3075fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3076 init_test(cx, |_| {});
3077
3078 let editor = cx.add_window(|window, cx| {
3079 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3080 let mut editor = build_editor(buffer.clone(), window, cx);
3081 editor.change_selections(None, window, cx, |s| {
3082 s.select_ranges([3..4, 11..12, 19..20])
3083 });
3084 editor
3085 });
3086
3087 _ = editor.update(cx, |editor, window, cx| {
3088 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3089 editor.buffer.update(cx, |buffer, cx| {
3090 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3091 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3092 });
3093 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3094
3095 editor.insert("Z", window, cx);
3096 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3097
3098 // The selections are moved after the inserted characters
3099 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3100 });
3101}
3102
3103#[gpui::test]
3104async fn test_tab(cx: &mut TestAppContext) {
3105 init_test(cx, |settings| {
3106 settings.defaults.tab_size = NonZeroU32::new(3)
3107 });
3108
3109 let mut cx = EditorTestContext::new(cx).await;
3110 cx.set_state(indoc! {"
3111 ˇabˇc
3112 ˇ🏀ˇ🏀ˇefg
3113 dˇ
3114 "});
3115 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 ˇab ˇc
3118 ˇ🏀 ˇ🏀 ˇefg
3119 d ˇ
3120 "});
3121
3122 cx.set_state(indoc! {"
3123 a
3124 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3125 "});
3126 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3127 cx.assert_editor_state(indoc! {"
3128 a
3129 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3130 "});
3131}
3132
3133#[gpui::test]
3134async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3135 init_test(cx, |_| {});
3136
3137 let mut cx = EditorTestContext::new(cx).await;
3138 let language = Arc::new(
3139 Language::new(
3140 LanguageConfig::default(),
3141 Some(tree_sitter_rust::LANGUAGE.into()),
3142 )
3143 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3144 .unwrap(),
3145 );
3146 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3147
3148 // test when all cursors are not at suggested indent
3149 // then simply move to their suggested indent location
3150 cx.set_state(indoc! {"
3151 const a: B = (
3152 c(
3153 ˇ
3154 ˇ )
3155 );
3156 "});
3157 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3158 cx.assert_editor_state(indoc! {"
3159 const a: B = (
3160 c(
3161 ˇ
3162 ˇ)
3163 );
3164 "});
3165
3166 // test cursor already at suggested indent not moving when
3167 // other cursors are yet to reach their suggested indents
3168 cx.set_state(indoc! {"
3169 ˇ
3170 const a: B = (
3171 c(
3172 d(
3173 ˇ
3174 )
3175 ˇ
3176 ˇ )
3177 );
3178 "});
3179 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3180 cx.assert_editor_state(indoc! {"
3181 ˇ
3182 const a: B = (
3183 c(
3184 d(
3185 ˇ
3186 )
3187 ˇ
3188 ˇ)
3189 );
3190 "});
3191 // test when all cursors are at suggested indent then tab is inserted
3192 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3193 cx.assert_editor_state(indoc! {"
3194 ˇ
3195 const a: B = (
3196 c(
3197 d(
3198 ˇ
3199 )
3200 ˇ
3201 ˇ)
3202 );
3203 "});
3204
3205 // test when current indent is less than suggested indent,
3206 // we adjust line to match suggested indent and move cursor to it
3207 //
3208 // when no other cursor is at word boundary, all of them should move
3209 cx.set_state(indoc! {"
3210 const a: B = (
3211 c(
3212 d(
3213 ˇ
3214 ˇ )
3215 ˇ )
3216 );
3217 "});
3218 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3219 cx.assert_editor_state(indoc! {"
3220 const a: B = (
3221 c(
3222 d(
3223 ˇ
3224 ˇ)
3225 ˇ)
3226 );
3227 "});
3228
3229 // test when current indent is less than suggested indent,
3230 // we adjust line to match suggested indent and move cursor to it
3231 //
3232 // when some other cursor is at word boundary, it should not move
3233 cx.set_state(indoc! {"
3234 const a: B = (
3235 c(
3236 d(
3237 ˇ
3238 ˇ )
3239 ˇ)
3240 );
3241 "});
3242 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3243 cx.assert_editor_state(indoc! {"
3244 const a: B = (
3245 c(
3246 d(
3247 ˇ
3248 ˇ)
3249 ˇ)
3250 );
3251 "});
3252
3253 // test when current indent is more than suggested indent,
3254 // we just move cursor to current indent instead of suggested indent
3255 //
3256 // when no other cursor is at word boundary, all of them should move
3257 cx.set_state(indoc! {"
3258 const a: B = (
3259 c(
3260 d(
3261 ˇ
3262 ˇ )
3263 ˇ )
3264 );
3265 "});
3266 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3267 cx.assert_editor_state(indoc! {"
3268 const a: B = (
3269 c(
3270 d(
3271 ˇ
3272 ˇ)
3273 ˇ)
3274 );
3275 "});
3276 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3277 cx.assert_editor_state(indoc! {"
3278 const a: B = (
3279 c(
3280 d(
3281 ˇ
3282 ˇ)
3283 ˇ)
3284 );
3285 "});
3286
3287 // test when current indent is more than suggested indent,
3288 // we just move cursor to current indent instead of suggested indent
3289 //
3290 // when some other cursor is at word boundary, it doesn't move
3291 cx.set_state(indoc! {"
3292 const a: B = (
3293 c(
3294 d(
3295 ˇ
3296 ˇ )
3297 ˇ)
3298 );
3299 "});
3300 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3301 cx.assert_editor_state(indoc! {"
3302 const a: B = (
3303 c(
3304 d(
3305 ˇ
3306 ˇ)
3307 ˇ)
3308 );
3309 "});
3310
3311 // handle auto-indent when there are multiple cursors on the same line
3312 cx.set_state(indoc! {"
3313 const a: B = (
3314 c(
3315 ˇ ˇ
3316 ˇ )
3317 );
3318 "});
3319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 const a: B = (
3322 c(
3323 ˇ
3324 ˇ)
3325 );
3326 "});
3327}
3328
3329#[gpui::test]
3330async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3331 init_test(cx, |settings| {
3332 settings.defaults.tab_size = NonZeroU32::new(3)
3333 });
3334
3335 let mut cx = EditorTestContext::new(cx).await;
3336 cx.set_state(indoc! {"
3337 ˇ
3338 \t ˇ
3339 \t ˇ
3340 \t ˇ
3341 \t \t\t \t \t\t \t\t \t \t ˇ
3342 "});
3343
3344 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3345 cx.assert_editor_state(indoc! {"
3346 ˇ
3347 \t ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t \t\t \t \t\t \t\t \t \t ˇ
3351 "});
3352}
3353
3354#[gpui::test]
3355async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3356 init_test(cx, |settings| {
3357 settings.defaults.tab_size = NonZeroU32::new(4)
3358 });
3359
3360 let language = Arc::new(
3361 Language::new(
3362 LanguageConfig::default(),
3363 Some(tree_sitter_rust::LANGUAGE.into()),
3364 )
3365 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3366 .unwrap(),
3367 );
3368
3369 let mut cx = EditorTestContext::new(cx).await;
3370 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3371 cx.set_state(indoc! {"
3372 fn a() {
3373 if b {
3374 \t ˇc
3375 }
3376 }
3377 "});
3378
3379 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3380 cx.assert_editor_state(indoc! {"
3381 fn a() {
3382 if b {
3383 ˇc
3384 }
3385 }
3386 "});
3387}
3388
3389#[gpui::test]
3390async fn test_indent_outdent(cx: &mut TestAppContext) {
3391 init_test(cx, |settings| {
3392 settings.defaults.tab_size = NonZeroU32::new(4);
3393 });
3394
3395 let mut cx = EditorTestContext::new(cx).await;
3396
3397 cx.set_state(indoc! {"
3398 «oneˇ» «twoˇ»
3399 three
3400 four
3401 "});
3402 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3403 cx.assert_editor_state(indoc! {"
3404 «oneˇ» «twoˇ»
3405 three
3406 four
3407 "});
3408
3409 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3410 cx.assert_editor_state(indoc! {"
3411 «oneˇ» «twoˇ»
3412 three
3413 four
3414 "});
3415
3416 // select across line ending
3417 cx.set_state(indoc! {"
3418 one two
3419 t«hree
3420 ˇ» four
3421 "});
3422 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3423 cx.assert_editor_state(indoc! {"
3424 one two
3425 t«hree
3426 ˇ» four
3427 "});
3428
3429 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3430 cx.assert_editor_state(indoc! {"
3431 one two
3432 t«hree
3433 ˇ» four
3434 "});
3435
3436 // Ensure that indenting/outdenting works when the cursor is at column 0.
3437 cx.set_state(indoc! {"
3438 one two
3439 ˇthree
3440 four
3441 "});
3442 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3443 cx.assert_editor_state(indoc! {"
3444 one two
3445 ˇthree
3446 four
3447 "});
3448
3449 cx.set_state(indoc! {"
3450 one two
3451 ˇ three
3452 four
3453 "});
3454 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3455 cx.assert_editor_state(indoc! {"
3456 one two
3457 ˇthree
3458 four
3459 "});
3460}
3461
3462#[gpui::test]
3463async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3464 init_test(cx, |settings| {
3465 settings.defaults.hard_tabs = Some(true);
3466 });
3467
3468 let mut cx = EditorTestContext::new(cx).await;
3469
3470 // select two ranges on one line
3471 cx.set_state(indoc! {"
3472 «oneˇ» «twoˇ»
3473 three
3474 four
3475 "});
3476 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3477 cx.assert_editor_state(indoc! {"
3478 \t«oneˇ» «twoˇ»
3479 three
3480 four
3481 "});
3482 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3483 cx.assert_editor_state(indoc! {"
3484 \t\t«oneˇ» «twoˇ»
3485 three
3486 four
3487 "});
3488 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3489 cx.assert_editor_state(indoc! {"
3490 \t«oneˇ» «twoˇ»
3491 three
3492 four
3493 "});
3494 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3495 cx.assert_editor_state(indoc! {"
3496 «oneˇ» «twoˇ»
3497 three
3498 four
3499 "});
3500
3501 // select across a line ending
3502 cx.set_state(indoc! {"
3503 one two
3504 t«hree
3505 ˇ»four
3506 "});
3507 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 \tt«hree
3511 ˇ»four
3512 "});
3513 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3514 cx.assert_editor_state(indoc! {"
3515 one two
3516 \t\tt«hree
3517 ˇ»four
3518 "});
3519 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3520 cx.assert_editor_state(indoc! {"
3521 one two
3522 \tt«hree
3523 ˇ»four
3524 "});
3525 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3526 cx.assert_editor_state(indoc! {"
3527 one two
3528 t«hree
3529 ˇ»four
3530 "});
3531
3532 // Ensure that indenting/outdenting works when the cursor is at column 0.
3533 cx.set_state(indoc! {"
3534 one two
3535 ˇthree
3536 four
3537 "});
3538 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3539 cx.assert_editor_state(indoc! {"
3540 one two
3541 ˇthree
3542 four
3543 "});
3544 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3545 cx.assert_editor_state(indoc! {"
3546 one two
3547 \tˇthree
3548 four
3549 "});
3550 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3551 cx.assert_editor_state(indoc! {"
3552 one two
3553 ˇthree
3554 four
3555 "});
3556}
3557
3558#[gpui::test]
3559fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3560 init_test(cx, |settings| {
3561 settings.languages.extend([
3562 (
3563 "TOML".into(),
3564 LanguageSettingsContent {
3565 tab_size: NonZeroU32::new(2),
3566 ..Default::default()
3567 },
3568 ),
3569 (
3570 "Rust".into(),
3571 LanguageSettingsContent {
3572 tab_size: NonZeroU32::new(4),
3573 ..Default::default()
3574 },
3575 ),
3576 ]);
3577 });
3578
3579 let toml_language = Arc::new(Language::new(
3580 LanguageConfig {
3581 name: "TOML".into(),
3582 ..Default::default()
3583 },
3584 None,
3585 ));
3586 let rust_language = Arc::new(Language::new(
3587 LanguageConfig {
3588 name: "Rust".into(),
3589 ..Default::default()
3590 },
3591 None,
3592 ));
3593
3594 let toml_buffer =
3595 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3596 let rust_buffer =
3597 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3598 let multibuffer = cx.new(|cx| {
3599 let mut multibuffer = MultiBuffer::new(ReadWrite);
3600 multibuffer.push_excerpts(
3601 toml_buffer.clone(),
3602 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3603 cx,
3604 );
3605 multibuffer.push_excerpts(
3606 rust_buffer.clone(),
3607 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3608 cx,
3609 );
3610 multibuffer
3611 });
3612
3613 cx.add_window(|window, cx| {
3614 let mut editor = build_editor(multibuffer, window, cx);
3615
3616 assert_eq!(
3617 editor.text(cx),
3618 indoc! {"
3619 a = 1
3620 b = 2
3621
3622 const c: usize = 3;
3623 "}
3624 );
3625
3626 select_ranges(
3627 &mut editor,
3628 indoc! {"
3629 «aˇ» = 1
3630 b = 2
3631
3632 «const c:ˇ» usize = 3;
3633 "},
3634 window,
3635 cx,
3636 );
3637
3638 editor.tab(&Tab, window, cx);
3639 assert_text_with_selections(
3640 &mut editor,
3641 indoc! {"
3642 «aˇ» = 1
3643 b = 2
3644
3645 «const c:ˇ» usize = 3;
3646 "},
3647 cx,
3648 );
3649 editor.backtab(&Backtab, window, cx);
3650 assert_text_with_selections(
3651 &mut editor,
3652 indoc! {"
3653 «aˇ» = 1
3654 b = 2
3655
3656 «const c:ˇ» usize = 3;
3657 "},
3658 cx,
3659 );
3660
3661 editor
3662 });
3663}
3664
3665#[gpui::test]
3666async fn test_backspace(cx: &mut TestAppContext) {
3667 init_test(cx, |_| {});
3668
3669 let mut cx = EditorTestContext::new(cx).await;
3670
3671 // Basic backspace
3672 cx.set_state(indoc! {"
3673 onˇe two three
3674 fou«rˇ» five six
3675 seven «ˇeight nine
3676 »ten
3677 "});
3678 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3679 cx.assert_editor_state(indoc! {"
3680 oˇe two three
3681 fouˇ five six
3682 seven ˇten
3683 "});
3684
3685 // Test backspace inside and around indents
3686 cx.set_state(indoc! {"
3687 zero
3688 ˇone
3689 ˇtwo
3690 ˇ ˇ ˇ three
3691 ˇ ˇ four
3692 "});
3693 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3694 cx.assert_editor_state(indoc! {"
3695 zero
3696 ˇone
3697 ˇtwo
3698 ˇ threeˇ four
3699 "});
3700}
3701
3702#[gpui::test]
3703async fn test_delete(cx: &mut TestAppContext) {
3704 init_test(cx, |_| {});
3705
3706 let mut cx = EditorTestContext::new(cx).await;
3707 cx.set_state(indoc! {"
3708 onˇe two three
3709 fou«rˇ» five six
3710 seven «ˇeight nine
3711 »ten
3712 "});
3713 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3714 cx.assert_editor_state(indoc! {"
3715 onˇ two three
3716 fouˇ five six
3717 seven ˇten
3718 "});
3719}
3720
3721#[gpui::test]
3722fn test_delete_line(cx: &mut TestAppContext) {
3723 init_test(cx, |_| {});
3724
3725 let editor = cx.add_window(|window, cx| {
3726 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3727 build_editor(buffer, window, cx)
3728 });
3729 _ = editor.update(cx, |editor, window, cx| {
3730 editor.change_selections(None, window, cx, |s| {
3731 s.select_display_ranges([
3732 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3733 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3734 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3735 ])
3736 });
3737 editor.delete_line(&DeleteLine, window, cx);
3738 assert_eq!(editor.display_text(cx), "ghi");
3739 assert_eq!(
3740 editor.selections.display_ranges(cx),
3741 vec![
3742 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3743 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3744 ]
3745 );
3746 });
3747
3748 let editor = cx.add_window(|window, cx| {
3749 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3750 build_editor(buffer, window, cx)
3751 });
3752 _ = editor.update(cx, |editor, window, cx| {
3753 editor.change_selections(None, window, cx, |s| {
3754 s.select_display_ranges([
3755 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3756 ])
3757 });
3758 editor.delete_line(&DeleteLine, window, cx);
3759 assert_eq!(editor.display_text(cx), "ghi\n");
3760 assert_eq!(
3761 editor.selections.display_ranges(cx),
3762 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3763 );
3764 });
3765}
3766
3767#[gpui::test]
3768fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3769 init_test(cx, |_| {});
3770
3771 cx.add_window(|window, cx| {
3772 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3773 let mut editor = build_editor(buffer.clone(), window, cx);
3774 let buffer = buffer.read(cx).as_singleton().unwrap();
3775
3776 assert_eq!(
3777 editor.selections.ranges::<Point>(cx),
3778 &[Point::new(0, 0)..Point::new(0, 0)]
3779 );
3780
3781 // When on single line, replace newline at end by space
3782 editor.join_lines(&JoinLines, window, cx);
3783 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3784 assert_eq!(
3785 editor.selections.ranges::<Point>(cx),
3786 &[Point::new(0, 3)..Point::new(0, 3)]
3787 );
3788
3789 // When multiple lines are selected, remove newlines that are spanned by the selection
3790 editor.change_selections(None, window, cx, |s| {
3791 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3792 });
3793 editor.join_lines(&JoinLines, window, cx);
3794 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3795 assert_eq!(
3796 editor.selections.ranges::<Point>(cx),
3797 &[Point::new(0, 11)..Point::new(0, 11)]
3798 );
3799
3800 // Undo should be transactional
3801 editor.undo(&Undo, window, cx);
3802 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3803 assert_eq!(
3804 editor.selections.ranges::<Point>(cx),
3805 &[Point::new(0, 5)..Point::new(2, 2)]
3806 );
3807
3808 // When joining an empty line don't insert a space
3809 editor.change_selections(None, window, cx, |s| {
3810 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3811 });
3812 editor.join_lines(&JoinLines, window, cx);
3813 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3814 assert_eq!(
3815 editor.selections.ranges::<Point>(cx),
3816 [Point::new(2, 3)..Point::new(2, 3)]
3817 );
3818
3819 // We can remove trailing newlines
3820 editor.join_lines(&JoinLines, window, cx);
3821 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3822 assert_eq!(
3823 editor.selections.ranges::<Point>(cx),
3824 [Point::new(2, 3)..Point::new(2, 3)]
3825 );
3826
3827 // We don't blow up on the last line
3828 editor.join_lines(&JoinLines, window, cx);
3829 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3830 assert_eq!(
3831 editor.selections.ranges::<Point>(cx),
3832 [Point::new(2, 3)..Point::new(2, 3)]
3833 );
3834
3835 // reset to test indentation
3836 editor.buffer.update(cx, |buffer, cx| {
3837 buffer.edit(
3838 [
3839 (Point::new(1, 0)..Point::new(1, 2), " "),
3840 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3841 ],
3842 None,
3843 cx,
3844 )
3845 });
3846
3847 // We remove any leading spaces
3848 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3849 editor.change_selections(None, window, cx, |s| {
3850 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3851 });
3852 editor.join_lines(&JoinLines, window, cx);
3853 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3854
3855 // We don't insert a space for a line containing only spaces
3856 editor.join_lines(&JoinLines, window, cx);
3857 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3858
3859 // We ignore any leading tabs
3860 editor.join_lines(&JoinLines, window, cx);
3861 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3862
3863 editor
3864 });
3865}
3866
3867#[gpui::test]
3868fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3869 init_test(cx, |_| {});
3870
3871 cx.add_window(|window, cx| {
3872 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3873 let mut editor = build_editor(buffer.clone(), window, cx);
3874 let buffer = buffer.read(cx).as_singleton().unwrap();
3875
3876 editor.change_selections(None, window, cx, |s| {
3877 s.select_ranges([
3878 Point::new(0, 2)..Point::new(1, 1),
3879 Point::new(1, 2)..Point::new(1, 2),
3880 Point::new(3, 1)..Point::new(3, 2),
3881 ])
3882 });
3883
3884 editor.join_lines(&JoinLines, window, cx);
3885 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3886
3887 assert_eq!(
3888 editor.selections.ranges::<Point>(cx),
3889 [
3890 Point::new(0, 7)..Point::new(0, 7),
3891 Point::new(1, 3)..Point::new(1, 3)
3892 ]
3893 );
3894 editor
3895 });
3896}
3897
3898#[gpui::test]
3899async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3900 init_test(cx, |_| {});
3901
3902 let mut cx = EditorTestContext::new(cx).await;
3903
3904 let diff_base = r#"
3905 Line 0
3906 Line 1
3907 Line 2
3908 Line 3
3909 "#
3910 .unindent();
3911
3912 cx.set_state(
3913 &r#"
3914 ˇLine 0
3915 Line 1
3916 Line 2
3917 Line 3
3918 "#
3919 .unindent(),
3920 );
3921
3922 cx.set_head_text(&diff_base);
3923 executor.run_until_parked();
3924
3925 // Join lines
3926 cx.update_editor(|editor, window, cx| {
3927 editor.join_lines(&JoinLines, window, cx);
3928 });
3929 executor.run_until_parked();
3930
3931 cx.assert_editor_state(
3932 &r#"
3933 Line 0ˇ Line 1
3934 Line 2
3935 Line 3
3936 "#
3937 .unindent(),
3938 );
3939 // Join again
3940 cx.update_editor(|editor, window, cx| {
3941 editor.join_lines(&JoinLines, window, cx);
3942 });
3943 executor.run_until_parked();
3944
3945 cx.assert_editor_state(
3946 &r#"
3947 Line 0 Line 1ˇ Line 2
3948 Line 3
3949 "#
3950 .unindent(),
3951 );
3952}
3953
3954#[gpui::test]
3955async fn test_custom_newlines_cause_no_false_positive_diffs(
3956 executor: BackgroundExecutor,
3957 cx: &mut TestAppContext,
3958) {
3959 init_test(cx, |_| {});
3960 let mut cx = EditorTestContext::new(cx).await;
3961 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3962 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3963 executor.run_until_parked();
3964
3965 cx.update_editor(|editor, window, cx| {
3966 let snapshot = editor.snapshot(window, cx);
3967 assert_eq!(
3968 snapshot
3969 .buffer_snapshot
3970 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3971 .collect::<Vec<_>>(),
3972 Vec::new(),
3973 "Should not have any diffs for files with custom newlines"
3974 );
3975 });
3976}
3977
3978#[gpui::test]
3979async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3980 init_test(cx, |_| {});
3981
3982 let mut cx = EditorTestContext::new(cx).await;
3983
3984 // Test sort_lines_case_insensitive()
3985 cx.set_state(indoc! {"
3986 «z
3987 y
3988 x
3989 Z
3990 Y
3991 Xˇ»
3992 "});
3993 cx.update_editor(|e, window, cx| {
3994 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3995 });
3996 cx.assert_editor_state(indoc! {"
3997 «x
3998 X
3999 y
4000 Y
4001 z
4002 Zˇ»
4003 "});
4004
4005 // Test reverse_lines()
4006 cx.set_state(indoc! {"
4007 «5
4008 4
4009 3
4010 2
4011 1ˇ»
4012 "});
4013 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4014 cx.assert_editor_state(indoc! {"
4015 «1
4016 2
4017 3
4018 4
4019 5ˇ»
4020 "});
4021
4022 // Skip testing shuffle_line()
4023
4024 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
4025 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
4026
4027 // Don't manipulate when cursor is on single line, but expand the selection
4028 cx.set_state(indoc! {"
4029 ddˇdd
4030 ccc
4031 bb
4032 a
4033 "});
4034 cx.update_editor(|e, window, cx| {
4035 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4036 });
4037 cx.assert_editor_state(indoc! {"
4038 «ddddˇ»
4039 ccc
4040 bb
4041 a
4042 "});
4043
4044 // Basic manipulate case
4045 // Start selection moves to column 0
4046 // End of selection shrinks to fit shorter line
4047 cx.set_state(indoc! {"
4048 dd«d
4049 ccc
4050 bb
4051 aaaaaˇ»
4052 "});
4053 cx.update_editor(|e, window, cx| {
4054 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4055 });
4056 cx.assert_editor_state(indoc! {"
4057 «aaaaa
4058 bb
4059 ccc
4060 dddˇ»
4061 "});
4062
4063 // Manipulate case with newlines
4064 cx.set_state(indoc! {"
4065 dd«d
4066 ccc
4067
4068 bb
4069 aaaaa
4070
4071 ˇ»
4072 "});
4073 cx.update_editor(|e, window, cx| {
4074 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4075 });
4076 cx.assert_editor_state(indoc! {"
4077 «
4078
4079 aaaaa
4080 bb
4081 ccc
4082 dddˇ»
4083
4084 "});
4085
4086 // Adding new line
4087 cx.set_state(indoc! {"
4088 aa«a
4089 bbˇ»b
4090 "});
4091 cx.update_editor(|e, window, cx| {
4092 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4093 });
4094 cx.assert_editor_state(indoc! {"
4095 «aaa
4096 bbb
4097 added_lineˇ»
4098 "});
4099
4100 // Removing line
4101 cx.set_state(indoc! {"
4102 aa«a
4103 bbbˇ»
4104 "});
4105 cx.update_editor(|e, window, cx| {
4106 e.manipulate_lines(window, cx, |lines| {
4107 lines.pop();
4108 })
4109 });
4110 cx.assert_editor_state(indoc! {"
4111 «aaaˇ»
4112 "});
4113
4114 // Removing all lines
4115 cx.set_state(indoc! {"
4116 aa«a
4117 bbbˇ»
4118 "});
4119 cx.update_editor(|e, window, cx| {
4120 e.manipulate_lines(window, cx, |lines| {
4121 lines.drain(..);
4122 })
4123 });
4124 cx.assert_editor_state(indoc! {"
4125 ˇ
4126 "});
4127}
4128
4129#[gpui::test]
4130async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4131 init_test(cx, |_| {});
4132
4133 let mut cx = EditorTestContext::new(cx).await;
4134
4135 // Consider continuous selection as single selection
4136 cx.set_state(indoc! {"
4137 Aaa«aa
4138 cˇ»c«c
4139 bb
4140 aaaˇ»aa
4141 "});
4142 cx.update_editor(|e, window, cx| {
4143 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4144 });
4145 cx.assert_editor_state(indoc! {"
4146 «Aaaaa
4147 ccc
4148 bb
4149 aaaaaˇ»
4150 "});
4151
4152 cx.set_state(indoc! {"
4153 Aaa«aa
4154 cˇ»c«c
4155 bb
4156 aaaˇ»aa
4157 "});
4158 cx.update_editor(|e, window, cx| {
4159 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4160 });
4161 cx.assert_editor_state(indoc! {"
4162 «Aaaaa
4163 ccc
4164 bbˇ»
4165 "});
4166
4167 // Consider non continuous selection as distinct dedup operations
4168 cx.set_state(indoc! {"
4169 «aaaaa
4170 bb
4171 aaaaa
4172 aaaaaˇ»
4173
4174 aaa«aaˇ»
4175 "});
4176 cx.update_editor(|e, window, cx| {
4177 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4178 });
4179 cx.assert_editor_state(indoc! {"
4180 «aaaaa
4181 bbˇ»
4182
4183 «aaaaaˇ»
4184 "});
4185}
4186
4187#[gpui::test]
4188async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4189 init_test(cx, |_| {});
4190
4191 let mut cx = EditorTestContext::new(cx).await;
4192
4193 cx.set_state(indoc! {"
4194 «Aaa
4195 aAa
4196 Aaaˇ»
4197 "});
4198 cx.update_editor(|e, window, cx| {
4199 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4200 });
4201 cx.assert_editor_state(indoc! {"
4202 «Aaa
4203 aAaˇ»
4204 "});
4205
4206 cx.set_state(indoc! {"
4207 «Aaa
4208 aAa
4209 aaAˇ»
4210 "});
4211 cx.update_editor(|e, window, cx| {
4212 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4213 });
4214 cx.assert_editor_state(indoc! {"
4215 «Aaaˇ»
4216 "});
4217}
4218
4219#[gpui::test]
4220async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4221 init_test(cx, |_| {});
4222
4223 let mut cx = EditorTestContext::new(cx).await;
4224
4225 // Manipulate with multiple selections on a single line
4226 cx.set_state(indoc! {"
4227 dd«dd
4228 cˇ»c«c
4229 bb
4230 aaaˇ»aa
4231 "});
4232 cx.update_editor(|e, window, cx| {
4233 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4234 });
4235 cx.assert_editor_state(indoc! {"
4236 «aaaaa
4237 bb
4238 ccc
4239 ddddˇ»
4240 "});
4241
4242 // Manipulate with multiple disjoin selections
4243 cx.set_state(indoc! {"
4244 5«
4245 4
4246 3
4247 2
4248 1ˇ»
4249
4250 dd«dd
4251 ccc
4252 bb
4253 aaaˇ»aa
4254 "});
4255 cx.update_editor(|e, window, cx| {
4256 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4257 });
4258 cx.assert_editor_state(indoc! {"
4259 «1
4260 2
4261 3
4262 4
4263 5ˇ»
4264
4265 «aaaaa
4266 bb
4267 ccc
4268 ddddˇ»
4269 "});
4270
4271 // Adding lines on each selection
4272 cx.set_state(indoc! {"
4273 2«
4274 1ˇ»
4275
4276 bb«bb
4277 aaaˇ»aa
4278 "});
4279 cx.update_editor(|e, window, cx| {
4280 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4281 });
4282 cx.assert_editor_state(indoc! {"
4283 «2
4284 1
4285 added lineˇ»
4286
4287 «bbbb
4288 aaaaa
4289 added lineˇ»
4290 "});
4291
4292 // Removing lines on each selection
4293 cx.set_state(indoc! {"
4294 2«
4295 1ˇ»
4296
4297 bb«bb
4298 aaaˇ»aa
4299 "});
4300 cx.update_editor(|e, window, cx| {
4301 e.manipulate_lines(window, cx, |lines| {
4302 lines.pop();
4303 })
4304 });
4305 cx.assert_editor_state(indoc! {"
4306 «2ˇ»
4307
4308 «bbbbˇ»
4309 "});
4310}
4311
4312#[gpui::test]
4313async fn test_toggle_case(cx: &mut TestAppContext) {
4314 init_test(cx, |_| {});
4315
4316 let mut cx = EditorTestContext::new(cx).await;
4317
4318 // If all lower case -> upper case
4319 cx.set_state(indoc! {"
4320 «hello worldˇ»
4321 "});
4322 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4323 cx.assert_editor_state(indoc! {"
4324 «HELLO WORLDˇ»
4325 "});
4326
4327 // If all upper case -> lower case
4328 cx.set_state(indoc! {"
4329 «HELLO WORLDˇ»
4330 "});
4331 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4332 cx.assert_editor_state(indoc! {"
4333 «hello worldˇ»
4334 "});
4335
4336 // If any upper case characters are identified -> lower case
4337 // This matches JetBrains IDEs
4338 cx.set_state(indoc! {"
4339 «hEllo worldˇ»
4340 "});
4341 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4342 cx.assert_editor_state(indoc! {"
4343 «hello worldˇ»
4344 "});
4345}
4346
4347#[gpui::test]
4348async fn test_manipulate_text(cx: &mut TestAppContext) {
4349 init_test(cx, |_| {});
4350
4351 let mut cx = EditorTestContext::new(cx).await;
4352
4353 // Test convert_to_upper_case()
4354 cx.set_state(indoc! {"
4355 «hello worldˇ»
4356 "});
4357 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4358 cx.assert_editor_state(indoc! {"
4359 «HELLO WORLDˇ»
4360 "});
4361
4362 // Test convert_to_lower_case()
4363 cx.set_state(indoc! {"
4364 «HELLO WORLDˇ»
4365 "});
4366 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4367 cx.assert_editor_state(indoc! {"
4368 «hello worldˇ»
4369 "});
4370
4371 // Test multiple line, single selection case
4372 cx.set_state(indoc! {"
4373 «The quick brown
4374 fox jumps over
4375 the lazy dogˇ»
4376 "});
4377 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4378 cx.assert_editor_state(indoc! {"
4379 «The Quick Brown
4380 Fox Jumps Over
4381 The Lazy Dogˇ»
4382 "});
4383
4384 // Test multiple line, single selection case
4385 cx.set_state(indoc! {"
4386 «The quick brown
4387 fox jumps over
4388 the lazy dogˇ»
4389 "});
4390 cx.update_editor(|e, window, cx| {
4391 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4392 });
4393 cx.assert_editor_state(indoc! {"
4394 «TheQuickBrown
4395 FoxJumpsOver
4396 TheLazyDogˇ»
4397 "});
4398
4399 // From here on out, test more complex cases of manipulate_text()
4400
4401 // Test no selection case - should affect words cursors are in
4402 // Cursor at beginning, middle, and end of word
4403 cx.set_state(indoc! {"
4404 ˇhello big beauˇtiful worldˇ
4405 "});
4406 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4407 cx.assert_editor_state(indoc! {"
4408 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4409 "});
4410
4411 // Test multiple selections on a single line and across multiple lines
4412 cx.set_state(indoc! {"
4413 «Theˇ» quick «brown
4414 foxˇ» jumps «overˇ»
4415 the «lazyˇ» dog
4416 "});
4417 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4418 cx.assert_editor_state(indoc! {"
4419 «THEˇ» quick «BROWN
4420 FOXˇ» jumps «OVERˇ»
4421 the «LAZYˇ» dog
4422 "});
4423
4424 // Test case where text length grows
4425 cx.set_state(indoc! {"
4426 «tschüߡ»
4427 "});
4428 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4429 cx.assert_editor_state(indoc! {"
4430 «TSCHÜSSˇ»
4431 "});
4432
4433 // Test to make sure we don't crash when text shrinks
4434 cx.set_state(indoc! {"
4435 aaa_bbbˇ
4436 "});
4437 cx.update_editor(|e, window, cx| {
4438 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4439 });
4440 cx.assert_editor_state(indoc! {"
4441 «aaaBbbˇ»
4442 "});
4443
4444 // Test to make sure we all aware of the fact that each word can grow and shrink
4445 // Final selections should be aware of this fact
4446 cx.set_state(indoc! {"
4447 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4448 "});
4449 cx.update_editor(|e, window, cx| {
4450 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4451 });
4452 cx.assert_editor_state(indoc! {"
4453 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4454 "});
4455
4456 cx.set_state(indoc! {"
4457 «hElLo, WoRld!ˇ»
4458 "});
4459 cx.update_editor(|e, window, cx| {
4460 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4461 });
4462 cx.assert_editor_state(indoc! {"
4463 «HeLlO, wOrLD!ˇ»
4464 "});
4465}
4466
4467#[gpui::test]
4468fn test_duplicate_line(cx: &mut TestAppContext) {
4469 init_test(cx, |_| {});
4470
4471 let editor = cx.add_window(|window, cx| {
4472 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4473 build_editor(buffer, window, cx)
4474 });
4475 _ = editor.update(cx, |editor, window, cx| {
4476 editor.change_selections(None, window, cx, |s| {
4477 s.select_display_ranges([
4478 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4479 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4480 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4481 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4482 ])
4483 });
4484 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4485 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4486 assert_eq!(
4487 editor.selections.display_ranges(cx),
4488 vec![
4489 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4490 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4491 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4492 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4493 ]
4494 );
4495 });
4496
4497 let editor = cx.add_window(|window, cx| {
4498 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4499 build_editor(buffer, window, cx)
4500 });
4501 _ = editor.update(cx, |editor, window, cx| {
4502 editor.change_selections(None, window, cx, |s| {
4503 s.select_display_ranges([
4504 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4505 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4506 ])
4507 });
4508 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4509 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4510 assert_eq!(
4511 editor.selections.display_ranges(cx),
4512 vec![
4513 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4514 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4515 ]
4516 );
4517 });
4518
4519 // With `move_upwards` the selections stay in place, except for
4520 // the lines inserted above them
4521 let editor = cx.add_window(|window, cx| {
4522 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4523 build_editor(buffer, window, cx)
4524 });
4525 _ = editor.update(cx, |editor, window, cx| {
4526 editor.change_selections(None, window, cx, |s| {
4527 s.select_display_ranges([
4528 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4529 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4530 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4531 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4532 ])
4533 });
4534 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4535 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4536 assert_eq!(
4537 editor.selections.display_ranges(cx),
4538 vec![
4539 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4540 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4541 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4542 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4543 ]
4544 );
4545 });
4546
4547 let editor = cx.add_window(|window, cx| {
4548 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4549 build_editor(buffer, window, cx)
4550 });
4551 _ = editor.update(cx, |editor, window, cx| {
4552 editor.change_selections(None, window, cx, |s| {
4553 s.select_display_ranges([
4554 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4555 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4556 ])
4557 });
4558 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4559 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4560 assert_eq!(
4561 editor.selections.display_ranges(cx),
4562 vec![
4563 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4565 ]
4566 );
4567 });
4568
4569 let editor = cx.add_window(|window, cx| {
4570 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4571 build_editor(buffer, window, cx)
4572 });
4573 _ = editor.update(cx, |editor, window, cx| {
4574 editor.change_selections(None, window, cx, |s| {
4575 s.select_display_ranges([
4576 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4577 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4578 ])
4579 });
4580 editor.duplicate_selection(&DuplicateSelection, window, cx);
4581 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4582 assert_eq!(
4583 editor.selections.display_ranges(cx),
4584 vec![
4585 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4586 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4587 ]
4588 );
4589 });
4590}
4591
4592#[gpui::test]
4593fn test_move_line_up_down(cx: &mut TestAppContext) {
4594 init_test(cx, |_| {});
4595
4596 let editor = cx.add_window(|window, cx| {
4597 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4598 build_editor(buffer, window, cx)
4599 });
4600 _ = editor.update(cx, |editor, window, cx| {
4601 editor.fold_creases(
4602 vec![
4603 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4604 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4605 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4606 ],
4607 true,
4608 window,
4609 cx,
4610 );
4611 editor.change_selections(None, window, cx, |s| {
4612 s.select_display_ranges([
4613 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4614 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4615 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4616 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4617 ])
4618 });
4619 assert_eq!(
4620 editor.display_text(cx),
4621 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4622 );
4623
4624 editor.move_line_up(&MoveLineUp, window, cx);
4625 assert_eq!(
4626 editor.display_text(cx),
4627 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4628 );
4629 assert_eq!(
4630 editor.selections.display_ranges(cx),
4631 vec![
4632 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4633 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4634 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4635 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4636 ]
4637 );
4638 });
4639
4640 _ = editor.update(cx, |editor, window, cx| {
4641 editor.move_line_down(&MoveLineDown, window, cx);
4642 assert_eq!(
4643 editor.display_text(cx),
4644 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4645 );
4646 assert_eq!(
4647 editor.selections.display_ranges(cx),
4648 vec![
4649 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4650 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4651 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4652 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4653 ]
4654 );
4655 });
4656
4657 _ = editor.update(cx, |editor, window, cx| {
4658 editor.move_line_down(&MoveLineDown, window, cx);
4659 assert_eq!(
4660 editor.display_text(cx),
4661 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4662 );
4663 assert_eq!(
4664 editor.selections.display_ranges(cx),
4665 vec![
4666 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4667 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4668 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4669 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4670 ]
4671 );
4672 });
4673
4674 _ = editor.update(cx, |editor, window, cx| {
4675 editor.move_line_up(&MoveLineUp, window, cx);
4676 assert_eq!(
4677 editor.display_text(cx),
4678 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4679 );
4680 assert_eq!(
4681 editor.selections.display_ranges(cx),
4682 vec![
4683 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4684 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4685 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4686 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4687 ]
4688 );
4689 });
4690}
4691
4692#[gpui::test]
4693fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4694 init_test(cx, |_| {});
4695
4696 let editor = cx.add_window(|window, cx| {
4697 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4698 build_editor(buffer, window, cx)
4699 });
4700 _ = editor.update(cx, |editor, window, cx| {
4701 let snapshot = editor.buffer.read(cx).snapshot(cx);
4702 editor.insert_blocks(
4703 [BlockProperties {
4704 style: BlockStyle::Fixed,
4705 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4706 height: Some(1),
4707 render: Arc::new(|_| div().into_any()),
4708 priority: 0,
4709 render_in_minimap: true,
4710 }],
4711 Some(Autoscroll::fit()),
4712 cx,
4713 );
4714 editor.change_selections(None, window, cx, |s| {
4715 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4716 });
4717 editor.move_line_down(&MoveLineDown, window, cx);
4718 });
4719}
4720
4721#[gpui::test]
4722async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4723 init_test(cx, |_| {});
4724
4725 let mut cx = EditorTestContext::new(cx).await;
4726 cx.set_state(
4727 &"
4728 ˇzero
4729 one
4730 two
4731 three
4732 four
4733 five
4734 "
4735 .unindent(),
4736 );
4737
4738 // Create a four-line block that replaces three lines of text.
4739 cx.update_editor(|editor, window, cx| {
4740 let snapshot = editor.snapshot(window, cx);
4741 let snapshot = &snapshot.buffer_snapshot;
4742 let placement = BlockPlacement::Replace(
4743 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4744 );
4745 editor.insert_blocks(
4746 [BlockProperties {
4747 placement,
4748 height: Some(4),
4749 style: BlockStyle::Sticky,
4750 render: Arc::new(|_| gpui::div().into_any_element()),
4751 priority: 0,
4752 render_in_minimap: true,
4753 }],
4754 None,
4755 cx,
4756 );
4757 });
4758
4759 // Move down so that the cursor touches the block.
4760 cx.update_editor(|editor, window, cx| {
4761 editor.move_down(&Default::default(), window, cx);
4762 });
4763 cx.assert_editor_state(
4764 &"
4765 zero
4766 «one
4767 two
4768 threeˇ»
4769 four
4770 five
4771 "
4772 .unindent(),
4773 );
4774
4775 // Move down past the block.
4776 cx.update_editor(|editor, window, cx| {
4777 editor.move_down(&Default::default(), window, cx);
4778 });
4779 cx.assert_editor_state(
4780 &"
4781 zero
4782 one
4783 two
4784 three
4785 ˇfour
4786 five
4787 "
4788 .unindent(),
4789 );
4790}
4791
4792#[gpui::test]
4793fn test_transpose(cx: &mut TestAppContext) {
4794 init_test(cx, |_| {});
4795
4796 _ = cx.add_window(|window, cx| {
4797 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4798 editor.set_style(EditorStyle::default(), window, cx);
4799 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4800 editor.transpose(&Default::default(), window, cx);
4801 assert_eq!(editor.text(cx), "bac");
4802 assert_eq!(editor.selections.ranges(cx), [2..2]);
4803
4804 editor.transpose(&Default::default(), window, cx);
4805 assert_eq!(editor.text(cx), "bca");
4806 assert_eq!(editor.selections.ranges(cx), [3..3]);
4807
4808 editor.transpose(&Default::default(), window, cx);
4809 assert_eq!(editor.text(cx), "bac");
4810 assert_eq!(editor.selections.ranges(cx), [3..3]);
4811
4812 editor
4813 });
4814
4815 _ = cx.add_window(|window, cx| {
4816 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4817 editor.set_style(EditorStyle::default(), window, cx);
4818 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4819 editor.transpose(&Default::default(), window, cx);
4820 assert_eq!(editor.text(cx), "acb\nde");
4821 assert_eq!(editor.selections.ranges(cx), [3..3]);
4822
4823 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4824 editor.transpose(&Default::default(), window, cx);
4825 assert_eq!(editor.text(cx), "acbd\ne");
4826 assert_eq!(editor.selections.ranges(cx), [5..5]);
4827
4828 editor.transpose(&Default::default(), window, cx);
4829 assert_eq!(editor.text(cx), "acbde\n");
4830 assert_eq!(editor.selections.ranges(cx), [6..6]);
4831
4832 editor.transpose(&Default::default(), window, cx);
4833 assert_eq!(editor.text(cx), "acbd\ne");
4834 assert_eq!(editor.selections.ranges(cx), [6..6]);
4835
4836 editor
4837 });
4838
4839 _ = cx.add_window(|window, cx| {
4840 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4841 editor.set_style(EditorStyle::default(), window, cx);
4842 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4843 editor.transpose(&Default::default(), window, cx);
4844 assert_eq!(editor.text(cx), "bacd\ne");
4845 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4846
4847 editor.transpose(&Default::default(), window, cx);
4848 assert_eq!(editor.text(cx), "bcade\n");
4849 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4850
4851 editor.transpose(&Default::default(), window, cx);
4852 assert_eq!(editor.text(cx), "bcda\ne");
4853 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4854
4855 editor.transpose(&Default::default(), window, cx);
4856 assert_eq!(editor.text(cx), "bcade\n");
4857 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4858
4859 editor.transpose(&Default::default(), window, cx);
4860 assert_eq!(editor.text(cx), "bcaed\n");
4861 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4862
4863 editor
4864 });
4865
4866 _ = cx.add_window(|window, cx| {
4867 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4868 editor.set_style(EditorStyle::default(), window, cx);
4869 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4870 editor.transpose(&Default::default(), window, cx);
4871 assert_eq!(editor.text(cx), "🏀🍐✋");
4872 assert_eq!(editor.selections.ranges(cx), [8..8]);
4873
4874 editor.transpose(&Default::default(), window, cx);
4875 assert_eq!(editor.text(cx), "🏀✋🍐");
4876 assert_eq!(editor.selections.ranges(cx), [11..11]);
4877
4878 editor.transpose(&Default::default(), window, cx);
4879 assert_eq!(editor.text(cx), "🏀🍐✋");
4880 assert_eq!(editor.selections.ranges(cx), [11..11]);
4881
4882 editor
4883 });
4884}
4885
4886#[gpui::test]
4887async fn test_rewrap(cx: &mut TestAppContext) {
4888 init_test(cx, |settings| {
4889 settings.languages.extend([
4890 (
4891 "Markdown".into(),
4892 LanguageSettingsContent {
4893 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4894 ..Default::default()
4895 },
4896 ),
4897 (
4898 "Plain Text".into(),
4899 LanguageSettingsContent {
4900 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4901 ..Default::default()
4902 },
4903 ),
4904 ])
4905 });
4906
4907 let mut cx = EditorTestContext::new(cx).await;
4908
4909 let language_with_c_comments = Arc::new(Language::new(
4910 LanguageConfig {
4911 line_comments: vec!["// ".into()],
4912 ..LanguageConfig::default()
4913 },
4914 None,
4915 ));
4916 let language_with_pound_comments = Arc::new(Language::new(
4917 LanguageConfig {
4918 line_comments: vec!["# ".into()],
4919 ..LanguageConfig::default()
4920 },
4921 None,
4922 ));
4923 let markdown_language = Arc::new(Language::new(
4924 LanguageConfig {
4925 name: "Markdown".into(),
4926 ..LanguageConfig::default()
4927 },
4928 None,
4929 ));
4930 let language_with_doc_comments = Arc::new(Language::new(
4931 LanguageConfig {
4932 line_comments: vec!["// ".into(), "/// ".into()],
4933 ..LanguageConfig::default()
4934 },
4935 Some(tree_sitter_rust::LANGUAGE.into()),
4936 ));
4937
4938 let plaintext_language = Arc::new(Language::new(
4939 LanguageConfig {
4940 name: "Plain Text".into(),
4941 ..LanguageConfig::default()
4942 },
4943 None,
4944 ));
4945
4946 assert_rewrap(
4947 indoc! {"
4948 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4949 "},
4950 indoc! {"
4951 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4952 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4953 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4954 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4955 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4956 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4957 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4958 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4959 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4960 // porttitor id. Aliquam id accumsan eros.
4961 "},
4962 language_with_c_comments.clone(),
4963 &mut cx,
4964 );
4965
4966 // Test that rewrapping works inside of a selection
4967 assert_rewrap(
4968 indoc! {"
4969 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4970 "},
4971 indoc! {"
4972 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4973 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4974 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4975 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4976 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4977 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4978 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4979 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4980 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4981 // porttitor id. Aliquam id accumsan eros.ˇ»
4982 "},
4983 language_with_c_comments.clone(),
4984 &mut cx,
4985 );
4986
4987 // Test that cursors that expand to the same region are collapsed.
4988 assert_rewrap(
4989 indoc! {"
4990 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4991 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4992 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4993 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4994 "},
4995 indoc! {"
4996 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4997 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4998 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4999 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5000 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5001 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5002 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5003 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5004 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5005 // porttitor id. Aliquam id accumsan eros.
5006 "},
5007 language_with_c_comments.clone(),
5008 &mut cx,
5009 );
5010
5011 // Test that non-contiguous selections are treated separately.
5012 assert_rewrap(
5013 indoc! {"
5014 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5015 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5016 //
5017 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5018 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
5019 "},
5020 indoc! {"
5021 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5022 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5023 // auctor, eu lacinia sapien scelerisque.
5024 //
5025 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5026 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5027 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5028 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5029 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5030 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5031 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5032 "},
5033 language_with_c_comments.clone(),
5034 &mut cx,
5035 );
5036
5037 // Test that different comment prefixes are supported.
5038 assert_rewrap(
5039 indoc! {"
5040 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
5041 "},
5042 indoc! {"
5043 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5044 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5045 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5046 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5047 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5048 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5049 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5050 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5051 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5052 # accumsan eros.
5053 "},
5054 language_with_pound_comments.clone(),
5055 &mut cx,
5056 );
5057
5058 // Test that rewrapping is ignored outside of comments in most languages.
5059 assert_rewrap(
5060 indoc! {"
5061 /// Adds two numbers.
5062 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5063 fn add(a: u32, b: u32) -> u32 {
5064 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
5065 }
5066 "},
5067 indoc! {"
5068 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5069 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5070 fn add(a: u32, b: u32) -> u32 {
5071 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
5072 }
5073 "},
5074 language_with_doc_comments.clone(),
5075 &mut cx,
5076 );
5077
5078 // Test that rewrapping works in Markdown and Plain Text languages.
5079 assert_rewrap(
5080 indoc! {"
5081 # Hello
5082
5083 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5084 "},
5085 indoc! {"
5086 # Hello
5087
5088 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5089 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5090 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5091 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5092 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5093 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5094 Integer sit amet scelerisque nisi.
5095 "},
5096 markdown_language,
5097 &mut cx,
5098 );
5099
5100 assert_rewrap(
5101 indoc! {"
5102 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5103 "},
5104 indoc! {"
5105 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5106 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5107 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5108 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5109 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5110 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5111 Integer sit amet scelerisque nisi.
5112 "},
5113 plaintext_language,
5114 &mut cx,
5115 );
5116
5117 // Test rewrapping unaligned comments in a selection.
5118 assert_rewrap(
5119 indoc! {"
5120 fn foo() {
5121 if true {
5122 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5123 // Praesent semper egestas tellus id dignissim.ˇ»
5124 do_something();
5125 } else {
5126 //
5127 }
5128 }
5129 "},
5130 indoc! {"
5131 fn foo() {
5132 if true {
5133 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5134 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5135 // egestas tellus id dignissim.ˇ»
5136 do_something();
5137 } else {
5138 //
5139 }
5140 }
5141 "},
5142 language_with_doc_comments.clone(),
5143 &mut cx,
5144 );
5145
5146 assert_rewrap(
5147 indoc! {"
5148 fn foo() {
5149 if true {
5150 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5151 // Praesent semper egestas tellus id dignissim.»
5152 do_something();
5153 } else {
5154 //
5155 }
5156
5157 }
5158 "},
5159 indoc! {"
5160 fn foo() {
5161 if true {
5162 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5163 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5164 // egestas tellus id dignissim.»
5165 do_something();
5166 } else {
5167 //
5168 }
5169
5170 }
5171 "},
5172 language_with_doc_comments.clone(),
5173 &mut cx,
5174 );
5175
5176 #[track_caller]
5177 fn assert_rewrap(
5178 unwrapped_text: &str,
5179 wrapped_text: &str,
5180 language: Arc<Language>,
5181 cx: &mut EditorTestContext,
5182 ) {
5183 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5184 cx.set_state(unwrapped_text);
5185 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5186 cx.assert_editor_state(wrapped_text);
5187 }
5188}
5189
5190#[gpui::test]
5191async fn test_hard_wrap(cx: &mut TestAppContext) {
5192 init_test(cx, |_| {});
5193 let mut cx = EditorTestContext::new(cx).await;
5194
5195 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5196 cx.update_editor(|editor, _, cx| {
5197 editor.set_hard_wrap(Some(14), cx);
5198 });
5199
5200 cx.set_state(indoc!(
5201 "
5202 one two three ˇ
5203 "
5204 ));
5205 cx.simulate_input("four");
5206 cx.run_until_parked();
5207
5208 cx.assert_editor_state(indoc!(
5209 "
5210 one two three
5211 fourˇ
5212 "
5213 ));
5214
5215 cx.update_editor(|editor, window, cx| {
5216 editor.newline(&Default::default(), window, cx);
5217 });
5218 cx.run_until_parked();
5219 cx.assert_editor_state(indoc!(
5220 "
5221 one two three
5222 four
5223 ˇ
5224 "
5225 ));
5226
5227 cx.simulate_input("five");
5228 cx.run_until_parked();
5229 cx.assert_editor_state(indoc!(
5230 "
5231 one two three
5232 four
5233 fiveˇ
5234 "
5235 ));
5236
5237 cx.update_editor(|editor, window, cx| {
5238 editor.newline(&Default::default(), window, cx);
5239 });
5240 cx.run_until_parked();
5241 cx.simulate_input("# ");
5242 cx.run_until_parked();
5243 cx.assert_editor_state(indoc!(
5244 "
5245 one two three
5246 four
5247 five
5248 # ˇ
5249 "
5250 ));
5251
5252 cx.update_editor(|editor, window, cx| {
5253 editor.newline(&Default::default(), window, cx);
5254 });
5255 cx.run_until_parked();
5256 cx.assert_editor_state(indoc!(
5257 "
5258 one two three
5259 four
5260 five
5261 #\x20
5262 #ˇ
5263 "
5264 ));
5265
5266 cx.simulate_input(" 6");
5267 cx.run_until_parked();
5268 cx.assert_editor_state(indoc!(
5269 "
5270 one two three
5271 four
5272 five
5273 #
5274 # 6ˇ
5275 "
5276 ));
5277}
5278
5279#[gpui::test]
5280async fn test_clipboard(cx: &mut TestAppContext) {
5281 init_test(cx, |_| {});
5282
5283 let mut cx = EditorTestContext::new(cx).await;
5284
5285 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5286 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5287 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5288
5289 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5290 cx.set_state("two ˇfour ˇsix ˇ");
5291 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5292 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5293
5294 // Paste again but with only two cursors. Since the number of cursors doesn't
5295 // match the number of slices in the clipboard, the entire clipboard text
5296 // is pasted at each cursor.
5297 cx.set_state("ˇtwo one✅ four three six five ˇ");
5298 cx.update_editor(|e, window, cx| {
5299 e.handle_input("( ", window, cx);
5300 e.paste(&Paste, window, cx);
5301 e.handle_input(") ", window, cx);
5302 });
5303 cx.assert_editor_state(
5304 &([
5305 "( one✅ ",
5306 "three ",
5307 "five ) ˇtwo one✅ four three six five ( one✅ ",
5308 "three ",
5309 "five ) ˇ",
5310 ]
5311 .join("\n")),
5312 );
5313
5314 // Cut with three selections, one of which is full-line.
5315 cx.set_state(indoc! {"
5316 1«2ˇ»3
5317 4ˇ567
5318 «8ˇ»9"});
5319 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5320 cx.assert_editor_state(indoc! {"
5321 1ˇ3
5322 ˇ9"});
5323
5324 // Paste with three selections, noticing how the copied selection that was full-line
5325 // gets inserted before the second cursor.
5326 cx.set_state(indoc! {"
5327 1ˇ3
5328 9ˇ
5329 «oˇ»ne"});
5330 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5331 cx.assert_editor_state(indoc! {"
5332 12ˇ3
5333 4567
5334 9ˇ
5335 8ˇne"});
5336
5337 // Copy with a single cursor only, which writes the whole line into the clipboard.
5338 cx.set_state(indoc! {"
5339 The quick brown
5340 fox juˇmps over
5341 the lazy dog"});
5342 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5343 assert_eq!(
5344 cx.read_from_clipboard()
5345 .and_then(|item| item.text().as_deref().map(str::to_string)),
5346 Some("fox jumps over\n".to_string())
5347 );
5348
5349 // Paste with three selections, noticing how the copied full-line selection is inserted
5350 // before the empty selections but replaces the selection that is non-empty.
5351 cx.set_state(indoc! {"
5352 Tˇhe quick brown
5353 «foˇ»x jumps over
5354 tˇhe lazy dog"});
5355 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5356 cx.assert_editor_state(indoc! {"
5357 fox jumps over
5358 Tˇhe quick brown
5359 fox jumps over
5360 ˇx jumps over
5361 fox jumps over
5362 tˇhe lazy dog"});
5363}
5364
5365#[gpui::test]
5366async fn test_copy_trim(cx: &mut TestAppContext) {
5367 init_test(cx, |_| {});
5368
5369 let mut cx = EditorTestContext::new(cx).await;
5370 cx.set_state(
5371 r#" «for selection in selections.iter() {
5372 let mut start = selection.start;
5373 let mut end = selection.end;
5374 let is_entire_line = selection.is_empty();
5375 if is_entire_line {
5376 start = Point::new(start.row, 0);ˇ»
5377 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5378 }
5379 "#,
5380 );
5381 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5382 assert_eq!(
5383 cx.read_from_clipboard()
5384 .and_then(|item| item.text().as_deref().map(str::to_string)),
5385 Some(
5386 "for selection in selections.iter() {
5387 let mut start = selection.start;
5388 let mut end = selection.end;
5389 let is_entire_line = selection.is_empty();
5390 if is_entire_line {
5391 start = Point::new(start.row, 0);"
5392 .to_string()
5393 ),
5394 "Regular copying preserves all indentation selected",
5395 );
5396 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5397 assert_eq!(
5398 cx.read_from_clipboard()
5399 .and_then(|item| item.text().as_deref().map(str::to_string)),
5400 Some(
5401 "for selection in selections.iter() {
5402let mut start = selection.start;
5403let mut end = selection.end;
5404let is_entire_line = selection.is_empty();
5405if is_entire_line {
5406 start = Point::new(start.row, 0);"
5407 .to_string()
5408 ),
5409 "Copying with stripping should strip all leading whitespaces"
5410 );
5411
5412 cx.set_state(
5413 r#" « for selection in selections.iter() {
5414 let mut start = selection.start;
5415 let mut end = selection.end;
5416 let is_entire_line = selection.is_empty();
5417 if is_entire_line {
5418 start = Point::new(start.row, 0);ˇ»
5419 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5420 }
5421 "#,
5422 );
5423 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5424 assert_eq!(
5425 cx.read_from_clipboard()
5426 .and_then(|item| item.text().as_deref().map(str::to_string)),
5427 Some(
5428 " for selection in selections.iter() {
5429 let mut start = selection.start;
5430 let mut end = selection.end;
5431 let is_entire_line = selection.is_empty();
5432 if is_entire_line {
5433 start = Point::new(start.row, 0);"
5434 .to_string()
5435 ),
5436 "Regular copying preserves all indentation selected",
5437 );
5438 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5439 assert_eq!(
5440 cx.read_from_clipboard()
5441 .and_then(|item| item.text().as_deref().map(str::to_string)),
5442 Some(
5443 "for selection in selections.iter() {
5444let mut start = selection.start;
5445let mut end = selection.end;
5446let is_entire_line = selection.is_empty();
5447if is_entire_line {
5448 start = Point::new(start.row, 0);"
5449 .to_string()
5450 ),
5451 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5452 );
5453
5454 cx.set_state(
5455 r#" «ˇ for selection in selections.iter() {
5456 let mut start = selection.start;
5457 let mut end = selection.end;
5458 let is_entire_line = selection.is_empty();
5459 if is_entire_line {
5460 start = Point::new(start.row, 0);»
5461 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5462 }
5463 "#,
5464 );
5465 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5466 assert_eq!(
5467 cx.read_from_clipboard()
5468 .and_then(|item| item.text().as_deref().map(str::to_string)),
5469 Some(
5470 " for selection in selections.iter() {
5471 let mut start = selection.start;
5472 let mut end = selection.end;
5473 let is_entire_line = selection.is_empty();
5474 if is_entire_line {
5475 start = Point::new(start.row, 0);"
5476 .to_string()
5477 ),
5478 "Regular copying for reverse selection works the same",
5479 );
5480 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5481 assert_eq!(
5482 cx.read_from_clipboard()
5483 .and_then(|item| item.text().as_deref().map(str::to_string)),
5484 Some(
5485 "for selection in selections.iter() {
5486let mut start = selection.start;
5487let mut end = selection.end;
5488let is_entire_line = selection.is_empty();
5489if is_entire_line {
5490 start = Point::new(start.row, 0);"
5491 .to_string()
5492 ),
5493 "Copying with stripping for reverse selection works the same"
5494 );
5495
5496 cx.set_state(
5497 r#" for selection «in selections.iter() {
5498 let mut start = selection.start;
5499 let mut end = selection.end;
5500 let is_entire_line = selection.is_empty();
5501 if is_entire_line {
5502 start = Point::new(start.row, 0);ˇ»
5503 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5504 }
5505 "#,
5506 );
5507 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5508 assert_eq!(
5509 cx.read_from_clipboard()
5510 .and_then(|item| item.text().as_deref().map(str::to_string)),
5511 Some(
5512 "in selections.iter() {
5513 let mut start = selection.start;
5514 let mut end = selection.end;
5515 let is_entire_line = selection.is_empty();
5516 if is_entire_line {
5517 start = Point::new(start.row, 0);"
5518 .to_string()
5519 ),
5520 "When selecting past the indent, the copying works as usual",
5521 );
5522 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5523 assert_eq!(
5524 cx.read_from_clipboard()
5525 .and_then(|item| item.text().as_deref().map(str::to_string)),
5526 Some(
5527 "in selections.iter() {
5528 let mut start = selection.start;
5529 let mut end = selection.end;
5530 let is_entire_line = selection.is_empty();
5531 if is_entire_line {
5532 start = Point::new(start.row, 0);"
5533 .to_string()
5534 ),
5535 "When selecting past the indent, nothing is trimmed"
5536 );
5537
5538 cx.set_state(
5539 r#" «for selection in selections.iter() {
5540 let mut start = selection.start;
5541
5542 let mut end = selection.end;
5543 let is_entire_line = selection.is_empty();
5544 if is_entire_line {
5545 start = Point::new(start.row, 0);
5546ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5547 }
5548 "#,
5549 );
5550 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5551 assert_eq!(
5552 cx.read_from_clipboard()
5553 .and_then(|item| item.text().as_deref().map(str::to_string)),
5554 Some(
5555 "for selection in selections.iter() {
5556let mut start = selection.start;
5557
5558let mut end = selection.end;
5559let is_entire_line = selection.is_empty();
5560if is_entire_line {
5561 start = Point::new(start.row, 0);
5562"
5563 .to_string()
5564 ),
5565 "Copying with stripping should ignore empty lines"
5566 );
5567}
5568
5569#[gpui::test]
5570async fn test_paste_multiline(cx: &mut TestAppContext) {
5571 init_test(cx, |_| {});
5572
5573 let mut cx = EditorTestContext::new(cx).await;
5574 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5575
5576 // Cut an indented block, without the leading whitespace.
5577 cx.set_state(indoc! {"
5578 const a: B = (
5579 c(),
5580 «d(
5581 e,
5582 f
5583 )ˇ»
5584 );
5585 "});
5586 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5587 cx.assert_editor_state(indoc! {"
5588 const a: B = (
5589 c(),
5590 ˇ
5591 );
5592 "});
5593
5594 // Paste it at the same position.
5595 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5596 cx.assert_editor_state(indoc! {"
5597 const a: B = (
5598 c(),
5599 d(
5600 e,
5601 f
5602 )ˇ
5603 );
5604 "});
5605
5606 // Paste it at a line with a lower indent level.
5607 cx.set_state(indoc! {"
5608 ˇ
5609 const a: B = (
5610 c(),
5611 );
5612 "});
5613 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5614 cx.assert_editor_state(indoc! {"
5615 d(
5616 e,
5617 f
5618 )ˇ
5619 const a: B = (
5620 c(),
5621 );
5622 "});
5623
5624 // Cut an indented block, with the leading whitespace.
5625 cx.set_state(indoc! {"
5626 const a: B = (
5627 c(),
5628 « d(
5629 e,
5630 f
5631 )
5632 ˇ»);
5633 "});
5634 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5635 cx.assert_editor_state(indoc! {"
5636 const a: B = (
5637 c(),
5638 ˇ);
5639 "});
5640
5641 // Paste it at the same position.
5642 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5643 cx.assert_editor_state(indoc! {"
5644 const a: B = (
5645 c(),
5646 d(
5647 e,
5648 f
5649 )
5650 ˇ);
5651 "});
5652
5653 // Paste it at a line with a higher indent level.
5654 cx.set_state(indoc! {"
5655 const a: B = (
5656 c(),
5657 d(
5658 e,
5659 fˇ
5660 )
5661 );
5662 "});
5663 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5664 cx.assert_editor_state(indoc! {"
5665 const a: B = (
5666 c(),
5667 d(
5668 e,
5669 f d(
5670 e,
5671 f
5672 )
5673 ˇ
5674 )
5675 );
5676 "});
5677
5678 // Copy an indented block, starting mid-line
5679 cx.set_state(indoc! {"
5680 const a: B = (
5681 c(),
5682 somethin«g(
5683 e,
5684 f
5685 )ˇ»
5686 );
5687 "});
5688 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5689
5690 // Paste it on a line with a lower indent level
5691 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5692 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5693 cx.assert_editor_state(indoc! {"
5694 const a: B = (
5695 c(),
5696 something(
5697 e,
5698 f
5699 )
5700 );
5701 g(
5702 e,
5703 f
5704 )ˇ"});
5705}
5706
5707#[gpui::test]
5708async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5709 init_test(cx, |_| {});
5710
5711 cx.write_to_clipboard(ClipboardItem::new_string(
5712 " d(\n e\n );\n".into(),
5713 ));
5714
5715 let mut cx = EditorTestContext::new(cx).await;
5716 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5717
5718 cx.set_state(indoc! {"
5719 fn a() {
5720 b();
5721 if c() {
5722 ˇ
5723 }
5724 }
5725 "});
5726
5727 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5728 cx.assert_editor_state(indoc! {"
5729 fn a() {
5730 b();
5731 if c() {
5732 d(
5733 e
5734 );
5735 ˇ
5736 }
5737 }
5738 "});
5739
5740 cx.set_state(indoc! {"
5741 fn a() {
5742 b();
5743 ˇ
5744 }
5745 "});
5746
5747 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5748 cx.assert_editor_state(indoc! {"
5749 fn a() {
5750 b();
5751 d(
5752 e
5753 );
5754 ˇ
5755 }
5756 "});
5757}
5758
5759#[gpui::test]
5760fn test_select_all(cx: &mut TestAppContext) {
5761 init_test(cx, |_| {});
5762
5763 let editor = cx.add_window(|window, cx| {
5764 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5765 build_editor(buffer, window, cx)
5766 });
5767 _ = editor.update(cx, |editor, window, cx| {
5768 editor.select_all(&SelectAll, window, cx);
5769 assert_eq!(
5770 editor.selections.display_ranges(cx),
5771 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5772 );
5773 });
5774}
5775
5776#[gpui::test]
5777fn test_select_line(cx: &mut TestAppContext) {
5778 init_test(cx, |_| {});
5779
5780 let editor = cx.add_window(|window, cx| {
5781 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5782 build_editor(buffer, window, cx)
5783 });
5784 _ = editor.update(cx, |editor, window, cx| {
5785 editor.change_selections(None, window, cx, |s| {
5786 s.select_display_ranges([
5787 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5788 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5789 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5790 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5791 ])
5792 });
5793 editor.select_line(&SelectLine, window, cx);
5794 assert_eq!(
5795 editor.selections.display_ranges(cx),
5796 vec![
5797 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5798 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5799 ]
5800 );
5801 });
5802
5803 _ = editor.update(cx, |editor, window, cx| {
5804 editor.select_line(&SelectLine, window, cx);
5805 assert_eq!(
5806 editor.selections.display_ranges(cx),
5807 vec![
5808 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5809 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5810 ]
5811 );
5812 });
5813
5814 _ = editor.update(cx, |editor, window, cx| {
5815 editor.select_line(&SelectLine, window, cx);
5816 assert_eq!(
5817 editor.selections.display_ranges(cx),
5818 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5819 );
5820 });
5821}
5822
5823#[gpui::test]
5824async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5825 init_test(cx, |_| {});
5826 let mut cx = EditorTestContext::new(cx).await;
5827
5828 #[track_caller]
5829 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5830 cx.set_state(initial_state);
5831 cx.update_editor(|e, window, cx| {
5832 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5833 });
5834 cx.assert_editor_state(expected_state);
5835 }
5836
5837 // Selection starts and ends at the middle of lines, left-to-right
5838 test(
5839 &mut cx,
5840 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5841 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5842 );
5843 // Same thing, right-to-left
5844 test(
5845 &mut cx,
5846 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5847 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5848 );
5849
5850 // Whole buffer, left-to-right, last line *doesn't* end with newline
5851 test(
5852 &mut cx,
5853 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5854 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5855 );
5856 // Same thing, right-to-left
5857 test(
5858 &mut cx,
5859 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5860 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5861 );
5862
5863 // Whole buffer, left-to-right, last line ends with newline
5864 test(
5865 &mut cx,
5866 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5867 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5868 );
5869 // Same thing, right-to-left
5870 test(
5871 &mut cx,
5872 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5873 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5874 );
5875
5876 // Starts at the end of a line, ends at the start of another
5877 test(
5878 &mut cx,
5879 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5880 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5881 );
5882}
5883
5884#[gpui::test]
5885async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5886 init_test(cx, |_| {});
5887
5888 let editor = cx.add_window(|window, cx| {
5889 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5890 build_editor(buffer, window, cx)
5891 });
5892
5893 // setup
5894 _ = editor.update(cx, |editor, window, cx| {
5895 editor.fold_creases(
5896 vec![
5897 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5898 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5899 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5900 ],
5901 true,
5902 window,
5903 cx,
5904 );
5905 assert_eq!(
5906 editor.display_text(cx),
5907 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5908 );
5909 });
5910
5911 _ = editor.update(cx, |editor, window, cx| {
5912 editor.change_selections(None, window, cx, |s| {
5913 s.select_display_ranges([
5914 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5915 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5916 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5917 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5918 ])
5919 });
5920 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5921 assert_eq!(
5922 editor.display_text(cx),
5923 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5924 );
5925 });
5926 EditorTestContext::for_editor(editor, cx)
5927 .await
5928 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5929
5930 _ = editor.update(cx, |editor, window, cx| {
5931 editor.change_selections(None, window, cx, |s| {
5932 s.select_display_ranges([
5933 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5934 ])
5935 });
5936 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5937 assert_eq!(
5938 editor.display_text(cx),
5939 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5940 );
5941 assert_eq!(
5942 editor.selections.display_ranges(cx),
5943 [
5944 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5945 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5946 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5947 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5948 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5949 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5950 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5951 ]
5952 );
5953 });
5954 EditorTestContext::for_editor(editor, cx)
5955 .await
5956 .assert_editor_state(
5957 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5958 );
5959}
5960
5961#[gpui::test]
5962async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5963 init_test(cx, |_| {});
5964
5965 let mut cx = EditorTestContext::new(cx).await;
5966
5967 cx.set_state(indoc!(
5968 r#"abc
5969 defˇghi
5970
5971 jk
5972 nlmo
5973 "#
5974 ));
5975
5976 cx.update_editor(|editor, window, cx| {
5977 editor.add_selection_above(&Default::default(), window, cx);
5978 });
5979
5980 cx.assert_editor_state(indoc!(
5981 r#"abcˇ
5982 defˇghi
5983
5984 jk
5985 nlmo
5986 "#
5987 ));
5988
5989 cx.update_editor(|editor, window, cx| {
5990 editor.add_selection_above(&Default::default(), window, cx);
5991 });
5992
5993 cx.assert_editor_state(indoc!(
5994 r#"abcˇ
5995 defˇghi
5996
5997 jk
5998 nlmo
5999 "#
6000 ));
6001
6002 cx.update_editor(|editor, window, cx| {
6003 editor.add_selection_below(&Default::default(), window, cx);
6004 });
6005
6006 cx.assert_editor_state(indoc!(
6007 r#"abc
6008 defˇghi
6009
6010 jk
6011 nlmo
6012 "#
6013 ));
6014
6015 cx.update_editor(|editor, window, cx| {
6016 editor.undo_selection(&Default::default(), window, cx);
6017 });
6018
6019 cx.assert_editor_state(indoc!(
6020 r#"abcˇ
6021 defˇghi
6022
6023 jk
6024 nlmo
6025 "#
6026 ));
6027
6028 cx.update_editor(|editor, window, cx| {
6029 editor.redo_selection(&Default::default(), window, cx);
6030 });
6031
6032 cx.assert_editor_state(indoc!(
6033 r#"abc
6034 defˇghi
6035
6036 jk
6037 nlmo
6038 "#
6039 ));
6040
6041 cx.update_editor(|editor, window, cx| {
6042 editor.add_selection_below(&Default::default(), window, cx);
6043 });
6044
6045 cx.assert_editor_state(indoc!(
6046 r#"abc
6047 defˇghi
6048 ˇ
6049 jk
6050 nlmo
6051 "#
6052 ));
6053
6054 cx.update_editor(|editor, window, cx| {
6055 editor.add_selection_below(&Default::default(), window, cx);
6056 });
6057
6058 cx.assert_editor_state(indoc!(
6059 r#"abc
6060 defˇghi
6061 ˇ
6062 jkˇ
6063 nlmo
6064 "#
6065 ));
6066
6067 cx.update_editor(|editor, window, cx| {
6068 editor.add_selection_below(&Default::default(), window, cx);
6069 });
6070
6071 cx.assert_editor_state(indoc!(
6072 r#"abc
6073 defˇghi
6074 ˇ
6075 jkˇ
6076 nlmˇo
6077 "#
6078 ));
6079
6080 cx.update_editor(|editor, window, cx| {
6081 editor.add_selection_below(&Default::default(), window, cx);
6082 });
6083
6084 cx.assert_editor_state(indoc!(
6085 r#"abc
6086 defˇghi
6087 ˇ
6088 jkˇ
6089 nlmˇo
6090 ˇ"#
6091 ));
6092
6093 // change selections
6094 cx.set_state(indoc!(
6095 r#"abc
6096 def«ˇg»hi
6097
6098 jk
6099 nlmo
6100 "#
6101 ));
6102
6103 cx.update_editor(|editor, window, cx| {
6104 editor.add_selection_below(&Default::default(), window, cx);
6105 });
6106
6107 cx.assert_editor_state(indoc!(
6108 r#"abc
6109 def«ˇg»hi
6110
6111 jk
6112 nlm«ˇo»
6113 "#
6114 ));
6115
6116 cx.update_editor(|editor, window, cx| {
6117 editor.add_selection_below(&Default::default(), window, cx);
6118 });
6119
6120 cx.assert_editor_state(indoc!(
6121 r#"abc
6122 def«ˇg»hi
6123
6124 jk
6125 nlm«ˇo»
6126 "#
6127 ));
6128
6129 cx.update_editor(|editor, window, cx| {
6130 editor.add_selection_above(&Default::default(), window, cx);
6131 });
6132
6133 cx.assert_editor_state(indoc!(
6134 r#"abc
6135 def«ˇg»hi
6136
6137 jk
6138 nlmo
6139 "#
6140 ));
6141
6142 cx.update_editor(|editor, window, cx| {
6143 editor.add_selection_above(&Default::default(), window, cx);
6144 });
6145
6146 cx.assert_editor_state(indoc!(
6147 r#"abc
6148 def«ˇg»hi
6149
6150 jk
6151 nlmo
6152 "#
6153 ));
6154
6155 // Change selections again
6156 cx.set_state(indoc!(
6157 r#"a«bc
6158 defgˇ»hi
6159
6160 jk
6161 nlmo
6162 "#
6163 ));
6164
6165 cx.update_editor(|editor, window, cx| {
6166 editor.add_selection_below(&Default::default(), window, cx);
6167 });
6168
6169 cx.assert_editor_state(indoc!(
6170 r#"a«bcˇ»
6171 d«efgˇ»hi
6172
6173 j«kˇ»
6174 nlmo
6175 "#
6176 ));
6177
6178 cx.update_editor(|editor, window, cx| {
6179 editor.add_selection_below(&Default::default(), window, cx);
6180 });
6181 cx.assert_editor_state(indoc!(
6182 r#"a«bcˇ»
6183 d«efgˇ»hi
6184
6185 j«kˇ»
6186 n«lmoˇ»
6187 "#
6188 ));
6189 cx.update_editor(|editor, window, cx| {
6190 editor.add_selection_above(&Default::default(), window, cx);
6191 });
6192
6193 cx.assert_editor_state(indoc!(
6194 r#"a«bcˇ»
6195 d«efgˇ»hi
6196
6197 j«kˇ»
6198 nlmo
6199 "#
6200 ));
6201
6202 // Change selections again
6203 cx.set_state(indoc!(
6204 r#"abc
6205 d«ˇefghi
6206
6207 jk
6208 nlm»o
6209 "#
6210 ));
6211
6212 cx.update_editor(|editor, window, cx| {
6213 editor.add_selection_above(&Default::default(), window, cx);
6214 });
6215
6216 cx.assert_editor_state(indoc!(
6217 r#"a«ˇbc»
6218 d«ˇef»ghi
6219
6220 j«ˇk»
6221 n«ˇlm»o
6222 "#
6223 ));
6224
6225 cx.update_editor(|editor, window, cx| {
6226 editor.add_selection_below(&Default::default(), window, cx);
6227 });
6228
6229 cx.assert_editor_state(indoc!(
6230 r#"abc
6231 d«ˇef»ghi
6232
6233 j«ˇk»
6234 n«ˇlm»o
6235 "#
6236 ));
6237}
6238
6239#[gpui::test]
6240async fn test_select_next(cx: &mut TestAppContext) {
6241 init_test(cx, |_| {});
6242
6243 let mut cx = EditorTestContext::new(cx).await;
6244 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6245
6246 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6247 .unwrap();
6248 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6249
6250 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6251 .unwrap();
6252 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6253
6254 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6255 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6256
6257 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6258 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6259
6260 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6261 .unwrap();
6262 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6263
6264 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6265 .unwrap();
6266 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6267
6268 // Test selection direction should be preserved
6269 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6270
6271 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6272 .unwrap();
6273 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6274}
6275
6276#[gpui::test]
6277async fn test_select_all_matches(cx: &mut TestAppContext) {
6278 init_test(cx, |_| {});
6279
6280 let mut cx = EditorTestContext::new(cx).await;
6281
6282 // Test caret-only selections
6283 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6284 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6285 .unwrap();
6286 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6287
6288 // Test left-to-right selections
6289 cx.set_state("abc\n«abcˇ»\nabc");
6290 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6291 .unwrap();
6292 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6293
6294 // Test right-to-left selections
6295 cx.set_state("abc\n«ˇabc»\nabc");
6296 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6297 .unwrap();
6298 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6299
6300 // Test selecting whitespace with caret selection
6301 cx.set_state("abc\nˇ abc\nabc");
6302 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6303 .unwrap();
6304 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6305
6306 // Test selecting whitespace with left-to-right selection
6307 cx.set_state("abc\n«ˇ »abc\nabc");
6308 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6309 .unwrap();
6310 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6311
6312 // Test no matches with right-to-left selection
6313 cx.set_state("abc\n« ˇ»abc\nabc");
6314 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6315 .unwrap();
6316 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6317}
6318
6319#[gpui::test]
6320async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6321 init_test(cx, |_| {});
6322
6323 let mut cx = EditorTestContext::new(cx).await;
6324
6325 let large_body_1 = "\nd".repeat(200);
6326 let large_body_2 = "\ne".repeat(200);
6327
6328 cx.set_state(&format!(
6329 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6330 ));
6331 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6332 let scroll_position = editor.scroll_position(cx);
6333 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6334 scroll_position
6335 });
6336
6337 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6338 .unwrap();
6339 cx.assert_editor_state(&format!(
6340 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6341 ));
6342 let scroll_position_after_selection =
6343 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6344 assert_eq!(
6345 initial_scroll_position, scroll_position_after_selection,
6346 "Scroll position should not change after selecting all matches"
6347 );
6348}
6349
6350#[gpui::test]
6351async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6352 init_test(cx, |_| {});
6353
6354 let mut cx = EditorLspTestContext::new_rust(
6355 lsp::ServerCapabilities {
6356 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6357 ..Default::default()
6358 },
6359 cx,
6360 )
6361 .await;
6362
6363 cx.set_state(indoc! {"
6364 line 1
6365 line 2
6366 linˇe 3
6367 line 4
6368 line 5
6369 "});
6370
6371 // Make an edit
6372 cx.update_editor(|editor, window, cx| {
6373 editor.handle_input("X", window, cx);
6374 });
6375
6376 // Move cursor to a different position
6377 cx.update_editor(|editor, window, cx| {
6378 editor.change_selections(None, window, cx, |s| {
6379 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6380 });
6381 });
6382
6383 cx.assert_editor_state(indoc! {"
6384 line 1
6385 line 2
6386 linXe 3
6387 line 4
6388 liˇne 5
6389 "});
6390
6391 cx.lsp
6392 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6393 Ok(Some(vec![lsp::TextEdit::new(
6394 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6395 "PREFIX ".to_string(),
6396 )]))
6397 });
6398
6399 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6400 .unwrap()
6401 .await
6402 .unwrap();
6403
6404 cx.assert_editor_state(indoc! {"
6405 PREFIX line 1
6406 line 2
6407 linXe 3
6408 line 4
6409 liˇne 5
6410 "});
6411
6412 // Undo formatting
6413 cx.update_editor(|editor, window, cx| {
6414 editor.undo(&Default::default(), window, cx);
6415 });
6416
6417 // Verify cursor moved back to position after edit
6418 cx.assert_editor_state(indoc! {"
6419 line 1
6420 line 2
6421 linXˇe 3
6422 line 4
6423 line 5
6424 "});
6425}
6426
6427#[gpui::test]
6428async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
6429 init_test(cx, |_| {});
6430
6431 let mut cx = EditorTestContext::new(cx).await;
6432
6433 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
6434 cx.update_editor(|editor, window, cx| {
6435 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
6436 });
6437
6438 cx.set_state(indoc! {"
6439 line 1
6440 line 2
6441 linˇe 3
6442 line 4
6443 line 5
6444 line 6
6445 line 7
6446 line 8
6447 line 9
6448 line 10
6449 "});
6450
6451 let snapshot = cx.buffer_snapshot();
6452 let edit_position = snapshot.anchor_after(Point::new(2, 4));
6453
6454 cx.update(|_, cx| {
6455 provider.update(cx, |provider, _| {
6456 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
6457 id: None,
6458 edits: vec![(edit_position..edit_position, "X".into())],
6459 edit_preview: None,
6460 }))
6461 })
6462 });
6463
6464 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
6465 cx.update_editor(|editor, window, cx| {
6466 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
6467 });
6468
6469 cx.assert_editor_state(indoc! {"
6470 line 1
6471 line 2
6472 lineXˇ 3
6473 line 4
6474 line 5
6475 line 6
6476 line 7
6477 line 8
6478 line 9
6479 line 10
6480 "});
6481
6482 cx.update_editor(|editor, window, cx| {
6483 editor.change_selections(None, window, cx, |s| {
6484 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
6485 });
6486 });
6487
6488 cx.assert_editor_state(indoc! {"
6489 line 1
6490 line 2
6491 lineX 3
6492 line 4
6493 line 5
6494 line 6
6495 line 7
6496 line 8
6497 line 9
6498 liˇne 10
6499 "});
6500
6501 cx.update_editor(|editor, window, cx| {
6502 editor.undo(&Default::default(), window, cx);
6503 });
6504
6505 cx.assert_editor_state(indoc! {"
6506 line 1
6507 line 2
6508 lineˇ 3
6509 line 4
6510 line 5
6511 line 6
6512 line 7
6513 line 8
6514 line 9
6515 line 10
6516 "});
6517}
6518
6519#[gpui::test]
6520async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6521 init_test(cx, |_| {});
6522
6523 let mut cx = EditorTestContext::new(cx).await;
6524 cx.set_state(
6525 r#"let foo = 2;
6526lˇet foo = 2;
6527let fooˇ = 2;
6528let foo = 2;
6529let foo = ˇ2;"#,
6530 );
6531
6532 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6533 .unwrap();
6534 cx.assert_editor_state(
6535 r#"let foo = 2;
6536«letˇ» foo = 2;
6537let «fooˇ» = 2;
6538let foo = 2;
6539let foo = «2ˇ»;"#,
6540 );
6541
6542 // noop for multiple selections with different contents
6543 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6544 .unwrap();
6545 cx.assert_editor_state(
6546 r#"let foo = 2;
6547«letˇ» foo = 2;
6548let «fooˇ» = 2;
6549let foo = 2;
6550let foo = «2ˇ»;"#,
6551 );
6552
6553 // Test last selection direction should be preserved
6554 cx.set_state(
6555 r#"let foo = 2;
6556let foo = 2;
6557let «fooˇ» = 2;
6558let «ˇfoo» = 2;
6559let foo = 2;"#,
6560 );
6561
6562 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6563 .unwrap();
6564 cx.assert_editor_state(
6565 r#"let foo = 2;
6566let foo = 2;
6567let «fooˇ» = 2;
6568let «ˇfoo» = 2;
6569let «ˇfoo» = 2;"#,
6570 );
6571}
6572
6573#[gpui::test]
6574async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6575 init_test(cx, |_| {});
6576
6577 let mut cx =
6578 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6579
6580 cx.assert_editor_state(indoc! {"
6581 ˇbbb
6582 ccc
6583
6584 bbb
6585 ccc
6586 "});
6587 cx.dispatch_action(SelectPrevious::default());
6588 cx.assert_editor_state(indoc! {"
6589 «bbbˇ»
6590 ccc
6591
6592 bbb
6593 ccc
6594 "});
6595 cx.dispatch_action(SelectPrevious::default());
6596 cx.assert_editor_state(indoc! {"
6597 «bbbˇ»
6598 ccc
6599
6600 «bbbˇ»
6601 ccc
6602 "});
6603}
6604
6605#[gpui::test]
6606async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6607 init_test(cx, |_| {});
6608
6609 let mut cx = EditorTestContext::new(cx).await;
6610 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6611
6612 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6613 .unwrap();
6614 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6615
6616 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6617 .unwrap();
6618 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6619
6620 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6621 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6622
6623 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6624 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6625
6626 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6627 .unwrap();
6628 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6629
6630 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6631 .unwrap();
6632 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6633}
6634
6635#[gpui::test]
6636async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6637 init_test(cx, |_| {});
6638
6639 let mut cx = EditorTestContext::new(cx).await;
6640 cx.set_state("aˇ");
6641
6642 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6643 .unwrap();
6644 cx.assert_editor_state("«aˇ»");
6645 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6646 .unwrap();
6647 cx.assert_editor_state("«aˇ»");
6648}
6649
6650#[gpui::test]
6651async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6652 init_test(cx, |_| {});
6653
6654 let mut cx = EditorTestContext::new(cx).await;
6655 cx.set_state(
6656 r#"let foo = 2;
6657lˇet foo = 2;
6658let fooˇ = 2;
6659let foo = 2;
6660let foo = ˇ2;"#,
6661 );
6662
6663 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6664 .unwrap();
6665 cx.assert_editor_state(
6666 r#"let foo = 2;
6667«letˇ» foo = 2;
6668let «fooˇ» = 2;
6669let foo = 2;
6670let foo = «2ˇ»;"#,
6671 );
6672
6673 // noop for multiple selections with different contents
6674 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6675 .unwrap();
6676 cx.assert_editor_state(
6677 r#"let foo = 2;
6678«letˇ» foo = 2;
6679let «fooˇ» = 2;
6680let foo = 2;
6681let foo = «2ˇ»;"#,
6682 );
6683}
6684
6685#[gpui::test]
6686async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6687 init_test(cx, |_| {});
6688
6689 let mut cx = EditorTestContext::new(cx).await;
6690 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6691
6692 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6693 .unwrap();
6694 // selection direction is preserved
6695 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6696
6697 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6698 .unwrap();
6699 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6700
6701 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6702 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6703
6704 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6705 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6706
6707 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6708 .unwrap();
6709 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6710
6711 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6712 .unwrap();
6713 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6714}
6715
6716#[gpui::test]
6717async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6718 init_test(cx, |_| {});
6719
6720 let language = Arc::new(Language::new(
6721 LanguageConfig::default(),
6722 Some(tree_sitter_rust::LANGUAGE.into()),
6723 ));
6724
6725 let text = r#"
6726 use mod1::mod2::{mod3, mod4};
6727
6728 fn fn_1(param1: bool, param2: &str) {
6729 let var1 = "text";
6730 }
6731 "#
6732 .unindent();
6733
6734 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6735 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6736 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6737
6738 editor
6739 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6740 .await;
6741
6742 editor.update_in(cx, |editor, window, cx| {
6743 editor.change_selections(None, window, cx, |s| {
6744 s.select_display_ranges([
6745 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6746 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6747 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6748 ]);
6749 });
6750 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6751 });
6752 editor.update(cx, |editor, cx| {
6753 assert_text_with_selections(
6754 editor,
6755 indoc! {r#"
6756 use mod1::mod2::{mod3, «mod4ˇ»};
6757
6758 fn fn_1«ˇ(param1: bool, param2: &str)» {
6759 let var1 = "«ˇtext»";
6760 }
6761 "#},
6762 cx,
6763 );
6764 });
6765
6766 editor.update_in(cx, |editor, window, cx| {
6767 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6768 });
6769 editor.update(cx, |editor, cx| {
6770 assert_text_with_selections(
6771 editor,
6772 indoc! {r#"
6773 use mod1::mod2::«{mod3, mod4}ˇ»;
6774
6775 «ˇfn fn_1(param1: bool, param2: &str) {
6776 let var1 = "text";
6777 }»
6778 "#},
6779 cx,
6780 );
6781 });
6782
6783 editor.update_in(cx, |editor, window, cx| {
6784 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6785 });
6786 assert_eq!(
6787 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6788 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6789 );
6790
6791 // Trying to expand the selected syntax node one more time has no effect.
6792 editor.update_in(cx, |editor, window, cx| {
6793 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6794 });
6795 assert_eq!(
6796 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6797 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6798 );
6799
6800 editor.update_in(cx, |editor, window, cx| {
6801 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6802 });
6803 editor.update(cx, |editor, cx| {
6804 assert_text_with_selections(
6805 editor,
6806 indoc! {r#"
6807 use mod1::mod2::«{mod3, mod4}ˇ»;
6808
6809 «ˇfn fn_1(param1: bool, param2: &str) {
6810 let var1 = "text";
6811 }»
6812 "#},
6813 cx,
6814 );
6815 });
6816
6817 editor.update_in(cx, |editor, window, cx| {
6818 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6819 });
6820 editor.update(cx, |editor, cx| {
6821 assert_text_with_selections(
6822 editor,
6823 indoc! {r#"
6824 use mod1::mod2::{mod3, «mod4ˇ»};
6825
6826 fn fn_1«ˇ(param1: bool, param2: &str)» {
6827 let var1 = "«ˇtext»";
6828 }
6829 "#},
6830 cx,
6831 );
6832 });
6833
6834 editor.update_in(cx, |editor, window, cx| {
6835 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6836 });
6837 editor.update(cx, |editor, cx| {
6838 assert_text_with_selections(
6839 editor,
6840 indoc! {r#"
6841 use mod1::mod2::{mod3, mo«ˇ»d4};
6842
6843 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6844 let var1 = "te«ˇ»xt";
6845 }
6846 "#},
6847 cx,
6848 );
6849 });
6850
6851 // Trying to shrink the selected syntax node one more time has no effect.
6852 editor.update_in(cx, |editor, window, cx| {
6853 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6854 });
6855 editor.update_in(cx, |editor, _, cx| {
6856 assert_text_with_selections(
6857 editor,
6858 indoc! {r#"
6859 use mod1::mod2::{mod3, mo«ˇ»d4};
6860
6861 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6862 let var1 = "te«ˇ»xt";
6863 }
6864 "#},
6865 cx,
6866 );
6867 });
6868
6869 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6870 // a fold.
6871 editor.update_in(cx, |editor, window, cx| {
6872 editor.fold_creases(
6873 vec![
6874 Crease::simple(
6875 Point::new(0, 21)..Point::new(0, 24),
6876 FoldPlaceholder::test(),
6877 ),
6878 Crease::simple(
6879 Point::new(3, 20)..Point::new(3, 22),
6880 FoldPlaceholder::test(),
6881 ),
6882 ],
6883 true,
6884 window,
6885 cx,
6886 );
6887 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6888 });
6889 editor.update(cx, |editor, cx| {
6890 assert_text_with_selections(
6891 editor,
6892 indoc! {r#"
6893 use mod1::mod2::«{mod3, mod4}ˇ»;
6894
6895 fn fn_1«ˇ(param1: bool, param2: &str)» {
6896 let var1 = "«ˇtext»";
6897 }
6898 "#},
6899 cx,
6900 );
6901 });
6902}
6903
6904#[gpui::test]
6905async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6906 init_test(cx, |_| {});
6907
6908 let language = Arc::new(Language::new(
6909 LanguageConfig::default(),
6910 Some(tree_sitter_rust::LANGUAGE.into()),
6911 ));
6912
6913 let text = "let a = 2;";
6914
6915 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6916 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6917 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6918
6919 editor
6920 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6921 .await;
6922
6923 // Test case 1: Cursor at end of word
6924 editor.update_in(cx, |editor, window, cx| {
6925 editor.change_selections(None, window, cx, |s| {
6926 s.select_display_ranges([
6927 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6928 ]);
6929 });
6930 });
6931 editor.update(cx, |editor, cx| {
6932 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6933 });
6934 editor.update_in(cx, |editor, window, cx| {
6935 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6936 });
6937 editor.update(cx, |editor, cx| {
6938 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6939 });
6940 editor.update_in(cx, |editor, window, cx| {
6941 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6942 });
6943 editor.update(cx, |editor, cx| {
6944 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6945 });
6946
6947 // Test case 2: Cursor at end of statement
6948 editor.update_in(cx, |editor, window, cx| {
6949 editor.change_selections(None, window, cx, |s| {
6950 s.select_display_ranges([
6951 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6952 ]);
6953 });
6954 });
6955 editor.update(cx, |editor, cx| {
6956 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6957 });
6958 editor.update_in(cx, |editor, window, cx| {
6959 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6960 });
6961 editor.update(cx, |editor, cx| {
6962 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6963 });
6964}
6965
6966#[gpui::test]
6967async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6968 init_test(cx, |_| {});
6969
6970 let language = Arc::new(Language::new(
6971 LanguageConfig::default(),
6972 Some(tree_sitter_rust::LANGUAGE.into()),
6973 ));
6974
6975 let text = r#"
6976 use mod1::mod2::{mod3, mod4};
6977
6978 fn fn_1(param1: bool, param2: &str) {
6979 let var1 = "hello world";
6980 }
6981 "#
6982 .unindent();
6983
6984 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6985 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6986 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6987
6988 editor
6989 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6990 .await;
6991
6992 // Test 1: Cursor on a letter of a string word
6993 editor.update_in(cx, |editor, window, cx| {
6994 editor.change_selections(None, window, cx, |s| {
6995 s.select_display_ranges([
6996 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6997 ]);
6998 });
6999 });
7000 editor.update_in(cx, |editor, window, cx| {
7001 assert_text_with_selections(
7002 editor,
7003 indoc! {r#"
7004 use mod1::mod2::{mod3, mod4};
7005
7006 fn fn_1(param1: bool, param2: &str) {
7007 let var1 = "hˇello world";
7008 }
7009 "#},
7010 cx,
7011 );
7012 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7013 assert_text_with_selections(
7014 editor,
7015 indoc! {r#"
7016 use mod1::mod2::{mod3, mod4};
7017
7018 fn fn_1(param1: bool, param2: &str) {
7019 let var1 = "«ˇhello» world";
7020 }
7021 "#},
7022 cx,
7023 );
7024 });
7025
7026 // Test 2: Partial selection within a word
7027 editor.update_in(cx, |editor, window, cx| {
7028 editor.change_selections(None, window, cx, |s| {
7029 s.select_display_ranges([
7030 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7031 ]);
7032 });
7033 });
7034 editor.update_in(cx, |editor, window, cx| {
7035 assert_text_with_selections(
7036 editor,
7037 indoc! {r#"
7038 use mod1::mod2::{mod3, mod4};
7039
7040 fn fn_1(param1: bool, param2: &str) {
7041 let var1 = "h«elˇ»lo world";
7042 }
7043 "#},
7044 cx,
7045 );
7046 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7047 assert_text_with_selections(
7048 editor,
7049 indoc! {r#"
7050 use mod1::mod2::{mod3, mod4};
7051
7052 fn fn_1(param1: bool, param2: &str) {
7053 let var1 = "«ˇhello» world";
7054 }
7055 "#},
7056 cx,
7057 );
7058 });
7059
7060 // Test 3: Complete word already selected
7061 editor.update_in(cx, |editor, window, cx| {
7062 editor.change_selections(None, window, cx, |s| {
7063 s.select_display_ranges([
7064 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7065 ]);
7066 });
7067 });
7068 editor.update_in(cx, |editor, window, cx| {
7069 assert_text_with_selections(
7070 editor,
7071 indoc! {r#"
7072 use mod1::mod2::{mod3, mod4};
7073
7074 fn fn_1(param1: bool, param2: &str) {
7075 let var1 = "«helloˇ» world";
7076 }
7077 "#},
7078 cx,
7079 );
7080 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7081 assert_text_with_selections(
7082 editor,
7083 indoc! {r#"
7084 use mod1::mod2::{mod3, mod4};
7085
7086 fn fn_1(param1: bool, param2: &str) {
7087 let var1 = "«hello worldˇ»";
7088 }
7089 "#},
7090 cx,
7091 );
7092 });
7093
7094 // Test 4: Selection spanning across words
7095 editor.update_in(cx, |editor, window, cx| {
7096 editor.change_selections(None, window, cx, |s| {
7097 s.select_display_ranges([
7098 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7099 ]);
7100 });
7101 });
7102 editor.update_in(cx, |editor, window, cx| {
7103 assert_text_with_selections(
7104 editor,
7105 indoc! {r#"
7106 use mod1::mod2::{mod3, mod4};
7107
7108 fn fn_1(param1: bool, param2: &str) {
7109 let var1 = "hel«lo woˇ»rld";
7110 }
7111 "#},
7112 cx,
7113 );
7114 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7115 assert_text_with_selections(
7116 editor,
7117 indoc! {r#"
7118 use mod1::mod2::{mod3, mod4};
7119
7120 fn fn_1(param1: bool, param2: &str) {
7121 let var1 = "«ˇhello world»";
7122 }
7123 "#},
7124 cx,
7125 );
7126 });
7127
7128 // Test 5: Expansion beyond string
7129 editor.update_in(cx, |editor, window, cx| {
7130 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7131 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7132 assert_text_with_selections(
7133 editor,
7134 indoc! {r#"
7135 use mod1::mod2::{mod3, mod4};
7136
7137 fn fn_1(param1: bool, param2: &str) {
7138 «ˇlet var1 = "hello world";»
7139 }
7140 "#},
7141 cx,
7142 );
7143 });
7144}
7145
7146#[gpui::test]
7147async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7148 init_test(cx, |_| {});
7149
7150 let base_text = r#"
7151 impl A {
7152 // this is an uncommitted comment
7153
7154 fn b() {
7155 c();
7156 }
7157
7158 // this is another uncommitted comment
7159
7160 fn d() {
7161 // e
7162 // f
7163 }
7164 }
7165
7166 fn g() {
7167 // h
7168 }
7169 "#
7170 .unindent();
7171
7172 let text = r#"
7173 ˇimpl A {
7174
7175 fn b() {
7176 c();
7177 }
7178
7179 fn d() {
7180 // e
7181 // f
7182 }
7183 }
7184
7185 fn g() {
7186 // h
7187 }
7188 "#
7189 .unindent();
7190
7191 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7192 cx.set_state(&text);
7193 cx.set_head_text(&base_text);
7194 cx.update_editor(|editor, window, cx| {
7195 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7196 });
7197
7198 cx.assert_state_with_diff(
7199 "
7200 ˇimpl A {
7201 - // this is an uncommitted comment
7202
7203 fn b() {
7204 c();
7205 }
7206
7207 - // this is another uncommitted comment
7208 -
7209 fn d() {
7210 // e
7211 // f
7212 }
7213 }
7214
7215 fn g() {
7216 // h
7217 }
7218 "
7219 .unindent(),
7220 );
7221
7222 let expected_display_text = "
7223 impl A {
7224 // this is an uncommitted comment
7225
7226 fn b() {
7227 ⋯
7228 }
7229
7230 // this is another uncommitted comment
7231
7232 fn d() {
7233 ⋯
7234 }
7235 }
7236
7237 fn g() {
7238 ⋯
7239 }
7240 "
7241 .unindent();
7242
7243 cx.update_editor(|editor, window, cx| {
7244 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7245 assert_eq!(editor.display_text(cx), expected_display_text);
7246 });
7247}
7248
7249#[gpui::test]
7250async fn test_autoindent(cx: &mut TestAppContext) {
7251 init_test(cx, |_| {});
7252
7253 let language = Arc::new(
7254 Language::new(
7255 LanguageConfig {
7256 brackets: BracketPairConfig {
7257 pairs: vec![
7258 BracketPair {
7259 start: "{".to_string(),
7260 end: "}".to_string(),
7261 close: false,
7262 surround: false,
7263 newline: true,
7264 },
7265 BracketPair {
7266 start: "(".to_string(),
7267 end: ")".to_string(),
7268 close: false,
7269 surround: false,
7270 newline: true,
7271 },
7272 ],
7273 ..Default::default()
7274 },
7275 ..Default::default()
7276 },
7277 Some(tree_sitter_rust::LANGUAGE.into()),
7278 )
7279 .with_indents_query(
7280 r#"
7281 (_ "(" ")" @end) @indent
7282 (_ "{" "}" @end) @indent
7283 "#,
7284 )
7285 .unwrap(),
7286 );
7287
7288 let text = "fn a() {}";
7289
7290 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7291 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7292 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7293 editor
7294 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7295 .await;
7296
7297 editor.update_in(cx, |editor, window, cx| {
7298 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7299 editor.newline(&Newline, window, cx);
7300 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7301 assert_eq!(
7302 editor.selections.ranges(cx),
7303 &[
7304 Point::new(1, 4)..Point::new(1, 4),
7305 Point::new(3, 4)..Point::new(3, 4),
7306 Point::new(5, 0)..Point::new(5, 0)
7307 ]
7308 );
7309 });
7310}
7311
7312#[gpui::test]
7313async fn test_autoindent_selections(cx: &mut TestAppContext) {
7314 init_test(cx, |_| {});
7315
7316 {
7317 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7318 cx.set_state(indoc! {"
7319 impl A {
7320
7321 fn b() {}
7322
7323 «fn c() {
7324
7325 }ˇ»
7326 }
7327 "});
7328
7329 cx.update_editor(|editor, window, cx| {
7330 editor.autoindent(&Default::default(), window, cx);
7331 });
7332
7333 cx.assert_editor_state(indoc! {"
7334 impl A {
7335
7336 fn b() {}
7337
7338 «fn c() {
7339
7340 }ˇ»
7341 }
7342 "});
7343 }
7344
7345 {
7346 let mut cx = EditorTestContext::new_multibuffer(
7347 cx,
7348 [indoc! { "
7349 impl A {
7350 «
7351 // a
7352 fn b(){}
7353 »
7354 «
7355 }
7356 fn c(){}
7357 »
7358 "}],
7359 );
7360
7361 let buffer = cx.update_editor(|editor, _, cx| {
7362 let buffer = editor.buffer().update(cx, |buffer, _| {
7363 buffer.all_buffers().iter().next().unwrap().clone()
7364 });
7365 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7366 buffer
7367 });
7368
7369 cx.run_until_parked();
7370 cx.update_editor(|editor, window, cx| {
7371 editor.select_all(&Default::default(), window, cx);
7372 editor.autoindent(&Default::default(), window, cx)
7373 });
7374 cx.run_until_parked();
7375
7376 cx.update(|_, cx| {
7377 assert_eq!(
7378 buffer.read(cx).text(),
7379 indoc! { "
7380 impl A {
7381
7382 // a
7383 fn b(){}
7384
7385
7386 }
7387 fn c(){}
7388
7389 " }
7390 )
7391 });
7392 }
7393}
7394
7395#[gpui::test]
7396async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7397 init_test(cx, |_| {});
7398
7399 let mut cx = EditorTestContext::new(cx).await;
7400
7401 let language = Arc::new(Language::new(
7402 LanguageConfig {
7403 brackets: BracketPairConfig {
7404 pairs: vec![
7405 BracketPair {
7406 start: "{".to_string(),
7407 end: "}".to_string(),
7408 close: true,
7409 surround: true,
7410 newline: true,
7411 },
7412 BracketPair {
7413 start: "(".to_string(),
7414 end: ")".to_string(),
7415 close: true,
7416 surround: true,
7417 newline: true,
7418 },
7419 BracketPair {
7420 start: "/*".to_string(),
7421 end: " */".to_string(),
7422 close: true,
7423 surround: true,
7424 newline: true,
7425 },
7426 BracketPair {
7427 start: "[".to_string(),
7428 end: "]".to_string(),
7429 close: false,
7430 surround: false,
7431 newline: true,
7432 },
7433 BracketPair {
7434 start: "\"".to_string(),
7435 end: "\"".to_string(),
7436 close: true,
7437 surround: true,
7438 newline: false,
7439 },
7440 BracketPair {
7441 start: "<".to_string(),
7442 end: ">".to_string(),
7443 close: false,
7444 surround: true,
7445 newline: true,
7446 },
7447 ],
7448 ..Default::default()
7449 },
7450 autoclose_before: "})]".to_string(),
7451 ..Default::default()
7452 },
7453 Some(tree_sitter_rust::LANGUAGE.into()),
7454 ));
7455
7456 cx.language_registry().add(language.clone());
7457 cx.update_buffer(|buffer, cx| {
7458 buffer.set_language(Some(language), cx);
7459 });
7460
7461 cx.set_state(
7462 &r#"
7463 🏀ˇ
7464 εˇ
7465 ❤️ˇ
7466 "#
7467 .unindent(),
7468 );
7469
7470 // autoclose multiple nested brackets at multiple cursors
7471 cx.update_editor(|editor, window, cx| {
7472 editor.handle_input("{", window, cx);
7473 editor.handle_input("{", window, cx);
7474 editor.handle_input("{", window, cx);
7475 });
7476 cx.assert_editor_state(
7477 &"
7478 🏀{{{ˇ}}}
7479 ε{{{ˇ}}}
7480 ❤️{{{ˇ}}}
7481 "
7482 .unindent(),
7483 );
7484
7485 // insert a different closing bracket
7486 cx.update_editor(|editor, window, cx| {
7487 editor.handle_input(")", window, cx);
7488 });
7489 cx.assert_editor_state(
7490 &"
7491 🏀{{{)ˇ}}}
7492 ε{{{)ˇ}}}
7493 ❤️{{{)ˇ}}}
7494 "
7495 .unindent(),
7496 );
7497
7498 // skip over the auto-closed brackets when typing a closing bracket
7499 cx.update_editor(|editor, window, cx| {
7500 editor.move_right(&MoveRight, window, cx);
7501 editor.handle_input("}", window, cx);
7502 editor.handle_input("}", window, cx);
7503 editor.handle_input("}", window, cx);
7504 });
7505 cx.assert_editor_state(
7506 &"
7507 🏀{{{)}}}}ˇ
7508 ε{{{)}}}}ˇ
7509 ❤️{{{)}}}}ˇ
7510 "
7511 .unindent(),
7512 );
7513
7514 // autoclose multi-character pairs
7515 cx.set_state(
7516 &"
7517 ˇ
7518 ˇ
7519 "
7520 .unindent(),
7521 );
7522 cx.update_editor(|editor, window, cx| {
7523 editor.handle_input("/", window, cx);
7524 editor.handle_input("*", window, cx);
7525 });
7526 cx.assert_editor_state(
7527 &"
7528 /*ˇ */
7529 /*ˇ */
7530 "
7531 .unindent(),
7532 );
7533
7534 // one cursor autocloses a multi-character pair, one cursor
7535 // does not autoclose.
7536 cx.set_state(
7537 &"
7538 /ˇ
7539 ˇ
7540 "
7541 .unindent(),
7542 );
7543 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7544 cx.assert_editor_state(
7545 &"
7546 /*ˇ */
7547 *ˇ
7548 "
7549 .unindent(),
7550 );
7551
7552 // Don't autoclose if the next character isn't whitespace and isn't
7553 // listed in the language's "autoclose_before" section.
7554 cx.set_state("ˇa b");
7555 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7556 cx.assert_editor_state("{ˇa b");
7557
7558 // Don't autoclose if `close` is false for the bracket pair
7559 cx.set_state("ˇ");
7560 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7561 cx.assert_editor_state("[ˇ");
7562
7563 // Surround with brackets if text is selected
7564 cx.set_state("«aˇ» b");
7565 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7566 cx.assert_editor_state("{«aˇ»} b");
7567
7568 // Autoclose when not immediately after a word character
7569 cx.set_state("a ˇ");
7570 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7571 cx.assert_editor_state("a \"ˇ\"");
7572
7573 // Autoclose pair where the start and end characters are the same
7574 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7575 cx.assert_editor_state("a \"\"ˇ");
7576
7577 // Don't autoclose when immediately after a word character
7578 cx.set_state("aˇ");
7579 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7580 cx.assert_editor_state("a\"ˇ");
7581
7582 // Do autoclose when after a non-word character
7583 cx.set_state("{ˇ");
7584 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7585 cx.assert_editor_state("{\"ˇ\"");
7586
7587 // Non identical pairs autoclose regardless of preceding character
7588 cx.set_state("aˇ");
7589 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7590 cx.assert_editor_state("a{ˇ}");
7591
7592 // Don't autoclose pair if autoclose is disabled
7593 cx.set_state("ˇ");
7594 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7595 cx.assert_editor_state("<ˇ");
7596
7597 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7598 cx.set_state("«aˇ» b");
7599 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7600 cx.assert_editor_state("<«aˇ»> b");
7601}
7602
7603#[gpui::test]
7604async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7605 init_test(cx, |settings| {
7606 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7607 });
7608
7609 let mut cx = EditorTestContext::new(cx).await;
7610
7611 let language = Arc::new(Language::new(
7612 LanguageConfig {
7613 brackets: BracketPairConfig {
7614 pairs: vec![
7615 BracketPair {
7616 start: "{".to_string(),
7617 end: "}".to_string(),
7618 close: true,
7619 surround: true,
7620 newline: true,
7621 },
7622 BracketPair {
7623 start: "(".to_string(),
7624 end: ")".to_string(),
7625 close: true,
7626 surround: true,
7627 newline: true,
7628 },
7629 BracketPair {
7630 start: "[".to_string(),
7631 end: "]".to_string(),
7632 close: false,
7633 surround: false,
7634 newline: true,
7635 },
7636 ],
7637 ..Default::default()
7638 },
7639 autoclose_before: "})]".to_string(),
7640 ..Default::default()
7641 },
7642 Some(tree_sitter_rust::LANGUAGE.into()),
7643 ));
7644
7645 cx.language_registry().add(language.clone());
7646 cx.update_buffer(|buffer, cx| {
7647 buffer.set_language(Some(language), cx);
7648 });
7649
7650 cx.set_state(
7651 &"
7652 ˇ
7653 ˇ
7654 ˇ
7655 "
7656 .unindent(),
7657 );
7658
7659 // ensure only matching closing brackets are skipped over
7660 cx.update_editor(|editor, window, cx| {
7661 editor.handle_input("}", window, cx);
7662 editor.move_left(&MoveLeft, window, cx);
7663 editor.handle_input(")", window, cx);
7664 editor.move_left(&MoveLeft, window, cx);
7665 });
7666 cx.assert_editor_state(
7667 &"
7668 ˇ)}
7669 ˇ)}
7670 ˇ)}
7671 "
7672 .unindent(),
7673 );
7674
7675 // skip-over closing brackets at multiple cursors
7676 cx.update_editor(|editor, window, cx| {
7677 editor.handle_input(")", window, cx);
7678 editor.handle_input("}", window, cx);
7679 });
7680 cx.assert_editor_state(
7681 &"
7682 )}ˇ
7683 )}ˇ
7684 )}ˇ
7685 "
7686 .unindent(),
7687 );
7688
7689 // ignore non-close brackets
7690 cx.update_editor(|editor, window, cx| {
7691 editor.handle_input("]", window, cx);
7692 editor.move_left(&MoveLeft, window, cx);
7693 editor.handle_input("]", window, cx);
7694 });
7695 cx.assert_editor_state(
7696 &"
7697 )}]ˇ]
7698 )}]ˇ]
7699 )}]ˇ]
7700 "
7701 .unindent(),
7702 );
7703}
7704
7705#[gpui::test]
7706async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7707 init_test(cx, |_| {});
7708
7709 let mut cx = EditorTestContext::new(cx).await;
7710
7711 let html_language = Arc::new(
7712 Language::new(
7713 LanguageConfig {
7714 name: "HTML".into(),
7715 brackets: BracketPairConfig {
7716 pairs: vec![
7717 BracketPair {
7718 start: "<".into(),
7719 end: ">".into(),
7720 close: true,
7721 ..Default::default()
7722 },
7723 BracketPair {
7724 start: "{".into(),
7725 end: "}".into(),
7726 close: true,
7727 ..Default::default()
7728 },
7729 BracketPair {
7730 start: "(".into(),
7731 end: ")".into(),
7732 close: true,
7733 ..Default::default()
7734 },
7735 ],
7736 ..Default::default()
7737 },
7738 autoclose_before: "})]>".into(),
7739 ..Default::default()
7740 },
7741 Some(tree_sitter_html::LANGUAGE.into()),
7742 )
7743 .with_injection_query(
7744 r#"
7745 (script_element
7746 (raw_text) @injection.content
7747 (#set! injection.language "javascript"))
7748 "#,
7749 )
7750 .unwrap(),
7751 );
7752
7753 let javascript_language = Arc::new(Language::new(
7754 LanguageConfig {
7755 name: "JavaScript".into(),
7756 brackets: BracketPairConfig {
7757 pairs: vec![
7758 BracketPair {
7759 start: "/*".into(),
7760 end: " */".into(),
7761 close: true,
7762 ..Default::default()
7763 },
7764 BracketPair {
7765 start: "{".into(),
7766 end: "}".into(),
7767 close: true,
7768 ..Default::default()
7769 },
7770 BracketPair {
7771 start: "(".into(),
7772 end: ")".into(),
7773 close: true,
7774 ..Default::default()
7775 },
7776 ],
7777 ..Default::default()
7778 },
7779 autoclose_before: "})]>".into(),
7780 ..Default::default()
7781 },
7782 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7783 ));
7784
7785 cx.language_registry().add(html_language.clone());
7786 cx.language_registry().add(javascript_language.clone());
7787
7788 cx.update_buffer(|buffer, cx| {
7789 buffer.set_language(Some(html_language), cx);
7790 });
7791
7792 cx.set_state(
7793 &r#"
7794 <body>ˇ
7795 <script>
7796 var x = 1;ˇ
7797 </script>
7798 </body>ˇ
7799 "#
7800 .unindent(),
7801 );
7802
7803 // Precondition: different languages are active at different locations.
7804 cx.update_editor(|editor, window, cx| {
7805 let snapshot = editor.snapshot(window, cx);
7806 let cursors = editor.selections.ranges::<usize>(cx);
7807 let languages = cursors
7808 .iter()
7809 .map(|c| snapshot.language_at(c.start).unwrap().name())
7810 .collect::<Vec<_>>();
7811 assert_eq!(
7812 languages,
7813 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7814 );
7815 });
7816
7817 // Angle brackets autoclose in HTML, but not JavaScript.
7818 cx.update_editor(|editor, window, cx| {
7819 editor.handle_input("<", window, cx);
7820 editor.handle_input("a", window, cx);
7821 });
7822 cx.assert_editor_state(
7823 &r#"
7824 <body><aˇ>
7825 <script>
7826 var x = 1;<aˇ
7827 </script>
7828 </body><aˇ>
7829 "#
7830 .unindent(),
7831 );
7832
7833 // Curly braces and parens autoclose in both HTML and JavaScript.
7834 cx.update_editor(|editor, window, cx| {
7835 editor.handle_input(" b=", window, cx);
7836 editor.handle_input("{", window, cx);
7837 editor.handle_input("c", window, cx);
7838 editor.handle_input("(", window, cx);
7839 });
7840 cx.assert_editor_state(
7841 &r#"
7842 <body><a b={c(ˇ)}>
7843 <script>
7844 var x = 1;<a b={c(ˇ)}
7845 </script>
7846 </body><a b={c(ˇ)}>
7847 "#
7848 .unindent(),
7849 );
7850
7851 // Brackets that were already autoclosed are skipped.
7852 cx.update_editor(|editor, window, cx| {
7853 editor.handle_input(")", window, cx);
7854 editor.handle_input("d", window, cx);
7855 editor.handle_input("}", window, cx);
7856 });
7857 cx.assert_editor_state(
7858 &r#"
7859 <body><a b={c()d}ˇ>
7860 <script>
7861 var x = 1;<a b={c()d}ˇ
7862 </script>
7863 </body><a b={c()d}ˇ>
7864 "#
7865 .unindent(),
7866 );
7867 cx.update_editor(|editor, window, cx| {
7868 editor.handle_input(">", window, cx);
7869 });
7870 cx.assert_editor_state(
7871 &r#"
7872 <body><a b={c()d}>ˇ
7873 <script>
7874 var x = 1;<a b={c()d}>ˇ
7875 </script>
7876 </body><a b={c()d}>ˇ
7877 "#
7878 .unindent(),
7879 );
7880
7881 // Reset
7882 cx.set_state(
7883 &r#"
7884 <body>ˇ
7885 <script>
7886 var x = 1;ˇ
7887 </script>
7888 </body>ˇ
7889 "#
7890 .unindent(),
7891 );
7892
7893 cx.update_editor(|editor, window, cx| {
7894 editor.handle_input("<", window, cx);
7895 });
7896 cx.assert_editor_state(
7897 &r#"
7898 <body><ˇ>
7899 <script>
7900 var x = 1;<ˇ
7901 </script>
7902 </body><ˇ>
7903 "#
7904 .unindent(),
7905 );
7906
7907 // When backspacing, the closing angle brackets are removed.
7908 cx.update_editor(|editor, window, cx| {
7909 editor.backspace(&Backspace, window, cx);
7910 });
7911 cx.assert_editor_state(
7912 &r#"
7913 <body>ˇ
7914 <script>
7915 var x = 1;ˇ
7916 </script>
7917 </body>ˇ
7918 "#
7919 .unindent(),
7920 );
7921
7922 // Block comments autoclose in JavaScript, but not HTML.
7923 cx.update_editor(|editor, window, cx| {
7924 editor.handle_input("/", window, cx);
7925 editor.handle_input("*", window, cx);
7926 });
7927 cx.assert_editor_state(
7928 &r#"
7929 <body>/*ˇ
7930 <script>
7931 var x = 1;/*ˇ */
7932 </script>
7933 </body>/*ˇ
7934 "#
7935 .unindent(),
7936 );
7937}
7938
7939#[gpui::test]
7940async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7941 init_test(cx, |_| {});
7942
7943 let mut cx = EditorTestContext::new(cx).await;
7944
7945 let rust_language = Arc::new(
7946 Language::new(
7947 LanguageConfig {
7948 name: "Rust".into(),
7949 brackets: serde_json::from_value(json!([
7950 { "start": "{", "end": "}", "close": true, "newline": true },
7951 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7952 ]))
7953 .unwrap(),
7954 autoclose_before: "})]>".into(),
7955 ..Default::default()
7956 },
7957 Some(tree_sitter_rust::LANGUAGE.into()),
7958 )
7959 .with_override_query("(string_literal) @string")
7960 .unwrap(),
7961 );
7962
7963 cx.language_registry().add(rust_language.clone());
7964 cx.update_buffer(|buffer, cx| {
7965 buffer.set_language(Some(rust_language), cx);
7966 });
7967
7968 cx.set_state(
7969 &r#"
7970 let x = ˇ
7971 "#
7972 .unindent(),
7973 );
7974
7975 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7976 cx.update_editor(|editor, window, cx| {
7977 editor.handle_input("\"", window, cx);
7978 });
7979 cx.assert_editor_state(
7980 &r#"
7981 let x = "ˇ"
7982 "#
7983 .unindent(),
7984 );
7985
7986 // Inserting another quotation mark. The cursor moves across the existing
7987 // automatically-inserted quotation mark.
7988 cx.update_editor(|editor, window, cx| {
7989 editor.handle_input("\"", window, cx);
7990 });
7991 cx.assert_editor_state(
7992 &r#"
7993 let x = ""ˇ
7994 "#
7995 .unindent(),
7996 );
7997
7998 // Reset
7999 cx.set_state(
8000 &r#"
8001 let x = ˇ
8002 "#
8003 .unindent(),
8004 );
8005
8006 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8007 cx.update_editor(|editor, window, cx| {
8008 editor.handle_input("\"", window, cx);
8009 editor.handle_input(" ", window, cx);
8010 editor.move_left(&Default::default(), window, cx);
8011 editor.handle_input("\\", window, cx);
8012 editor.handle_input("\"", window, cx);
8013 });
8014 cx.assert_editor_state(
8015 &r#"
8016 let x = "\"ˇ "
8017 "#
8018 .unindent(),
8019 );
8020
8021 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8022 // mark. Nothing is inserted.
8023 cx.update_editor(|editor, window, cx| {
8024 editor.move_right(&Default::default(), window, cx);
8025 editor.handle_input("\"", window, cx);
8026 });
8027 cx.assert_editor_state(
8028 &r#"
8029 let x = "\" "ˇ
8030 "#
8031 .unindent(),
8032 );
8033}
8034
8035#[gpui::test]
8036async fn test_surround_with_pair(cx: &mut TestAppContext) {
8037 init_test(cx, |_| {});
8038
8039 let language = Arc::new(Language::new(
8040 LanguageConfig {
8041 brackets: BracketPairConfig {
8042 pairs: vec![
8043 BracketPair {
8044 start: "{".to_string(),
8045 end: "}".to_string(),
8046 close: true,
8047 surround: true,
8048 newline: true,
8049 },
8050 BracketPair {
8051 start: "/* ".to_string(),
8052 end: "*/".to_string(),
8053 close: true,
8054 surround: true,
8055 ..Default::default()
8056 },
8057 ],
8058 ..Default::default()
8059 },
8060 ..Default::default()
8061 },
8062 Some(tree_sitter_rust::LANGUAGE.into()),
8063 ));
8064
8065 let text = r#"
8066 a
8067 b
8068 c
8069 "#
8070 .unindent();
8071
8072 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8073 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8074 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8075 editor
8076 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8077 .await;
8078
8079 editor.update_in(cx, |editor, window, cx| {
8080 editor.change_selections(None, window, cx, |s| {
8081 s.select_display_ranges([
8082 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8083 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8084 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8085 ])
8086 });
8087
8088 editor.handle_input("{", window, cx);
8089 editor.handle_input("{", window, cx);
8090 editor.handle_input("{", window, cx);
8091 assert_eq!(
8092 editor.text(cx),
8093 "
8094 {{{a}}}
8095 {{{b}}}
8096 {{{c}}}
8097 "
8098 .unindent()
8099 );
8100 assert_eq!(
8101 editor.selections.display_ranges(cx),
8102 [
8103 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8104 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8105 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8106 ]
8107 );
8108
8109 editor.undo(&Undo, window, cx);
8110 editor.undo(&Undo, window, cx);
8111 editor.undo(&Undo, window, cx);
8112 assert_eq!(
8113 editor.text(cx),
8114 "
8115 a
8116 b
8117 c
8118 "
8119 .unindent()
8120 );
8121 assert_eq!(
8122 editor.selections.display_ranges(cx),
8123 [
8124 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8125 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8126 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8127 ]
8128 );
8129
8130 // Ensure inserting the first character of a multi-byte bracket pair
8131 // doesn't surround the selections with the bracket.
8132 editor.handle_input("/", window, cx);
8133 assert_eq!(
8134 editor.text(cx),
8135 "
8136 /
8137 /
8138 /
8139 "
8140 .unindent()
8141 );
8142 assert_eq!(
8143 editor.selections.display_ranges(cx),
8144 [
8145 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8146 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8147 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8148 ]
8149 );
8150
8151 editor.undo(&Undo, window, cx);
8152 assert_eq!(
8153 editor.text(cx),
8154 "
8155 a
8156 b
8157 c
8158 "
8159 .unindent()
8160 );
8161 assert_eq!(
8162 editor.selections.display_ranges(cx),
8163 [
8164 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8165 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8166 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8167 ]
8168 );
8169
8170 // Ensure inserting the last character of a multi-byte bracket pair
8171 // doesn't surround the selections with the bracket.
8172 editor.handle_input("*", window, cx);
8173 assert_eq!(
8174 editor.text(cx),
8175 "
8176 *
8177 *
8178 *
8179 "
8180 .unindent()
8181 );
8182 assert_eq!(
8183 editor.selections.display_ranges(cx),
8184 [
8185 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8186 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8187 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8188 ]
8189 );
8190 });
8191}
8192
8193#[gpui::test]
8194async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8195 init_test(cx, |_| {});
8196
8197 let language = Arc::new(Language::new(
8198 LanguageConfig {
8199 brackets: BracketPairConfig {
8200 pairs: vec![BracketPair {
8201 start: "{".to_string(),
8202 end: "}".to_string(),
8203 close: true,
8204 surround: true,
8205 newline: true,
8206 }],
8207 ..Default::default()
8208 },
8209 autoclose_before: "}".to_string(),
8210 ..Default::default()
8211 },
8212 Some(tree_sitter_rust::LANGUAGE.into()),
8213 ));
8214
8215 let text = r#"
8216 a
8217 b
8218 c
8219 "#
8220 .unindent();
8221
8222 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8223 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8224 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8225 editor
8226 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8227 .await;
8228
8229 editor.update_in(cx, |editor, window, cx| {
8230 editor.change_selections(None, window, cx, |s| {
8231 s.select_ranges([
8232 Point::new(0, 1)..Point::new(0, 1),
8233 Point::new(1, 1)..Point::new(1, 1),
8234 Point::new(2, 1)..Point::new(2, 1),
8235 ])
8236 });
8237
8238 editor.handle_input("{", window, cx);
8239 editor.handle_input("{", window, cx);
8240 editor.handle_input("_", window, cx);
8241 assert_eq!(
8242 editor.text(cx),
8243 "
8244 a{{_}}
8245 b{{_}}
8246 c{{_}}
8247 "
8248 .unindent()
8249 );
8250 assert_eq!(
8251 editor.selections.ranges::<Point>(cx),
8252 [
8253 Point::new(0, 4)..Point::new(0, 4),
8254 Point::new(1, 4)..Point::new(1, 4),
8255 Point::new(2, 4)..Point::new(2, 4)
8256 ]
8257 );
8258
8259 editor.backspace(&Default::default(), window, cx);
8260 editor.backspace(&Default::default(), window, cx);
8261 assert_eq!(
8262 editor.text(cx),
8263 "
8264 a{}
8265 b{}
8266 c{}
8267 "
8268 .unindent()
8269 );
8270 assert_eq!(
8271 editor.selections.ranges::<Point>(cx),
8272 [
8273 Point::new(0, 2)..Point::new(0, 2),
8274 Point::new(1, 2)..Point::new(1, 2),
8275 Point::new(2, 2)..Point::new(2, 2)
8276 ]
8277 );
8278
8279 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8280 assert_eq!(
8281 editor.text(cx),
8282 "
8283 a
8284 b
8285 c
8286 "
8287 .unindent()
8288 );
8289 assert_eq!(
8290 editor.selections.ranges::<Point>(cx),
8291 [
8292 Point::new(0, 1)..Point::new(0, 1),
8293 Point::new(1, 1)..Point::new(1, 1),
8294 Point::new(2, 1)..Point::new(2, 1)
8295 ]
8296 );
8297 });
8298}
8299
8300#[gpui::test]
8301async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8302 init_test(cx, |settings| {
8303 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8304 });
8305
8306 let mut cx = EditorTestContext::new(cx).await;
8307
8308 let language = Arc::new(Language::new(
8309 LanguageConfig {
8310 brackets: BracketPairConfig {
8311 pairs: vec![
8312 BracketPair {
8313 start: "{".to_string(),
8314 end: "}".to_string(),
8315 close: true,
8316 surround: true,
8317 newline: true,
8318 },
8319 BracketPair {
8320 start: "(".to_string(),
8321 end: ")".to_string(),
8322 close: true,
8323 surround: true,
8324 newline: true,
8325 },
8326 BracketPair {
8327 start: "[".to_string(),
8328 end: "]".to_string(),
8329 close: false,
8330 surround: true,
8331 newline: true,
8332 },
8333 ],
8334 ..Default::default()
8335 },
8336 autoclose_before: "})]".to_string(),
8337 ..Default::default()
8338 },
8339 Some(tree_sitter_rust::LANGUAGE.into()),
8340 ));
8341
8342 cx.language_registry().add(language.clone());
8343 cx.update_buffer(|buffer, cx| {
8344 buffer.set_language(Some(language), cx);
8345 });
8346
8347 cx.set_state(
8348 &"
8349 {(ˇ)}
8350 [[ˇ]]
8351 {(ˇ)}
8352 "
8353 .unindent(),
8354 );
8355
8356 cx.update_editor(|editor, window, cx| {
8357 editor.backspace(&Default::default(), window, cx);
8358 editor.backspace(&Default::default(), window, cx);
8359 });
8360
8361 cx.assert_editor_state(
8362 &"
8363 ˇ
8364 ˇ]]
8365 ˇ
8366 "
8367 .unindent(),
8368 );
8369
8370 cx.update_editor(|editor, window, cx| {
8371 editor.handle_input("{", window, cx);
8372 editor.handle_input("{", window, cx);
8373 editor.move_right(&MoveRight, window, cx);
8374 editor.move_right(&MoveRight, window, cx);
8375 editor.move_left(&MoveLeft, window, cx);
8376 editor.move_left(&MoveLeft, window, cx);
8377 editor.backspace(&Default::default(), window, cx);
8378 });
8379
8380 cx.assert_editor_state(
8381 &"
8382 {ˇ}
8383 {ˇ}]]
8384 {ˇ}
8385 "
8386 .unindent(),
8387 );
8388
8389 cx.update_editor(|editor, window, cx| {
8390 editor.backspace(&Default::default(), window, cx);
8391 });
8392
8393 cx.assert_editor_state(
8394 &"
8395 ˇ
8396 ˇ]]
8397 ˇ
8398 "
8399 .unindent(),
8400 );
8401}
8402
8403#[gpui::test]
8404async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8405 init_test(cx, |_| {});
8406
8407 let language = Arc::new(Language::new(
8408 LanguageConfig::default(),
8409 Some(tree_sitter_rust::LANGUAGE.into()),
8410 ));
8411
8412 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8413 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8414 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8415 editor
8416 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8417 .await;
8418
8419 editor.update_in(cx, |editor, window, cx| {
8420 editor.set_auto_replace_emoji_shortcode(true);
8421
8422 editor.handle_input("Hello ", window, cx);
8423 editor.handle_input(":wave", window, cx);
8424 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8425
8426 editor.handle_input(":", window, cx);
8427 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8428
8429 editor.handle_input(" :smile", window, cx);
8430 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8431
8432 editor.handle_input(":", window, cx);
8433 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8434
8435 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8436 editor.handle_input(":wave", window, cx);
8437 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8438
8439 editor.handle_input(":", window, cx);
8440 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8441
8442 editor.handle_input(":1", window, cx);
8443 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8444
8445 editor.handle_input(":", window, cx);
8446 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8447
8448 // Ensure shortcode does not get replaced when it is part of a word
8449 editor.handle_input(" Test:wave", window, cx);
8450 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8451
8452 editor.handle_input(":", window, cx);
8453 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8454
8455 editor.set_auto_replace_emoji_shortcode(false);
8456
8457 // Ensure shortcode does not get replaced when auto replace is off
8458 editor.handle_input(" :wave", window, cx);
8459 assert_eq!(
8460 editor.text(cx),
8461 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8462 );
8463
8464 editor.handle_input(":", window, cx);
8465 assert_eq!(
8466 editor.text(cx),
8467 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8468 );
8469 });
8470}
8471
8472#[gpui::test]
8473async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8474 init_test(cx, |_| {});
8475
8476 let (text, insertion_ranges) = marked_text_ranges(
8477 indoc! {"
8478 ˇ
8479 "},
8480 false,
8481 );
8482
8483 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8484 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8485
8486 _ = editor.update_in(cx, |editor, window, cx| {
8487 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8488
8489 editor
8490 .insert_snippet(&insertion_ranges, snippet, window, cx)
8491 .unwrap();
8492
8493 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8494 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8495 assert_eq!(editor.text(cx), expected_text);
8496 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8497 }
8498
8499 assert(
8500 editor,
8501 cx,
8502 indoc! {"
8503 type «» =•
8504 "},
8505 );
8506
8507 assert!(editor.context_menu_visible(), "There should be a matches");
8508 });
8509}
8510
8511#[gpui::test]
8512async fn test_snippets(cx: &mut TestAppContext) {
8513 init_test(cx, |_| {});
8514
8515 let (text, insertion_ranges) = marked_text_ranges(
8516 indoc! {"
8517 a.ˇ b
8518 a.ˇ b
8519 a.ˇ b
8520 "},
8521 false,
8522 );
8523
8524 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8525 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8526
8527 editor.update_in(cx, |editor, window, cx| {
8528 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8529
8530 editor
8531 .insert_snippet(&insertion_ranges, snippet, window, cx)
8532 .unwrap();
8533
8534 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8535 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8536 assert_eq!(editor.text(cx), expected_text);
8537 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8538 }
8539
8540 assert(
8541 editor,
8542 cx,
8543 indoc! {"
8544 a.f(«one», two, «three») b
8545 a.f(«one», two, «three») b
8546 a.f(«one», two, «three») b
8547 "},
8548 );
8549
8550 // Can't move earlier than the first tab stop
8551 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8552 assert(
8553 editor,
8554 cx,
8555 indoc! {"
8556 a.f(«one», two, «three») b
8557 a.f(«one», two, «three») b
8558 a.f(«one», two, «three») b
8559 "},
8560 );
8561
8562 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8563 assert(
8564 editor,
8565 cx,
8566 indoc! {"
8567 a.f(one, «two», three) b
8568 a.f(one, «two», three) b
8569 a.f(one, «two», three) b
8570 "},
8571 );
8572
8573 editor.move_to_prev_snippet_tabstop(window, cx);
8574 assert(
8575 editor,
8576 cx,
8577 indoc! {"
8578 a.f(«one», two, «three») b
8579 a.f(«one», two, «three») b
8580 a.f(«one», two, «three») b
8581 "},
8582 );
8583
8584 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8585 assert(
8586 editor,
8587 cx,
8588 indoc! {"
8589 a.f(one, «two», three) b
8590 a.f(one, «two», three) b
8591 a.f(one, «two», three) b
8592 "},
8593 );
8594 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8595 assert(
8596 editor,
8597 cx,
8598 indoc! {"
8599 a.f(one, two, three)ˇ b
8600 a.f(one, two, three)ˇ b
8601 a.f(one, two, three)ˇ b
8602 "},
8603 );
8604
8605 // As soon as the last tab stop is reached, snippet state is gone
8606 editor.move_to_prev_snippet_tabstop(window, cx);
8607 assert(
8608 editor,
8609 cx,
8610 indoc! {"
8611 a.f(one, two, three)ˇ b
8612 a.f(one, two, three)ˇ b
8613 a.f(one, two, three)ˇ b
8614 "},
8615 );
8616 });
8617}
8618
8619#[gpui::test]
8620async fn test_document_format_during_save(cx: &mut TestAppContext) {
8621 init_test(cx, |_| {});
8622
8623 let fs = FakeFs::new(cx.executor());
8624 fs.insert_file(path!("/file.rs"), Default::default()).await;
8625
8626 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8627
8628 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8629 language_registry.add(rust_lang());
8630 let mut fake_servers = language_registry.register_fake_lsp(
8631 "Rust",
8632 FakeLspAdapter {
8633 capabilities: lsp::ServerCapabilities {
8634 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8635 ..Default::default()
8636 },
8637 ..Default::default()
8638 },
8639 );
8640
8641 let buffer = project
8642 .update(cx, |project, cx| {
8643 project.open_local_buffer(path!("/file.rs"), cx)
8644 })
8645 .await
8646 .unwrap();
8647
8648 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8649 let (editor, cx) = cx.add_window_view(|window, cx| {
8650 build_editor_with_project(project.clone(), buffer, window, cx)
8651 });
8652 editor.update_in(cx, |editor, window, cx| {
8653 editor.set_text("one\ntwo\nthree\n", window, cx)
8654 });
8655 assert!(cx.read(|cx| editor.is_dirty(cx)));
8656
8657 cx.executor().start_waiting();
8658 let fake_server = fake_servers.next().await.unwrap();
8659
8660 {
8661 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8662 move |params, _| async move {
8663 assert_eq!(
8664 params.text_document.uri,
8665 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8666 );
8667 assert_eq!(params.options.tab_size, 4);
8668 Ok(Some(vec![lsp::TextEdit::new(
8669 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8670 ", ".to_string(),
8671 )]))
8672 },
8673 );
8674 let save = editor
8675 .update_in(cx, |editor, window, cx| {
8676 editor.save(true, project.clone(), window, cx)
8677 })
8678 .unwrap();
8679 cx.executor().start_waiting();
8680 save.await;
8681
8682 assert_eq!(
8683 editor.update(cx, |editor, cx| editor.text(cx)),
8684 "one, two\nthree\n"
8685 );
8686 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8687 }
8688
8689 {
8690 editor.update_in(cx, |editor, window, cx| {
8691 editor.set_text("one\ntwo\nthree\n", window, cx)
8692 });
8693 assert!(cx.read(|cx| editor.is_dirty(cx)));
8694
8695 // Ensure we can still save even if formatting hangs.
8696 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8697 move |params, _| async move {
8698 assert_eq!(
8699 params.text_document.uri,
8700 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8701 );
8702 futures::future::pending::<()>().await;
8703 unreachable!()
8704 },
8705 );
8706 let save = editor
8707 .update_in(cx, |editor, window, cx| {
8708 editor.save(true, project.clone(), window, cx)
8709 })
8710 .unwrap();
8711 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8712 cx.executor().start_waiting();
8713 save.await;
8714 assert_eq!(
8715 editor.update(cx, |editor, cx| editor.text(cx)),
8716 "one\ntwo\nthree\n"
8717 );
8718 }
8719
8720 // For non-dirty buffer, no formatting request should be sent
8721 {
8722 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8723
8724 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8725 panic!("Should not be invoked on non-dirty buffer");
8726 });
8727 let save = editor
8728 .update_in(cx, |editor, window, cx| {
8729 editor.save(true, project.clone(), window, cx)
8730 })
8731 .unwrap();
8732 cx.executor().start_waiting();
8733 save.await;
8734 }
8735
8736 // Set rust language override and assert overridden tabsize is sent to language server
8737 update_test_language_settings(cx, |settings| {
8738 settings.languages.insert(
8739 "Rust".into(),
8740 LanguageSettingsContent {
8741 tab_size: NonZeroU32::new(8),
8742 ..Default::default()
8743 },
8744 );
8745 });
8746
8747 {
8748 editor.update_in(cx, |editor, window, cx| {
8749 editor.set_text("somehting_new\n", window, cx)
8750 });
8751 assert!(cx.read(|cx| editor.is_dirty(cx)));
8752 let _formatting_request_signal = fake_server
8753 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8754 assert_eq!(
8755 params.text_document.uri,
8756 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8757 );
8758 assert_eq!(params.options.tab_size, 8);
8759 Ok(Some(vec![]))
8760 });
8761 let save = editor
8762 .update_in(cx, |editor, window, cx| {
8763 editor.save(true, project.clone(), window, cx)
8764 })
8765 .unwrap();
8766 cx.executor().start_waiting();
8767 save.await;
8768 }
8769}
8770
8771#[gpui::test]
8772async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8773 init_test(cx, |_| {});
8774
8775 let cols = 4;
8776 let rows = 10;
8777 let sample_text_1 = sample_text(rows, cols, 'a');
8778 assert_eq!(
8779 sample_text_1,
8780 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8781 );
8782 let sample_text_2 = sample_text(rows, cols, 'l');
8783 assert_eq!(
8784 sample_text_2,
8785 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8786 );
8787 let sample_text_3 = sample_text(rows, cols, 'v');
8788 assert_eq!(
8789 sample_text_3,
8790 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8791 );
8792
8793 let fs = FakeFs::new(cx.executor());
8794 fs.insert_tree(
8795 path!("/a"),
8796 json!({
8797 "main.rs": sample_text_1,
8798 "other.rs": sample_text_2,
8799 "lib.rs": sample_text_3,
8800 }),
8801 )
8802 .await;
8803
8804 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8805 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8806 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8807
8808 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8809 language_registry.add(rust_lang());
8810 let mut fake_servers = language_registry.register_fake_lsp(
8811 "Rust",
8812 FakeLspAdapter {
8813 capabilities: lsp::ServerCapabilities {
8814 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8815 ..Default::default()
8816 },
8817 ..Default::default()
8818 },
8819 );
8820
8821 let worktree = project.update(cx, |project, cx| {
8822 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8823 assert_eq!(worktrees.len(), 1);
8824 worktrees.pop().unwrap()
8825 });
8826 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8827
8828 let buffer_1 = project
8829 .update(cx, |project, cx| {
8830 project.open_buffer((worktree_id, "main.rs"), cx)
8831 })
8832 .await
8833 .unwrap();
8834 let buffer_2 = project
8835 .update(cx, |project, cx| {
8836 project.open_buffer((worktree_id, "other.rs"), cx)
8837 })
8838 .await
8839 .unwrap();
8840 let buffer_3 = project
8841 .update(cx, |project, cx| {
8842 project.open_buffer((worktree_id, "lib.rs"), cx)
8843 })
8844 .await
8845 .unwrap();
8846
8847 let multi_buffer = cx.new(|cx| {
8848 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8849 multi_buffer.push_excerpts(
8850 buffer_1.clone(),
8851 [
8852 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8853 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8854 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8855 ],
8856 cx,
8857 );
8858 multi_buffer.push_excerpts(
8859 buffer_2.clone(),
8860 [
8861 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8862 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8863 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8864 ],
8865 cx,
8866 );
8867 multi_buffer.push_excerpts(
8868 buffer_3.clone(),
8869 [
8870 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8871 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8872 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8873 ],
8874 cx,
8875 );
8876 multi_buffer
8877 });
8878 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8879 Editor::new(
8880 EditorMode::full(),
8881 multi_buffer,
8882 Some(project.clone()),
8883 window,
8884 cx,
8885 )
8886 });
8887
8888 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8889 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8890 s.select_ranges(Some(1..2))
8891 });
8892 editor.insert("|one|two|three|", window, cx);
8893 });
8894 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8895 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8896 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8897 s.select_ranges(Some(60..70))
8898 });
8899 editor.insert("|four|five|six|", window, cx);
8900 });
8901 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8902
8903 // First two buffers should be edited, but not the third one.
8904 assert_eq!(
8905 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8906 "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}",
8907 );
8908 buffer_1.update(cx, |buffer, _| {
8909 assert!(buffer.is_dirty());
8910 assert_eq!(
8911 buffer.text(),
8912 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8913 )
8914 });
8915 buffer_2.update(cx, |buffer, _| {
8916 assert!(buffer.is_dirty());
8917 assert_eq!(
8918 buffer.text(),
8919 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8920 )
8921 });
8922 buffer_3.update(cx, |buffer, _| {
8923 assert!(!buffer.is_dirty());
8924 assert_eq!(buffer.text(), sample_text_3,)
8925 });
8926 cx.executor().run_until_parked();
8927
8928 cx.executor().start_waiting();
8929 let save = multi_buffer_editor
8930 .update_in(cx, |editor, window, cx| {
8931 editor.save(true, project.clone(), window, cx)
8932 })
8933 .unwrap();
8934
8935 let fake_server = fake_servers.next().await.unwrap();
8936 fake_server
8937 .server
8938 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8939 Ok(Some(vec![lsp::TextEdit::new(
8940 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8941 format!("[{} formatted]", params.text_document.uri),
8942 )]))
8943 })
8944 .detach();
8945 save.await;
8946
8947 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8948 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8949 assert_eq!(
8950 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8951 uri!(
8952 "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}"
8953 ),
8954 );
8955 buffer_1.update(cx, |buffer, _| {
8956 assert!(!buffer.is_dirty());
8957 assert_eq!(
8958 buffer.text(),
8959 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8960 )
8961 });
8962 buffer_2.update(cx, |buffer, _| {
8963 assert!(!buffer.is_dirty());
8964 assert_eq!(
8965 buffer.text(),
8966 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8967 )
8968 });
8969 buffer_3.update(cx, |buffer, _| {
8970 assert!(!buffer.is_dirty());
8971 assert_eq!(buffer.text(), sample_text_3,)
8972 });
8973}
8974
8975#[gpui::test]
8976async fn test_range_format_during_save(cx: &mut TestAppContext) {
8977 init_test(cx, |_| {});
8978
8979 let fs = FakeFs::new(cx.executor());
8980 fs.insert_file(path!("/file.rs"), Default::default()).await;
8981
8982 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8983
8984 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8985 language_registry.add(rust_lang());
8986 let mut fake_servers = language_registry.register_fake_lsp(
8987 "Rust",
8988 FakeLspAdapter {
8989 capabilities: lsp::ServerCapabilities {
8990 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8991 ..Default::default()
8992 },
8993 ..Default::default()
8994 },
8995 );
8996
8997 let buffer = project
8998 .update(cx, |project, cx| {
8999 project.open_local_buffer(path!("/file.rs"), cx)
9000 })
9001 .await
9002 .unwrap();
9003
9004 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9005 let (editor, cx) = cx.add_window_view(|window, cx| {
9006 build_editor_with_project(project.clone(), buffer, window, cx)
9007 });
9008 editor.update_in(cx, |editor, window, cx| {
9009 editor.set_text("one\ntwo\nthree\n", window, cx)
9010 });
9011 assert!(cx.read(|cx| editor.is_dirty(cx)));
9012
9013 cx.executor().start_waiting();
9014 let fake_server = fake_servers.next().await.unwrap();
9015
9016 let save = editor
9017 .update_in(cx, |editor, window, cx| {
9018 editor.save(true, project.clone(), window, cx)
9019 })
9020 .unwrap();
9021 fake_server
9022 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9023 assert_eq!(
9024 params.text_document.uri,
9025 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9026 );
9027 assert_eq!(params.options.tab_size, 4);
9028 Ok(Some(vec![lsp::TextEdit::new(
9029 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9030 ", ".to_string(),
9031 )]))
9032 })
9033 .next()
9034 .await;
9035 cx.executor().start_waiting();
9036 save.await;
9037 assert_eq!(
9038 editor.update(cx, |editor, cx| editor.text(cx)),
9039 "one, two\nthree\n"
9040 );
9041 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9042
9043 editor.update_in(cx, |editor, window, cx| {
9044 editor.set_text("one\ntwo\nthree\n", window, cx)
9045 });
9046 assert!(cx.read(|cx| editor.is_dirty(cx)));
9047
9048 // Ensure we can still save even if formatting hangs.
9049 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9050 move |params, _| async move {
9051 assert_eq!(
9052 params.text_document.uri,
9053 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9054 );
9055 futures::future::pending::<()>().await;
9056 unreachable!()
9057 },
9058 );
9059 let save = editor
9060 .update_in(cx, |editor, window, cx| {
9061 editor.save(true, project.clone(), window, cx)
9062 })
9063 .unwrap();
9064 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9065 cx.executor().start_waiting();
9066 save.await;
9067 assert_eq!(
9068 editor.update(cx, |editor, cx| editor.text(cx)),
9069 "one\ntwo\nthree\n"
9070 );
9071 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9072
9073 // For non-dirty buffer, no formatting request should be sent
9074 let save = editor
9075 .update_in(cx, |editor, window, cx| {
9076 editor.save(true, project.clone(), window, cx)
9077 })
9078 .unwrap();
9079 let _pending_format_request = fake_server
9080 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9081 panic!("Should not be invoked on non-dirty buffer");
9082 })
9083 .next();
9084 cx.executor().start_waiting();
9085 save.await;
9086
9087 // Set Rust language override and assert overridden tabsize is sent to language server
9088 update_test_language_settings(cx, |settings| {
9089 settings.languages.insert(
9090 "Rust".into(),
9091 LanguageSettingsContent {
9092 tab_size: NonZeroU32::new(8),
9093 ..Default::default()
9094 },
9095 );
9096 });
9097
9098 editor.update_in(cx, |editor, window, cx| {
9099 editor.set_text("somehting_new\n", window, cx)
9100 });
9101 assert!(cx.read(|cx| editor.is_dirty(cx)));
9102 let save = editor
9103 .update_in(cx, |editor, window, cx| {
9104 editor.save(true, project.clone(), window, cx)
9105 })
9106 .unwrap();
9107 fake_server
9108 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9109 assert_eq!(
9110 params.text_document.uri,
9111 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9112 );
9113 assert_eq!(params.options.tab_size, 8);
9114 Ok(Some(Vec::new()))
9115 })
9116 .next()
9117 .await;
9118 save.await;
9119}
9120
9121#[gpui::test]
9122async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9123 init_test(cx, |settings| {
9124 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9125 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9126 ))
9127 });
9128
9129 let fs = FakeFs::new(cx.executor());
9130 fs.insert_file(path!("/file.rs"), Default::default()).await;
9131
9132 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9133
9134 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9135 language_registry.add(Arc::new(Language::new(
9136 LanguageConfig {
9137 name: "Rust".into(),
9138 matcher: LanguageMatcher {
9139 path_suffixes: vec!["rs".to_string()],
9140 ..Default::default()
9141 },
9142 ..LanguageConfig::default()
9143 },
9144 Some(tree_sitter_rust::LANGUAGE.into()),
9145 )));
9146 update_test_language_settings(cx, |settings| {
9147 // Enable Prettier formatting for the same buffer, and ensure
9148 // LSP is called instead of Prettier.
9149 settings.defaults.prettier = Some(PrettierSettings {
9150 allowed: true,
9151 ..PrettierSettings::default()
9152 });
9153 });
9154 let mut fake_servers = language_registry.register_fake_lsp(
9155 "Rust",
9156 FakeLspAdapter {
9157 capabilities: lsp::ServerCapabilities {
9158 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9159 ..Default::default()
9160 },
9161 ..Default::default()
9162 },
9163 );
9164
9165 let buffer = project
9166 .update(cx, |project, cx| {
9167 project.open_local_buffer(path!("/file.rs"), cx)
9168 })
9169 .await
9170 .unwrap();
9171
9172 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9173 let (editor, cx) = cx.add_window_view(|window, cx| {
9174 build_editor_with_project(project.clone(), buffer, window, cx)
9175 });
9176 editor.update_in(cx, |editor, window, cx| {
9177 editor.set_text("one\ntwo\nthree\n", window, cx)
9178 });
9179
9180 cx.executor().start_waiting();
9181 let fake_server = fake_servers.next().await.unwrap();
9182
9183 let format = editor
9184 .update_in(cx, |editor, window, cx| {
9185 editor.perform_format(
9186 project.clone(),
9187 FormatTrigger::Manual,
9188 FormatTarget::Buffers,
9189 window,
9190 cx,
9191 )
9192 })
9193 .unwrap();
9194 fake_server
9195 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9196 assert_eq!(
9197 params.text_document.uri,
9198 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9199 );
9200 assert_eq!(params.options.tab_size, 4);
9201 Ok(Some(vec![lsp::TextEdit::new(
9202 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9203 ", ".to_string(),
9204 )]))
9205 })
9206 .next()
9207 .await;
9208 cx.executor().start_waiting();
9209 format.await;
9210 assert_eq!(
9211 editor.update(cx, |editor, cx| editor.text(cx)),
9212 "one, two\nthree\n"
9213 );
9214
9215 editor.update_in(cx, |editor, window, cx| {
9216 editor.set_text("one\ntwo\nthree\n", window, cx)
9217 });
9218 // Ensure we don't lock if formatting hangs.
9219 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9220 move |params, _| async move {
9221 assert_eq!(
9222 params.text_document.uri,
9223 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9224 );
9225 futures::future::pending::<()>().await;
9226 unreachable!()
9227 },
9228 );
9229 let format = editor
9230 .update_in(cx, |editor, window, cx| {
9231 editor.perform_format(
9232 project,
9233 FormatTrigger::Manual,
9234 FormatTarget::Buffers,
9235 window,
9236 cx,
9237 )
9238 })
9239 .unwrap();
9240 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9241 cx.executor().start_waiting();
9242 format.await;
9243 assert_eq!(
9244 editor.update(cx, |editor, cx| editor.text(cx)),
9245 "one\ntwo\nthree\n"
9246 );
9247}
9248
9249#[gpui::test]
9250async fn test_multiple_formatters(cx: &mut TestAppContext) {
9251 init_test(cx, |settings| {
9252 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9253 settings.defaults.formatter =
9254 Some(language_settings::SelectedFormatter::List(FormatterList(
9255 vec![
9256 Formatter::LanguageServer { name: None },
9257 Formatter::CodeActions(
9258 [
9259 ("code-action-1".into(), true),
9260 ("code-action-2".into(), true),
9261 ]
9262 .into_iter()
9263 .collect(),
9264 ),
9265 ]
9266 .into(),
9267 )))
9268 });
9269
9270 let fs = FakeFs::new(cx.executor());
9271 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9272 .await;
9273
9274 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9275 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9276 language_registry.add(rust_lang());
9277
9278 let mut fake_servers = language_registry.register_fake_lsp(
9279 "Rust",
9280 FakeLspAdapter {
9281 capabilities: lsp::ServerCapabilities {
9282 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9283 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9284 commands: vec!["the-command-for-code-action-1".into()],
9285 ..Default::default()
9286 }),
9287 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9288 ..Default::default()
9289 },
9290 ..Default::default()
9291 },
9292 );
9293
9294 let buffer = project
9295 .update(cx, |project, cx| {
9296 project.open_local_buffer(path!("/file.rs"), cx)
9297 })
9298 .await
9299 .unwrap();
9300
9301 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9302 let (editor, cx) = cx.add_window_view(|window, cx| {
9303 build_editor_with_project(project.clone(), buffer, window, cx)
9304 });
9305
9306 cx.executor().start_waiting();
9307
9308 let fake_server = fake_servers.next().await.unwrap();
9309 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9310 move |_params, _| async move {
9311 Ok(Some(vec![lsp::TextEdit::new(
9312 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9313 "applied-formatting\n".to_string(),
9314 )]))
9315 },
9316 );
9317 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9318 move |params, _| async move {
9319 assert_eq!(
9320 params.context.only,
9321 Some(vec!["code-action-1".into(), "code-action-2".into()])
9322 );
9323 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9324 Ok(Some(vec![
9325 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9326 kind: Some("code-action-1".into()),
9327 edit: Some(lsp::WorkspaceEdit::new(
9328 [(
9329 uri.clone(),
9330 vec![lsp::TextEdit::new(
9331 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9332 "applied-code-action-1-edit\n".to_string(),
9333 )],
9334 )]
9335 .into_iter()
9336 .collect(),
9337 )),
9338 command: Some(lsp::Command {
9339 command: "the-command-for-code-action-1".into(),
9340 ..Default::default()
9341 }),
9342 ..Default::default()
9343 }),
9344 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9345 kind: Some("code-action-2".into()),
9346 edit: Some(lsp::WorkspaceEdit::new(
9347 [(
9348 uri.clone(),
9349 vec![lsp::TextEdit::new(
9350 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9351 "applied-code-action-2-edit\n".to_string(),
9352 )],
9353 )]
9354 .into_iter()
9355 .collect(),
9356 )),
9357 ..Default::default()
9358 }),
9359 ]))
9360 },
9361 );
9362
9363 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9364 move |params, _| async move { Ok(params) }
9365 });
9366
9367 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9368 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9369 let fake = fake_server.clone();
9370 let lock = command_lock.clone();
9371 move |params, _| {
9372 assert_eq!(params.command, "the-command-for-code-action-1");
9373 let fake = fake.clone();
9374 let lock = lock.clone();
9375 async move {
9376 lock.lock().await;
9377 fake.server
9378 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9379 label: None,
9380 edit: lsp::WorkspaceEdit {
9381 changes: Some(
9382 [(
9383 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9384 vec![lsp::TextEdit {
9385 range: lsp::Range::new(
9386 lsp::Position::new(0, 0),
9387 lsp::Position::new(0, 0),
9388 ),
9389 new_text: "applied-code-action-1-command\n".into(),
9390 }],
9391 )]
9392 .into_iter()
9393 .collect(),
9394 ),
9395 ..Default::default()
9396 },
9397 })
9398 .await
9399 .into_response()
9400 .unwrap();
9401 Ok(Some(json!(null)))
9402 }
9403 }
9404 });
9405
9406 cx.executor().start_waiting();
9407 editor
9408 .update_in(cx, |editor, window, cx| {
9409 editor.perform_format(
9410 project.clone(),
9411 FormatTrigger::Manual,
9412 FormatTarget::Buffers,
9413 window,
9414 cx,
9415 )
9416 })
9417 .unwrap()
9418 .await;
9419 editor.update(cx, |editor, cx| {
9420 assert_eq!(
9421 editor.text(cx),
9422 r#"
9423 applied-code-action-2-edit
9424 applied-code-action-1-command
9425 applied-code-action-1-edit
9426 applied-formatting
9427 one
9428 two
9429 three
9430 "#
9431 .unindent()
9432 );
9433 });
9434
9435 editor.update_in(cx, |editor, window, cx| {
9436 editor.undo(&Default::default(), window, cx);
9437 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9438 });
9439
9440 // Perform a manual edit while waiting for an LSP command
9441 // that's being run as part of a formatting code action.
9442 let lock_guard = command_lock.lock().await;
9443 let format = editor
9444 .update_in(cx, |editor, window, cx| {
9445 editor.perform_format(
9446 project.clone(),
9447 FormatTrigger::Manual,
9448 FormatTarget::Buffers,
9449 window,
9450 cx,
9451 )
9452 })
9453 .unwrap();
9454 cx.run_until_parked();
9455 editor.update(cx, |editor, cx| {
9456 assert_eq!(
9457 editor.text(cx),
9458 r#"
9459 applied-code-action-1-edit
9460 applied-formatting
9461 one
9462 two
9463 three
9464 "#
9465 .unindent()
9466 );
9467
9468 editor.buffer.update(cx, |buffer, cx| {
9469 let ix = buffer.len(cx);
9470 buffer.edit([(ix..ix, "edited\n")], None, cx);
9471 });
9472 });
9473
9474 // Allow the LSP command to proceed. Because the buffer was edited,
9475 // the second code action will not be run.
9476 drop(lock_guard);
9477 format.await;
9478 editor.update_in(cx, |editor, window, cx| {
9479 assert_eq!(
9480 editor.text(cx),
9481 r#"
9482 applied-code-action-1-command
9483 applied-code-action-1-edit
9484 applied-formatting
9485 one
9486 two
9487 three
9488 edited
9489 "#
9490 .unindent()
9491 );
9492
9493 // The manual edit is undone first, because it is the last thing the user did
9494 // (even though the command completed afterwards).
9495 editor.undo(&Default::default(), window, cx);
9496 assert_eq!(
9497 editor.text(cx),
9498 r#"
9499 applied-code-action-1-command
9500 applied-code-action-1-edit
9501 applied-formatting
9502 one
9503 two
9504 three
9505 "#
9506 .unindent()
9507 );
9508
9509 // All the formatting (including the command, which completed after the manual edit)
9510 // is undone together.
9511 editor.undo(&Default::default(), window, cx);
9512 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9513 });
9514}
9515
9516#[gpui::test]
9517async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9518 init_test(cx, |settings| {
9519 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9520 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9521 ))
9522 });
9523
9524 let fs = FakeFs::new(cx.executor());
9525 fs.insert_file(path!("/file.ts"), Default::default()).await;
9526
9527 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9528
9529 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9530 language_registry.add(Arc::new(Language::new(
9531 LanguageConfig {
9532 name: "TypeScript".into(),
9533 matcher: LanguageMatcher {
9534 path_suffixes: vec!["ts".to_string()],
9535 ..Default::default()
9536 },
9537 ..LanguageConfig::default()
9538 },
9539 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9540 )));
9541 update_test_language_settings(cx, |settings| {
9542 settings.defaults.prettier = Some(PrettierSettings {
9543 allowed: true,
9544 ..PrettierSettings::default()
9545 });
9546 });
9547 let mut fake_servers = language_registry.register_fake_lsp(
9548 "TypeScript",
9549 FakeLspAdapter {
9550 capabilities: lsp::ServerCapabilities {
9551 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9552 ..Default::default()
9553 },
9554 ..Default::default()
9555 },
9556 );
9557
9558 let buffer = project
9559 .update(cx, |project, cx| {
9560 project.open_local_buffer(path!("/file.ts"), cx)
9561 })
9562 .await
9563 .unwrap();
9564
9565 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9566 let (editor, cx) = cx.add_window_view(|window, cx| {
9567 build_editor_with_project(project.clone(), buffer, window, cx)
9568 });
9569 editor.update_in(cx, |editor, window, cx| {
9570 editor.set_text(
9571 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9572 window,
9573 cx,
9574 )
9575 });
9576
9577 cx.executor().start_waiting();
9578 let fake_server = fake_servers.next().await.unwrap();
9579
9580 let format = editor
9581 .update_in(cx, |editor, window, cx| {
9582 editor.perform_code_action_kind(
9583 project.clone(),
9584 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9585 window,
9586 cx,
9587 )
9588 })
9589 .unwrap();
9590 fake_server
9591 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9592 assert_eq!(
9593 params.text_document.uri,
9594 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9595 );
9596 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9597 lsp::CodeAction {
9598 title: "Organize Imports".to_string(),
9599 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9600 edit: Some(lsp::WorkspaceEdit {
9601 changes: Some(
9602 [(
9603 params.text_document.uri.clone(),
9604 vec![lsp::TextEdit::new(
9605 lsp::Range::new(
9606 lsp::Position::new(1, 0),
9607 lsp::Position::new(2, 0),
9608 ),
9609 "".to_string(),
9610 )],
9611 )]
9612 .into_iter()
9613 .collect(),
9614 ),
9615 ..Default::default()
9616 }),
9617 ..Default::default()
9618 },
9619 )]))
9620 })
9621 .next()
9622 .await;
9623 cx.executor().start_waiting();
9624 format.await;
9625 assert_eq!(
9626 editor.update(cx, |editor, cx| editor.text(cx)),
9627 "import { a } from 'module';\n\nconst x = a;\n"
9628 );
9629
9630 editor.update_in(cx, |editor, window, cx| {
9631 editor.set_text(
9632 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9633 window,
9634 cx,
9635 )
9636 });
9637 // Ensure we don't lock if code action hangs.
9638 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9639 move |params, _| async move {
9640 assert_eq!(
9641 params.text_document.uri,
9642 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9643 );
9644 futures::future::pending::<()>().await;
9645 unreachable!()
9646 },
9647 );
9648 let format = editor
9649 .update_in(cx, |editor, window, cx| {
9650 editor.perform_code_action_kind(
9651 project,
9652 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9653 window,
9654 cx,
9655 )
9656 })
9657 .unwrap();
9658 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9659 cx.executor().start_waiting();
9660 format.await;
9661 assert_eq!(
9662 editor.update(cx, |editor, cx| editor.text(cx)),
9663 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9664 );
9665}
9666
9667#[gpui::test]
9668async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9669 init_test(cx, |_| {});
9670
9671 let mut cx = EditorLspTestContext::new_rust(
9672 lsp::ServerCapabilities {
9673 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9674 ..Default::default()
9675 },
9676 cx,
9677 )
9678 .await;
9679
9680 cx.set_state(indoc! {"
9681 one.twoˇ
9682 "});
9683
9684 // The format request takes a long time. When it completes, it inserts
9685 // a newline and an indent before the `.`
9686 cx.lsp
9687 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9688 let executor = cx.background_executor().clone();
9689 async move {
9690 executor.timer(Duration::from_millis(100)).await;
9691 Ok(Some(vec![lsp::TextEdit {
9692 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9693 new_text: "\n ".into(),
9694 }]))
9695 }
9696 });
9697
9698 // Submit a format request.
9699 let format_1 = cx
9700 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9701 .unwrap();
9702 cx.executor().run_until_parked();
9703
9704 // Submit a second format request.
9705 let format_2 = cx
9706 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9707 .unwrap();
9708 cx.executor().run_until_parked();
9709
9710 // Wait for both format requests to complete
9711 cx.executor().advance_clock(Duration::from_millis(200));
9712 cx.executor().start_waiting();
9713 format_1.await.unwrap();
9714 cx.executor().start_waiting();
9715 format_2.await.unwrap();
9716
9717 // The formatting edits only happens once.
9718 cx.assert_editor_state(indoc! {"
9719 one
9720 .twoˇ
9721 "});
9722}
9723
9724#[gpui::test]
9725async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9726 init_test(cx, |settings| {
9727 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9728 });
9729
9730 let mut cx = EditorLspTestContext::new_rust(
9731 lsp::ServerCapabilities {
9732 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9733 ..Default::default()
9734 },
9735 cx,
9736 )
9737 .await;
9738
9739 // Set up a buffer white some trailing whitespace and no trailing newline.
9740 cx.set_state(
9741 &[
9742 "one ", //
9743 "twoˇ", //
9744 "three ", //
9745 "four", //
9746 ]
9747 .join("\n"),
9748 );
9749
9750 // Submit a format request.
9751 let format = cx
9752 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9753 .unwrap();
9754
9755 // Record which buffer changes have been sent to the language server
9756 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9757 cx.lsp
9758 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9759 let buffer_changes = buffer_changes.clone();
9760 move |params, _| {
9761 buffer_changes.lock().extend(
9762 params
9763 .content_changes
9764 .into_iter()
9765 .map(|e| (e.range.unwrap(), e.text)),
9766 );
9767 }
9768 });
9769
9770 // Handle formatting requests to the language server.
9771 cx.lsp
9772 .set_request_handler::<lsp::request::Formatting, _, _>({
9773 let buffer_changes = buffer_changes.clone();
9774 move |_, _| {
9775 // When formatting is requested, trailing whitespace has already been stripped,
9776 // and the trailing newline has already been added.
9777 assert_eq!(
9778 &buffer_changes.lock()[1..],
9779 &[
9780 (
9781 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9782 "".into()
9783 ),
9784 (
9785 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9786 "".into()
9787 ),
9788 (
9789 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9790 "\n".into()
9791 ),
9792 ]
9793 );
9794
9795 // Insert blank lines between each line of the buffer.
9796 async move {
9797 Ok(Some(vec![
9798 lsp::TextEdit {
9799 range: lsp::Range::new(
9800 lsp::Position::new(1, 0),
9801 lsp::Position::new(1, 0),
9802 ),
9803 new_text: "\n".into(),
9804 },
9805 lsp::TextEdit {
9806 range: lsp::Range::new(
9807 lsp::Position::new(2, 0),
9808 lsp::Position::new(2, 0),
9809 ),
9810 new_text: "\n".into(),
9811 },
9812 ]))
9813 }
9814 }
9815 });
9816
9817 // After formatting the buffer, the trailing whitespace is stripped,
9818 // a newline is appended, and the edits provided by the language server
9819 // have been applied.
9820 format.await.unwrap();
9821 cx.assert_editor_state(
9822 &[
9823 "one", //
9824 "", //
9825 "twoˇ", //
9826 "", //
9827 "three", //
9828 "four", //
9829 "", //
9830 ]
9831 .join("\n"),
9832 );
9833
9834 // Undoing the formatting undoes the trailing whitespace removal, the
9835 // trailing newline, and the LSP edits.
9836 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9837 cx.assert_editor_state(
9838 &[
9839 "one ", //
9840 "twoˇ", //
9841 "three ", //
9842 "four", //
9843 ]
9844 .join("\n"),
9845 );
9846}
9847
9848#[gpui::test]
9849async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9850 cx: &mut TestAppContext,
9851) {
9852 init_test(cx, |_| {});
9853
9854 cx.update(|cx| {
9855 cx.update_global::<SettingsStore, _>(|settings, cx| {
9856 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9857 settings.auto_signature_help = Some(true);
9858 });
9859 });
9860 });
9861
9862 let mut cx = EditorLspTestContext::new_rust(
9863 lsp::ServerCapabilities {
9864 signature_help_provider: Some(lsp::SignatureHelpOptions {
9865 ..Default::default()
9866 }),
9867 ..Default::default()
9868 },
9869 cx,
9870 )
9871 .await;
9872
9873 let language = Language::new(
9874 LanguageConfig {
9875 name: "Rust".into(),
9876 brackets: BracketPairConfig {
9877 pairs: vec![
9878 BracketPair {
9879 start: "{".to_string(),
9880 end: "}".to_string(),
9881 close: true,
9882 surround: true,
9883 newline: true,
9884 },
9885 BracketPair {
9886 start: "(".to_string(),
9887 end: ")".to_string(),
9888 close: true,
9889 surround: true,
9890 newline: true,
9891 },
9892 BracketPair {
9893 start: "/*".to_string(),
9894 end: " */".to_string(),
9895 close: true,
9896 surround: true,
9897 newline: true,
9898 },
9899 BracketPair {
9900 start: "[".to_string(),
9901 end: "]".to_string(),
9902 close: false,
9903 surround: false,
9904 newline: true,
9905 },
9906 BracketPair {
9907 start: "\"".to_string(),
9908 end: "\"".to_string(),
9909 close: true,
9910 surround: true,
9911 newline: false,
9912 },
9913 BracketPair {
9914 start: "<".to_string(),
9915 end: ">".to_string(),
9916 close: false,
9917 surround: true,
9918 newline: true,
9919 },
9920 ],
9921 ..Default::default()
9922 },
9923 autoclose_before: "})]".to_string(),
9924 ..Default::default()
9925 },
9926 Some(tree_sitter_rust::LANGUAGE.into()),
9927 );
9928 let language = Arc::new(language);
9929
9930 cx.language_registry().add(language.clone());
9931 cx.update_buffer(|buffer, cx| {
9932 buffer.set_language(Some(language), cx);
9933 });
9934
9935 cx.set_state(
9936 &r#"
9937 fn main() {
9938 sampleˇ
9939 }
9940 "#
9941 .unindent(),
9942 );
9943
9944 cx.update_editor(|editor, window, cx| {
9945 editor.handle_input("(", window, cx);
9946 });
9947 cx.assert_editor_state(
9948 &"
9949 fn main() {
9950 sample(ˇ)
9951 }
9952 "
9953 .unindent(),
9954 );
9955
9956 let mocked_response = lsp::SignatureHelp {
9957 signatures: vec![lsp::SignatureInformation {
9958 label: "fn sample(param1: u8, param2: u8)".to_string(),
9959 documentation: None,
9960 parameters: Some(vec![
9961 lsp::ParameterInformation {
9962 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9963 documentation: None,
9964 },
9965 lsp::ParameterInformation {
9966 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9967 documentation: None,
9968 },
9969 ]),
9970 active_parameter: None,
9971 }],
9972 active_signature: Some(0),
9973 active_parameter: Some(0),
9974 };
9975 handle_signature_help_request(&mut cx, mocked_response).await;
9976
9977 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9978 .await;
9979
9980 cx.editor(|editor, _, _| {
9981 let signature_help_state = editor.signature_help_state.popover().cloned();
9982 assert_eq!(
9983 signature_help_state.unwrap().label,
9984 "param1: u8, param2: u8"
9985 );
9986 });
9987}
9988
9989#[gpui::test]
9990async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9991 init_test(cx, |_| {});
9992
9993 cx.update(|cx| {
9994 cx.update_global::<SettingsStore, _>(|settings, cx| {
9995 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9996 settings.auto_signature_help = Some(false);
9997 settings.show_signature_help_after_edits = Some(false);
9998 });
9999 });
10000 });
10001
10002 let mut cx = EditorLspTestContext::new_rust(
10003 lsp::ServerCapabilities {
10004 signature_help_provider: Some(lsp::SignatureHelpOptions {
10005 ..Default::default()
10006 }),
10007 ..Default::default()
10008 },
10009 cx,
10010 )
10011 .await;
10012
10013 let language = Language::new(
10014 LanguageConfig {
10015 name: "Rust".into(),
10016 brackets: BracketPairConfig {
10017 pairs: vec![
10018 BracketPair {
10019 start: "{".to_string(),
10020 end: "}".to_string(),
10021 close: true,
10022 surround: true,
10023 newline: true,
10024 },
10025 BracketPair {
10026 start: "(".to_string(),
10027 end: ")".to_string(),
10028 close: true,
10029 surround: true,
10030 newline: true,
10031 },
10032 BracketPair {
10033 start: "/*".to_string(),
10034 end: " */".to_string(),
10035 close: true,
10036 surround: true,
10037 newline: true,
10038 },
10039 BracketPair {
10040 start: "[".to_string(),
10041 end: "]".to_string(),
10042 close: false,
10043 surround: false,
10044 newline: true,
10045 },
10046 BracketPair {
10047 start: "\"".to_string(),
10048 end: "\"".to_string(),
10049 close: true,
10050 surround: true,
10051 newline: false,
10052 },
10053 BracketPair {
10054 start: "<".to_string(),
10055 end: ">".to_string(),
10056 close: false,
10057 surround: true,
10058 newline: true,
10059 },
10060 ],
10061 ..Default::default()
10062 },
10063 autoclose_before: "})]".to_string(),
10064 ..Default::default()
10065 },
10066 Some(tree_sitter_rust::LANGUAGE.into()),
10067 );
10068 let language = Arc::new(language);
10069
10070 cx.language_registry().add(language.clone());
10071 cx.update_buffer(|buffer, cx| {
10072 buffer.set_language(Some(language), cx);
10073 });
10074
10075 // Ensure that signature_help is not called when no signature help is enabled.
10076 cx.set_state(
10077 &r#"
10078 fn main() {
10079 sampleˇ
10080 }
10081 "#
10082 .unindent(),
10083 );
10084 cx.update_editor(|editor, window, cx| {
10085 editor.handle_input("(", window, cx);
10086 });
10087 cx.assert_editor_state(
10088 &"
10089 fn main() {
10090 sample(ˇ)
10091 }
10092 "
10093 .unindent(),
10094 );
10095 cx.editor(|editor, _, _| {
10096 assert!(editor.signature_help_state.task().is_none());
10097 });
10098
10099 let mocked_response = lsp::SignatureHelp {
10100 signatures: vec![lsp::SignatureInformation {
10101 label: "fn sample(param1: u8, param2: u8)".to_string(),
10102 documentation: None,
10103 parameters: Some(vec![
10104 lsp::ParameterInformation {
10105 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10106 documentation: None,
10107 },
10108 lsp::ParameterInformation {
10109 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10110 documentation: None,
10111 },
10112 ]),
10113 active_parameter: None,
10114 }],
10115 active_signature: Some(0),
10116 active_parameter: Some(0),
10117 };
10118
10119 // Ensure that signature_help is called when enabled afte edits
10120 cx.update(|_, cx| {
10121 cx.update_global::<SettingsStore, _>(|settings, cx| {
10122 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10123 settings.auto_signature_help = Some(false);
10124 settings.show_signature_help_after_edits = Some(true);
10125 });
10126 });
10127 });
10128 cx.set_state(
10129 &r#"
10130 fn main() {
10131 sampleˇ
10132 }
10133 "#
10134 .unindent(),
10135 );
10136 cx.update_editor(|editor, window, cx| {
10137 editor.handle_input("(", window, cx);
10138 });
10139 cx.assert_editor_state(
10140 &"
10141 fn main() {
10142 sample(ˇ)
10143 }
10144 "
10145 .unindent(),
10146 );
10147 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10148 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10149 .await;
10150 cx.update_editor(|editor, _, _| {
10151 let signature_help_state = editor.signature_help_state.popover().cloned();
10152 assert!(signature_help_state.is_some());
10153 assert_eq!(
10154 signature_help_state.unwrap().label,
10155 "param1: u8, param2: u8"
10156 );
10157 editor.signature_help_state = SignatureHelpState::default();
10158 });
10159
10160 // Ensure that signature_help is called when auto signature help override is enabled
10161 cx.update(|_, cx| {
10162 cx.update_global::<SettingsStore, _>(|settings, cx| {
10163 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10164 settings.auto_signature_help = Some(true);
10165 settings.show_signature_help_after_edits = Some(false);
10166 });
10167 });
10168 });
10169 cx.set_state(
10170 &r#"
10171 fn main() {
10172 sampleˇ
10173 }
10174 "#
10175 .unindent(),
10176 );
10177 cx.update_editor(|editor, window, cx| {
10178 editor.handle_input("(", window, cx);
10179 });
10180 cx.assert_editor_state(
10181 &"
10182 fn main() {
10183 sample(ˇ)
10184 }
10185 "
10186 .unindent(),
10187 );
10188 handle_signature_help_request(&mut cx, mocked_response).await;
10189 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10190 .await;
10191 cx.editor(|editor, _, _| {
10192 let signature_help_state = editor.signature_help_state.popover().cloned();
10193 assert!(signature_help_state.is_some());
10194 assert_eq!(
10195 signature_help_state.unwrap().label,
10196 "param1: u8, param2: u8"
10197 );
10198 });
10199}
10200
10201#[gpui::test]
10202async fn test_signature_help(cx: &mut TestAppContext) {
10203 init_test(cx, |_| {});
10204 cx.update(|cx| {
10205 cx.update_global::<SettingsStore, _>(|settings, cx| {
10206 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10207 settings.auto_signature_help = Some(true);
10208 });
10209 });
10210 });
10211
10212 let mut cx = EditorLspTestContext::new_rust(
10213 lsp::ServerCapabilities {
10214 signature_help_provider: Some(lsp::SignatureHelpOptions {
10215 ..Default::default()
10216 }),
10217 ..Default::default()
10218 },
10219 cx,
10220 )
10221 .await;
10222
10223 // A test that directly calls `show_signature_help`
10224 cx.update_editor(|editor, window, cx| {
10225 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10226 });
10227
10228 let mocked_response = lsp::SignatureHelp {
10229 signatures: vec![lsp::SignatureInformation {
10230 label: "fn sample(param1: u8, param2: u8)".to_string(),
10231 documentation: None,
10232 parameters: Some(vec![
10233 lsp::ParameterInformation {
10234 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10235 documentation: None,
10236 },
10237 lsp::ParameterInformation {
10238 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10239 documentation: None,
10240 },
10241 ]),
10242 active_parameter: None,
10243 }],
10244 active_signature: Some(0),
10245 active_parameter: Some(0),
10246 };
10247 handle_signature_help_request(&mut cx, mocked_response).await;
10248
10249 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10250 .await;
10251
10252 cx.editor(|editor, _, _| {
10253 let signature_help_state = editor.signature_help_state.popover().cloned();
10254 assert!(signature_help_state.is_some());
10255 assert_eq!(
10256 signature_help_state.unwrap().label,
10257 "param1: u8, param2: u8"
10258 );
10259 });
10260
10261 // When exiting outside from inside the brackets, `signature_help` is closed.
10262 cx.set_state(indoc! {"
10263 fn main() {
10264 sample(ˇ);
10265 }
10266
10267 fn sample(param1: u8, param2: u8) {}
10268 "});
10269
10270 cx.update_editor(|editor, window, cx| {
10271 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10272 });
10273
10274 let mocked_response = lsp::SignatureHelp {
10275 signatures: Vec::new(),
10276 active_signature: None,
10277 active_parameter: None,
10278 };
10279 handle_signature_help_request(&mut cx, mocked_response).await;
10280
10281 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10282 .await;
10283
10284 cx.editor(|editor, _, _| {
10285 assert!(!editor.signature_help_state.is_shown());
10286 });
10287
10288 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10289 cx.set_state(indoc! {"
10290 fn main() {
10291 sample(ˇ);
10292 }
10293
10294 fn sample(param1: u8, param2: u8) {}
10295 "});
10296
10297 let mocked_response = lsp::SignatureHelp {
10298 signatures: vec![lsp::SignatureInformation {
10299 label: "fn sample(param1: u8, param2: u8)".to_string(),
10300 documentation: None,
10301 parameters: Some(vec![
10302 lsp::ParameterInformation {
10303 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10304 documentation: None,
10305 },
10306 lsp::ParameterInformation {
10307 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10308 documentation: None,
10309 },
10310 ]),
10311 active_parameter: None,
10312 }],
10313 active_signature: Some(0),
10314 active_parameter: Some(0),
10315 };
10316 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10317 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10318 .await;
10319 cx.editor(|editor, _, _| {
10320 assert!(editor.signature_help_state.is_shown());
10321 });
10322
10323 // Restore the popover with more parameter input
10324 cx.set_state(indoc! {"
10325 fn main() {
10326 sample(param1, param2ˇ);
10327 }
10328
10329 fn sample(param1: u8, param2: u8) {}
10330 "});
10331
10332 let mocked_response = lsp::SignatureHelp {
10333 signatures: vec![lsp::SignatureInformation {
10334 label: "fn sample(param1: u8, param2: u8)".to_string(),
10335 documentation: None,
10336 parameters: Some(vec![
10337 lsp::ParameterInformation {
10338 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10339 documentation: None,
10340 },
10341 lsp::ParameterInformation {
10342 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10343 documentation: None,
10344 },
10345 ]),
10346 active_parameter: None,
10347 }],
10348 active_signature: Some(0),
10349 active_parameter: Some(1),
10350 };
10351 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10352 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10353 .await;
10354
10355 // When selecting a range, the popover is gone.
10356 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10357 cx.update_editor(|editor, window, cx| {
10358 editor.change_selections(None, window, cx, |s| {
10359 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10360 })
10361 });
10362 cx.assert_editor_state(indoc! {"
10363 fn main() {
10364 sample(param1, «ˇparam2»);
10365 }
10366
10367 fn sample(param1: u8, param2: u8) {}
10368 "});
10369 cx.editor(|editor, _, _| {
10370 assert!(!editor.signature_help_state.is_shown());
10371 });
10372
10373 // When unselecting again, the popover is back if within the brackets.
10374 cx.update_editor(|editor, window, cx| {
10375 editor.change_selections(None, window, cx, |s| {
10376 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10377 })
10378 });
10379 cx.assert_editor_state(indoc! {"
10380 fn main() {
10381 sample(param1, ˇparam2);
10382 }
10383
10384 fn sample(param1: u8, param2: u8) {}
10385 "});
10386 handle_signature_help_request(&mut cx, mocked_response).await;
10387 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10388 .await;
10389 cx.editor(|editor, _, _| {
10390 assert!(editor.signature_help_state.is_shown());
10391 });
10392
10393 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10394 cx.update_editor(|editor, window, cx| {
10395 editor.change_selections(None, window, cx, |s| {
10396 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10397 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10398 })
10399 });
10400 cx.assert_editor_state(indoc! {"
10401 fn main() {
10402 sample(param1, ˇparam2);
10403 }
10404
10405 fn sample(param1: u8, param2: u8) {}
10406 "});
10407
10408 let mocked_response = lsp::SignatureHelp {
10409 signatures: vec![lsp::SignatureInformation {
10410 label: "fn sample(param1: u8, param2: u8)".to_string(),
10411 documentation: None,
10412 parameters: Some(vec![
10413 lsp::ParameterInformation {
10414 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10415 documentation: None,
10416 },
10417 lsp::ParameterInformation {
10418 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10419 documentation: None,
10420 },
10421 ]),
10422 active_parameter: None,
10423 }],
10424 active_signature: Some(0),
10425 active_parameter: Some(1),
10426 };
10427 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10428 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10429 .await;
10430 cx.update_editor(|editor, _, cx| {
10431 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10432 });
10433 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10434 .await;
10435 cx.update_editor(|editor, window, cx| {
10436 editor.change_selections(None, window, cx, |s| {
10437 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10438 })
10439 });
10440 cx.assert_editor_state(indoc! {"
10441 fn main() {
10442 sample(param1, «ˇparam2»);
10443 }
10444
10445 fn sample(param1: u8, param2: u8) {}
10446 "});
10447 cx.update_editor(|editor, window, cx| {
10448 editor.change_selections(None, window, cx, |s| {
10449 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10450 })
10451 });
10452 cx.assert_editor_state(indoc! {"
10453 fn main() {
10454 sample(param1, ˇparam2);
10455 }
10456
10457 fn sample(param1: u8, param2: u8) {}
10458 "});
10459 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10460 .await;
10461}
10462
10463#[gpui::test]
10464async fn test_completion_mode(cx: &mut TestAppContext) {
10465 init_test(cx, |_| {});
10466 let mut cx = EditorLspTestContext::new_rust(
10467 lsp::ServerCapabilities {
10468 completion_provider: Some(lsp::CompletionOptions {
10469 resolve_provider: Some(true),
10470 ..Default::default()
10471 }),
10472 ..Default::default()
10473 },
10474 cx,
10475 )
10476 .await;
10477
10478 struct Run {
10479 run_description: &'static str,
10480 initial_state: String,
10481 buffer_marked_text: String,
10482 completion_text: &'static str,
10483 expected_with_insert_mode: String,
10484 expected_with_replace_mode: String,
10485 expected_with_replace_subsequence_mode: String,
10486 expected_with_replace_suffix_mode: String,
10487 }
10488
10489 let runs = [
10490 Run {
10491 run_description: "Start of word matches completion text",
10492 initial_state: "before ediˇ after".into(),
10493 buffer_marked_text: "before <edi|> after".into(),
10494 completion_text: "editor",
10495 expected_with_insert_mode: "before editorˇ after".into(),
10496 expected_with_replace_mode: "before editorˇ after".into(),
10497 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10498 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10499 },
10500 Run {
10501 run_description: "Accept same text at the middle of the word",
10502 initial_state: "before ediˇtor after".into(),
10503 buffer_marked_text: "before <edi|tor> after".into(),
10504 completion_text: "editor",
10505 expected_with_insert_mode: "before editorˇtor after".into(),
10506 expected_with_replace_mode: "before editorˇ after".into(),
10507 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10508 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10509 },
10510 Run {
10511 run_description: "End of word matches completion text -- cursor at end",
10512 initial_state: "before torˇ after".into(),
10513 buffer_marked_text: "before <tor|> after".into(),
10514 completion_text: "editor",
10515 expected_with_insert_mode: "before editorˇ after".into(),
10516 expected_with_replace_mode: "before editorˇ after".into(),
10517 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10518 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10519 },
10520 Run {
10521 run_description: "End of word matches completion text -- cursor at start",
10522 initial_state: "before ˇtor after".into(),
10523 buffer_marked_text: "before <|tor> after".into(),
10524 completion_text: "editor",
10525 expected_with_insert_mode: "before editorˇtor after".into(),
10526 expected_with_replace_mode: "before editorˇ after".into(),
10527 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10528 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10529 },
10530 Run {
10531 run_description: "Prepend text containing whitespace",
10532 initial_state: "pˇfield: bool".into(),
10533 buffer_marked_text: "<p|field>: bool".into(),
10534 completion_text: "pub ",
10535 expected_with_insert_mode: "pub ˇfield: bool".into(),
10536 expected_with_replace_mode: "pub ˇ: bool".into(),
10537 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10538 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10539 },
10540 Run {
10541 run_description: "Add element to start of list",
10542 initial_state: "[element_ˇelement_2]".into(),
10543 buffer_marked_text: "[<element_|element_2>]".into(),
10544 completion_text: "element_1",
10545 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10546 expected_with_replace_mode: "[element_1ˇ]".into(),
10547 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10548 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10549 },
10550 Run {
10551 run_description: "Add element to start of list -- first and second elements are equal",
10552 initial_state: "[elˇelement]".into(),
10553 buffer_marked_text: "[<el|element>]".into(),
10554 completion_text: "element",
10555 expected_with_insert_mode: "[elementˇelement]".into(),
10556 expected_with_replace_mode: "[elementˇ]".into(),
10557 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10558 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10559 },
10560 Run {
10561 run_description: "Ends with matching suffix",
10562 initial_state: "SubˇError".into(),
10563 buffer_marked_text: "<Sub|Error>".into(),
10564 completion_text: "SubscriptionError",
10565 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10566 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10567 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10568 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10569 },
10570 Run {
10571 run_description: "Suffix is a subsequence -- contiguous",
10572 initial_state: "SubˇErr".into(),
10573 buffer_marked_text: "<Sub|Err>".into(),
10574 completion_text: "SubscriptionError",
10575 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10576 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10577 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10578 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10579 },
10580 Run {
10581 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10582 initial_state: "Suˇscrirr".into(),
10583 buffer_marked_text: "<Su|scrirr>".into(),
10584 completion_text: "SubscriptionError",
10585 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10586 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10587 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10588 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10589 },
10590 Run {
10591 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10592 initial_state: "foo(indˇix)".into(),
10593 buffer_marked_text: "foo(<ind|ix>)".into(),
10594 completion_text: "node_index",
10595 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10596 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10597 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10598 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10599 },
10600 ];
10601
10602 for run in runs {
10603 let run_variations = [
10604 (LspInsertMode::Insert, run.expected_with_insert_mode),
10605 (LspInsertMode::Replace, run.expected_with_replace_mode),
10606 (
10607 LspInsertMode::ReplaceSubsequence,
10608 run.expected_with_replace_subsequence_mode,
10609 ),
10610 (
10611 LspInsertMode::ReplaceSuffix,
10612 run.expected_with_replace_suffix_mode,
10613 ),
10614 ];
10615
10616 for (lsp_insert_mode, expected_text) in run_variations {
10617 eprintln!(
10618 "run = {:?}, mode = {lsp_insert_mode:.?}",
10619 run.run_description,
10620 );
10621
10622 update_test_language_settings(&mut cx, |settings| {
10623 settings.defaults.completions = Some(CompletionSettings {
10624 lsp_insert_mode,
10625 words: WordsCompletionMode::Disabled,
10626 lsp: true,
10627 lsp_fetch_timeout_ms: 0,
10628 });
10629 });
10630
10631 cx.set_state(&run.initial_state);
10632 cx.update_editor(|editor, window, cx| {
10633 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10634 });
10635
10636 let counter = Arc::new(AtomicUsize::new(0));
10637 handle_completion_request_with_insert_and_replace(
10638 &mut cx,
10639 &run.buffer_marked_text,
10640 vec![run.completion_text],
10641 counter.clone(),
10642 )
10643 .await;
10644 cx.condition(|editor, _| editor.context_menu_visible())
10645 .await;
10646 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10647
10648 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10649 editor
10650 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10651 .unwrap()
10652 });
10653 cx.assert_editor_state(&expected_text);
10654 handle_resolve_completion_request(&mut cx, None).await;
10655 apply_additional_edits.await.unwrap();
10656 }
10657 }
10658}
10659
10660#[gpui::test]
10661async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10662 init_test(cx, |_| {});
10663 let mut cx = EditorLspTestContext::new_rust(
10664 lsp::ServerCapabilities {
10665 completion_provider: Some(lsp::CompletionOptions {
10666 resolve_provider: Some(true),
10667 ..Default::default()
10668 }),
10669 ..Default::default()
10670 },
10671 cx,
10672 )
10673 .await;
10674
10675 let initial_state = "SubˇError";
10676 let buffer_marked_text = "<Sub|Error>";
10677 let completion_text = "SubscriptionError";
10678 let expected_with_insert_mode = "SubscriptionErrorˇError";
10679 let expected_with_replace_mode = "SubscriptionErrorˇ";
10680
10681 update_test_language_settings(&mut cx, |settings| {
10682 settings.defaults.completions = Some(CompletionSettings {
10683 words: WordsCompletionMode::Disabled,
10684 // set the opposite here to ensure that the action is overriding the default behavior
10685 lsp_insert_mode: LspInsertMode::Insert,
10686 lsp: true,
10687 lsp_fetch_timeout_ms: 0,
10688 });
10689 });
10690
10691 cx.set_state(initial_state);
10692 cx.update_editor(|editor, window, cx| {
10693 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10694 });
10695
10696 let counter = Arc::new(AtomicUsize::new(0));
10697 handle_completion_request_with_insert_and_replace(
10698 &mut cx,
10699 &buffer_marked_text,
10700 vec![completion_text],
10701 counter.clone(),
10702 )
10703 .await;
10704 cx.condition(|editor, _| editor.context_menu_visible())
10705 .await;
10706 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10707
10708 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10709 editor
10710 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10711 .unwrap()
10712 });
10713 cx.assert_editor_state(&expected_with_replace_mode);
10714 handle_resolve_completion_request(&mut cx, None).await;
10715 apply_additional_edits.await.unwrap();
10716
10717 update_test_language_settings(&mut cx, |settings| {
10718 settings.defaults.completions = Some(CompletionSettings {
10719 words: WordsCompletionMode::Disabled,
10720 // set the opposite here to ensure that the action is overriding the default behavior
10721 lsp_insert_mode: LspInsertMode::Replace,
10722 lsp: true,
10723 lsp_fetch_timeout_ms: 0,
10724 });
10725 });
10726
10727 cx.set_state(initial_state);
10728 cx.update_editor(|editor, window, cx| {
10729 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10730 });
10731 handle_completion_request_with_insert_and_replace(
10732 &mut cx,
10733 &buffer_marked_text,
10734 vec![completion_text],
10735 counter.clone(),
10736 )
10737 .await;
10738 cx.condition(|editor, _| editor.context_menu_visible())
10739 .await;
10740 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10741
10742 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10743 editor
10744 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10745 .unwrap()
10746 });
10747 cx.assert_editor_state(&expected_with_insert_mode);
10748 handle_resolve_completion_request(&mut cx, None).await;
10749 apply_additional_edits.await.unwrap();
10750}
10751
10752#[gpui::test]
10753async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10754 init_test(cx, |_| {});
10755 let mut cx = EditorLspTestContext::new_rust(
10756 lsp::ServerCapabilities {
10757 completion_provider: Some(lsp::CompletionOptions {
10758 resolve_provider: Some(true),
10759 ..Default::default()
10760 }),
10761 ..Default::default()
10762 },
10763 cx,
10764 )
10765 .await;
10766
10767 // scenario: surrounding text matches completion text
10768 let completion_text = "to_offset";
10769 let initial_state = indoc! {"
10770 1. buf.to_offˇsuffix
10771 2. buf.to_offˇsuf
10772 3. buf.to_offˇfix
10773 4. buf.to_offˇ
10774 5. into_offˇensive
10775 6. ˇsuffix
10776 7. let ˇ //
10777 8. aaˇzz
10778 9. buf.to_off«zzzzzˇ»suffix
10779 10. buf.«ˇzzzzz»suffix
10780 11. to_off«ˇzzzzz»
10781
10782 buf.to_offˇsuffix // newest cursor
10783 "};
10784 let completion_marked_buffer = indoc! {"
10785 1. buf.to_offsuffix
10786 2. buf.to_offsuf
10787 3. buf.to_offfix
10788 4. buf.to_off
10789 5. into_offensive
10790 6. suffix
10791 7. let //
10792 8. aazz
10793 9. buf.to_offzzzzzsuffix
10794 10. buf.zzzzzsuffix
10795 11. to_offzzzzz
10796
10797 buf.<to_off|suffix> // newest cursor
10798 "};
10799 let expected = indoc! {"
10800 1. buf.to_offsetˇ
10801 2. buf.to_offsetˇsuf
10802 3. buf.to_offsetˇfix
10803 4. buf.to_offsetˇ
10804 5. into_offsetˇensive
10805 6. to_offsetˇsuffix
10806 7. let to_offsetˇ //
10807 8. aato_offsetˇzz
10808 9. buf.to_offsetˇ
10809 10. buf.to_offsetˇsuffix
10810 11. to_offsetˇ
10811
10812 buf.to_offsetˇ // newest cursor
10813 "};
10814 cx.set_state(initial_state);
10815 cx.update_editor(|editor, window, cx| {
10816 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10817 });
10818 handle_completion_request_with_insert_and_replace(
10819 &mut cx,
10820 completion_marked_buffer,
10821 vec![completion_text],
10822 Arc::new(AtomicUsize::new(0)),
10823 )
10824 .await;
10825 cx.condition(|editor, _| editor.context_menu_visible())
10826 .await;
10827 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10828 editor
10829 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10830 .unwrap()
10831 });
10832 cx.assert_editor_state(expected);
10833 handle_resolve_completion_request(&mut cx, None).await;
10834 apply_additional_edits.await.unwrap();
10835
10836 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10837 let completion_text = "foo_and_bar";
10838 let initial_state = indoc! {"
10839 1. ooanbˇ
10840 2. zooanbˇ
10841 3. ooanbˇz
10842 4. zooanbˇz
10843 5. ooanˇ
10844 6. oanbˇ
10845
10846 ooanbˇ
10847 "};
10848 let completion_marked_buffer = indoc! {"
10849 1. ooanb
10850 2. zooanb
10851 3. ooanbz
10852 4. zooanbz
10853 5. ooan
10854 6. oanb
10855
10856 <ooanb|>
10857 "};
10858 let expected = indoc! {"
10859 1. foo_and_barˇ
10860 2. zfoo_and_barˇ
10861 3. foo_and_barˇz
10862 4. zfoo_and_barˇz
10863 5. ooanfoo_and_barˇ
10864 6. oanbfoo_and_barˇ
10865
10866 foo_and_barˇ
10867 "};
10868 cx.set_state(initial_state);
10869 cx.update_editor(|editor, window, cx| {
10870 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10871 });
10872 handle_completion_request_with_insert_and_replace(
10873 &mut cx,
10874 completion_marked_buffer,
10875 vec![completion_text],
10876 Arc::new(AtomicUsize::new(0)),
10877 )
10878 .await;
10879 cx.condition(|editor, _| editor.context_menu_visible())
10880 .await;
10881 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10882 editor
10883 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10884 .unwrap()
10885 });
10886 cx.assert_editor_state(expected);
10887 handle_resolve_completion_request(&mut cx, None).await;
10888 apply_additional_edits.await.unwrap();
10889
10890 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10891 // (expects the same as if it was inserted at the end)
10892 let completion_text = "foo_and_bar";
10893 let initial_state = indoc! {"
10894 1. ooˇanb
10895 2. zooˇanb
10896 3. ooˇanbz
10897 4. zooˇanbz
10898
10899 ooˇanb
10900 "};
10901 let completion_marked_buffer = indoc! {"
10902 1. ooanb
10903 2. zooanb
10904 3. ooanbz
10905 4. zooanbz
10906
10907 <oo|anb>
10908 "};
10909 let expected = indoc! {"
10910 1. foo_and_barˇ
10911 2. zfoo_and_barˇ
10912 3. foo_and_barˇz
10913 4. zfoo_and_barˇz
10914
10915 foo_and_barˇ
10916 "};
10917 cx.set_state(initial_state);
10918 cx.update_editor(|editor, window, cx| {
10919 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10920 });
10921 handle_completion_request_with_insert_and_replace(
10922 &mut cx,
10923 completion_marked_buffer,
10924 vec![completion_text],
10925 Arc::new(AtomicUsize::new(0)),
10926 )
10927 .await;
10928 cx.condition(|editor, _| editor.context_menu_visible())
10929 .await;
10930 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10931 editor
10932 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10933 .unwrap()
10934 });
10935 cx.assert_editor_state(expected);
10936 handle_resolve_completion_request(&mut cx, None).await;
10937 apply_additional_edits.await.unwrap();
10938}
10939
10940// This used to crash
10941#[gpui::test]
10942async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10943 init_test(cx, |_| {});
10944
10945 let buffer_text = indoc! {"
10946 fn main() {
10947 10.satu;
10948
10949 //
10950 // separate cursors so they open in different excerpts (manually reproducible)
10951 //
10952
10953 10.satu20;
10954 }
10955 "};
10956 let multibuffer_text_with_selections = indoc! {"
10957 fn main() {
10958 10.satuˇ;
10959
10960 //
10961
10962 //
10963
10964 10.satuˇ20;
10965 }
10966 "};
10967 let expected_multibuffer = indoc! {"
10968 fn main() {
10969 10.saturating_sub()ˇ;
10970
10971 //
10972
10973 //
10974
10975 10.saturating_sub()ˇ;
10976 }
10977 "};
10978
10979 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10980 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10981
10982 let fs = FakeFs::new(cx.executor());
10983 fs.insert_tree(
10984 path!("/a"),
10985 json!({
10986 "main.rs": buffer_text,
10987 }),
10988 )
10989 .await;
10990
10991 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10992 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10993 language_registry.add(rust_lang());
10994 let mut fake_servers = language_registry.register_fake_lsp(
10995 "Rust",
10996 FakeLspAdapter {
10997 capabilities: lsp::ServerCapabilities {
10998 completion_provider: Some(lsp::CompletionOptions {
10999 resolve_provider: None,
11000 ..lsp::CompletionOptions::default()
11001 }),
11002 ..lsp::ServerCapabilities::default()
11003 },
11004 ..FakeLspAdapter::default()
11005 },
11006 );
11007 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11008 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11009 let buffer = project
11010 .update(cx, |project, cx| {
11011 project.open_local_buffer(path!("/a/main.rs"), cx)
11012 })
11013 .await
11014 .unwrap();
11015
11016 let multi_buffer = cx.new(|cx| {
11017 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11018 multi_buffer.push_excerpts(
11019 buffer.clone(),
11020 [ExcerptRange::new(0..first_excerpt_end)],
11021 cx,
11022 );
11023 multi_buffer.push_excerpts(
11024 buffer.clone(),
11025 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11026 cx,
11027 );
11028 multi_buffer
11029 });
11030
11031 let editor = workspace
11032 .update(cx, |_, window, cx| {
11033 cx.new(|cx| {
11034 Editor::new(
11035 EditorMode::Full {
11036 scale_ui_elements_with_buffer_font_size: false,
11037 show_active_line_background: false,
11038 sized_by_content: false,
11039 },
11040 multi_buffer.clone(),
11041 Some(project.clone()),
11042 window,
11043 cx,
11044 )
11045 })
11046 })
11047 .unwrap();
11048
11049 let pane = workspace
11050 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11051 .unwrap();
11052 pane.update_in(cx, |pane, window, cx| {
11053 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11054 });
11055
11056 let fake_server = fake_servers.next().await.unwrap();
11057
11058 editor.update_in(cx, |editor, window, cx| {
11059 editor.change_selections(None, window, cx, |s| {
11060 s.select_ranges([
11061 Point::new(1, 11)..Point::new(1, 11),
11062 Point::new(7, 11)..Point::new(7, 11),
11063 ])
11064 });
11065
11066 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11067 });
11068
11069 editor.update_in(cx, |editor, window, cx| {
11070 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11071 });
11072
11073 fake_server
11074 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11075 let completion_item = lsp::CompletionItem {
11076 label: "saturating_sub()".into(),
11077 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11078 lsp::InsertReplaceEdit {
11079 new_text: "saturating_sub()".to_owned(),
11080 insert: lsp::Range::new(
11081 lsp::Position::new(7, 7),
11082 lsp::Position::new(7, 11),
11083 ),
11084 replace: lsp::Range::new(
11085 lsp::Position::new(7, 7),
11086 lsp::Position::new(7, 13),
11087 ),
11088 },
11089 )),
11090 ..lsp::CompletionItem::default()
11091 };
11092
11093 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11094 })
11095 .next()
11096 .await
11097 .unwrap();
11098
11099 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11100 .await;
11101
11102 editor
11103 .update_in(cx, |editor, window, cx| {
11104 editor
11105 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11106 .unwrap()
11107 })
11108 .await
11109 .unwrap();
11110
11111 editor.update(cx, |editor, cx| {
11112 assert_text_with_selections(editor, expected_multibuffer, cx);
11113 })
11114}
11115
11116#[gpui::test]
11117async fn test_completion(cx: &mut TestAppContext) {
11118 init_test(cx, |_| {});
11119
11120 let mut cx = EditorLspTestContext::new_rust(
11121 lsp::ServerCapabilities {
11122 completion_provider: Some(lsp::CompletionOptions {
11123 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11124 resolve_provider: Some(true),
11125 ..Default::default()
11126 }),
11127 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11128 ..Default::default()
11129 },
11130 cx,
11131 )
11132 .await;
11133 let counter = Arc::new(AtomicUsize::new(0));
11134
11135 cx.set_state(indoc! {"
11136 oneˇ
11137 two
11138 three
11139 "});
11140 cx.simulate_keystroke(".");
11141 handle_completion_request(
11142 &mut cx,
11143 indoc! {"
11144 one.|<>
11145 two
11146 three
11147 "},
11148 vec!["first_completion", "second_completion"],
11149 counter.clone(),
11150 )
11151 .await;
11152 cx.condition(|editor, _| editor.context_menu_visible())
11153 .await;
11154 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11155
11156 let _handler = handle_signature_help_request(
11157 &mut cx,
11158 lsp::SignatureHelp {
11159 signatures: vec![lsp::SignatureInformation {
11160 label: "test signature".to_string(),
11161 documentation: None,
11162 parameters: Some(vec![lsp::ParameterInformation {
11163 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11164 documentation: None,
11165 }]),
11166 active_parameter: None,
11167 }],
11168 active_signature: None,
11169 active_parameter: None,
11170 },
11171 );
11172 cx.update_editor(|editor, window, cx| {
11173 assert!(
11174 !editor.signature_help_state.is_shown(),
11175 "No signature help was called for"
11176 );
11177 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11178 });
11179 cx.run_until_parked();
11180 cx.update_editor(|editor, _, _| {
11181 assert!(
11182 !editor.signature_help_state.is_shown(),
11183 "No signature help should be shown when completions menu is open"
11184 );
11185 });
11186
11187 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11188 editor.context_menu_next(&Default::default(), window, cx);
11189 editor
11190 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11191 .unwrap()
11192 });
11193 cx.assert_editor_state(indoc! {"
11194 one.second_completionˇ
11195 two
11196 three
11197 "});
11198
11199 handle_resolve_completion_request(
11200 &mut cx,
11201 Some(vec![
11202 (
11203 //This overlaps with the primary completion edit which is
11204 //misbehavior from the LSP spec, test that we filter it out
11205 indoc! {"
11206 one.second_ˇcompletion
11207 two
11208 threeˇ
11209 "},
11210 "overlapping additional edit",
11211 ),
11212 (
11213 indoc! {"
11214 one.second_completion
11215 two
11216 threeˇ
11217 "},
11218 "\nadditional edit",
11219 ),
11220 ]),
11221 )
11222 .await;
11223 apply_additional_edits.await.unwrap();
11224 cx.assert_editor_state(indoc! {"
11225 one.second_completionˇ
11226 two
11227 three
11228 additional edit
11229 "});
11230
11231 cx.set_state(indoc! {"
11232 one.second_completion
11233 twoˇ
11234 threeˇ
11235 additional edit
11236 "});
11237 cx.simulate_keystroke(" ");
11238 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11239 cx.simulate_keystroke("s");
11240 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11241
11242 cx.assert_editor_state(indoc! {"
11243 one.second_completion
11244 two sˇ
11245 three sˇ
11246 additional edit
11247 "});
11248 handle_completion_request(
11249 &mut cx,
11250 indoc! {"
11251 one.second_completion
11252 two s
11253 three <s|>
11254 additional edit
11255 "},
11256 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11257 counter.clone(),
11258 )
11259 .await;
11260 cx.condition(|editor, _| editor.context_menu_visible())
11261 .await;
11262 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11263
11264 cx.simulate_keystroke("i");
11265
11266 handle_completion_request(
11267 &mut cx,
11268 indoc! {"
11269 one.second_completion
11270 two si
11271 three <si|>
11272 additional edit
11273 "},
11274 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11275 counter.clone(),
11276 )
11277 .await;
11278 cx.condition(|editor, _| editor.context_menu_visible())
11279 .await;
11280 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11281
11282 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11283 editor
11284 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11285 .unwrap()
11286 });
11287 cx.assert_editor_state(indoc! {"
11288 one.second_completion
11289 two sixth_completionˇ
11290 three sixth_completionˇ
11291 additional edit
11292 "});
11293
11294 apply_additional_edits.await.unwrap();
11295
11296 update_test_language_settings(&mut cx, |settings| {
11297 settings.defaults.show_completions_on_input = Some(false);
11298 });
11299 cx.set_state("editorˇ");
11300 cx.simulate_keystroke(".");
11301 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11302 cx.simulate_keystrokes("c l o");
11303 cx.assert_editor_state("editor.cloˇ");
11304 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11305 cx.update_editor(|editor, window, cx| {
11306 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11307 });
11308 handle_completion_request(
11309 &mut cx,
11310 "editor.<clo|>",
11311 vec!["close", "clobber"],
11312 counter.clone(),
11313 )
11314 .await;
11315 cx.condition(|editor, _| editor.context_menu_visible())
11316 .await;
11317 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11318
11319 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11320 editor
11321 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11322 .unwrap()
11323 });
11324 cx.assert_editor_state("editor.closeˇ");
11325 handle_resolve_completion_request(&mut cx, None).await;
11326 apply_additional_edits.await.unwrap();
11327}
11328
11329#[gpui::test]
11330async fn test_word_completion(cx: &mut TestAppContext) {
11331 let lsp_fetch_timeout_ms = 10;
11332 init_test(cx, |language_settings| {
11333 language_settings.defaults.completions = Some(CompletionSettings {
11334 words: WordsCompletionMode::Fallback,
11335 lsp: true,
11336 lsp_fetch_timeout_ms: 10,
11337 lsp_insert_mode: LspInsertMode::Insert,
11338 });
11339 });
11340
11341 let mut cx = EditorLspTestContext::new_rust(
11342 lsp::ServerCapabilities {
11343 completion_provider: Some(lsp::CompletionOptions {
11344 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11345 ..lsp::CompletionOptions::default()
11346 }),
11347 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11348 ..lsp::ServerCapabilities::default()
11349 },
11350 cx,
11351 )
11352 .await;
11353
11354 let throttle_completions = Arc::new(AtomicBool::new(false));
11355
11356 let lsp_throttle_completions = throttle_completions.clone();
11357 let _completion_requests_handler =
11358 cx.lsp
11359 .server
11360 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11361 let lsp_throttle_completions = lsp_throttle_completions.clone();
11362 let cx = cx.clone();
11363 async move {
11364 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11365 cx.background_executor()
11366 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11367 .await;
11368 }
11369 Ok(Some(lsp::CompletionResponse::Array(vec![
11370 lsp::CompletionItem {
11371 label: "first".into(),
11372 ..lsp::CompletionItem::default()
11373 },
11374 lsp::CompletionItem {
11375 label: "last".into(),
11376 ..lsp::CompletionItem::default()
11377 },
11378 ])))
11379 }
11380 });
11381
11382 cx.set_state(indoc! {"
11383 oneˇ
11384 two
11385 three
11386 "});
11387 cx.simulate_keystroke(".");
11388 cx.executor().run_until_parked();
11389 cx.condition(|editor, _| editor.context_menu_visible())
11390 .await;
11391 cx.update_editor(|editor, window, cx| {
11392 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11393 {
11394 assert_eq!(
11395 completion_menu_entries(&menu),
11396 &["first", "last"],
11397 "When LSP server is fast to reply, no fallback word completions are used"
11398 );
11399 } else {
11400 panic!("expected completion menu to be open");
11401 }
11402 editor.cancel(&Cancel, window, cx);
11403 });
11404 cx.executor().run_until_parked();
11405 cx.condition(|editor, _| !editor.context_menu_visible())
11406 .await;
11407
11408 throttle_completions.store(true, atomic::Ordering::Release);
11409 cx.simulate_keystroke(".");
11410 cx.executor()
11411 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11412 cx.executor().run_until_parked();
11413 cx.condition(|editor, _| editor.context_menu_visible())
11414 .await;
11415 cx.update_editor(|editor, _, _| {
11416 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11417 {
11418 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11419 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11420 } else {
11421 panic!("expected completion menu to be open");
11422 }
11423 });
11424}
11425
11426#[gpui::test]
11427async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11428 init_test(cx, |language_settings| {
11429 language_settings.defaults.completions = Some(CompletionSettings {
11430 words: WordsCompletionMode::Enabled,
11431 lsp: true,
11432 lsp_fetch_timeout_ms: 0,
11433 lsp_insert_mode: LspInsertMode::Insert,
11434 });
11435 });
11436
11437 let mut cx = EditorLspTestContext::new_rust(
11438 lsp::ServerCapabilities {
11439 completion_provider: Some(lsp::CompletionOptions {
11440 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11441 ..lsp::CompletionOptions::default()
11442 }),
11443 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11444 ..lsp::ServerCapabilities::default()
11445 },
11446 cx,
11447 )
11448 .await;
11449
11450 let _completion_requests_handler =
11451 cx.lsp
11452 .server
11453 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11454 Ok(Some(lsp::CompletionResponse::Array(vec![
11455 lsp::CompletionItem {
11456 label: "first".into(),
11457 ..lsp::CompletionItem::default()
11458 },
11459 lsp::CompletionItem {
11460 label: "last".into(),
11461 ..lsp::CompletionItem::default()
11462 },
11463 ])))
11464 });
11465
11466 cx.set_state(indoc! {"ˇ
11467 first
11468 last
11469 second
11470 "});
11471 cx.simulate_keystroke(".");
11472 cx.executor().run_until_parked();
11473 cx.condition(|editor, _| editor.context_menu_visible())
11474 .await;
11475 cx.update_editor(|editor, _, _| {
11476 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11477 {
11478 assert_eq!(
11479 completion_menu_entries(&menu),
11480 &["first", "last", "second"],
11481 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11482 );
11483 } else {
11484 panic!("expected completion menu to be open");
11485 }
11486 });
11487}
11488
11489#[gpui::test]
11490async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11491 init_test(cx, |language_settings| {
11492 language_settings.defaults.completions = Some(CompletionSettings {
11493 words: WordsCompletionMode::Disabled,
11494 lsp: true,
11495 lsp_fetch_timeout_ms: 0,
11496 lsp_insert_mode: LspInsertMode::Insert,
11497 });
11498 });
11499
11500 let mut cx = EditorLspTestContext::new_rust(
11501 lsp::ServerCapabilities {
11502 completion_provider: Some(lsp::CompletionOptions {
11503 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11504 ..lsp::CompletionOptions::default()
11505 }),
11506 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11507 ..lsp::ServerCapabilities::default()
11508 },
11509 cx,
11510 )
11511 .await;
11512
11513 let _completion_requests_handler =
11514 cx.lsp
11515 .server
11516 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11517 panic!("LSP completions should not be queried when dealing with word completions")
11518 });
11519
11520 cx.set_state(indoc! {"ˇ
11521 first
11522 last
11523 second
11524 "});
11525 cx.update_editor(|editor, window, cx| {
11526 editor.show_word_completions(&ShowWordCompletions, window, cx);
11527 });
11528 cx.executor().run_until_parked();
11529 cx.condition(|editor, _| editor.context_menu_visible())
11530 .await;
11531 cx.update_editor(|editor, _, _| {
11532 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11533 {
11534 assert_eq!(
11535 completion_menu_entries(&menu),
11536 &["first", "last", "second"],
11537 "`ShowWordCompletions` action should show word completions"
11538 );
11539 } else {
11540 panic!("expected completion menu to be open");
11541 }
11542 });
11543
11544 cx.simulate_keystroke("l");
11545 cx.executor().run_until_parked();
11546 cx.condition(|editor, _| editor.context_menu_visible())
11547 .await;
11548 cx.update_editor(|editor, _, _| {
11549 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11550 {
11551 assert_eq!(
11552 completion_menu_entries(&menu),
11553 &["last"],
11554 "After showing word completions, further editing should filter them and not query the LSP"
11555 );
11556 } else {
11557 panic!("expected completion menu to be open");
11558 }
11559 });
11560}
11561
11562#[gpui::test]
11563async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11564 init_test(cx, |language_settings| {
11565 language_settings.defaults.completions = Some(CompletionSettings {
11566 words: WordsCompletionMode::Fallback,
11567 lsp: false,
11568 lsp_fetch_timeout_ms: 0,
11569 lsp_insert_mode: LspInsertMode::Insert,
11570 });
11571 });
11572
11573 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11574
11575 cx.set_state(indoc! {"ˇ
11576 0_usize
11577 let
11578 33
11579 4.5f32
11580 "});
11581 cx.update_editor(|editor, window, cx| {
11582 editor.show_completions(&ShowCompletions::default(), window, cx);
11583 });
11584 cx.executor().run_until_parked();
11585 cx.condition(|editor, _| editor.context_menu_visible())
11586 .await;
11587 cx.update_editor(|editor, window, cx| {
11588 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11589 {
11590 assert_eq!(
11591 completion_menu_entries(&menu),
11592 &["let"],
11593 "With no digits in the completion query, no digits should be in the word completions"
11594 );
11595 } else {
11596 panic!("expected completion menu to be open");
11597 }
11598 editor.cancel(&Cancel, window, cx);
11599 });
11600
11601 cx.set_state(indoc! {"3ˇ
11602 0_usize
11603 let
11604 3
11605 33.35f32
11606 "});
11607 cx.update_editor(|editor, window, cx| {
11608 editor.show_completions(&ShowCompletions::default(), window, cx);
11609 });
11610 cx.executor().run_until_parked();
11611 cx.condition(|editor, _| editor.context_menu_visible())
11612 .await;
11613 cx.update_editor(|editor, _, _| {
11614 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11615 {
11616 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11617 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11618 } else {
11619 panic!("expected completion menu to be open");
11620 }
11621 });
11622}
11623
11624fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11625 let position = || lsp::Position {
11626 line: params.text_document_position.position.line,
11627 character: params.text_document_position.position.character,
11628 };
11629 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11630 range: lsp::Range {
11631 start: position(),
11632 end: position(),
11633 },
11634 new_text: text.to_string(),
11635 }))
11636}
11637
11638#[gpui::test]
11639async fn test_multiline_completion(cx: &mut TestAppContext) {
11640 init_test(cx, |_| {});
11641
11642 let fs = FakeFs::new(cx.executor());
11643 fs.insert_tree(
11644 path!("/a"),
11645 json!({
11646 "main.ts": "a",
11647 }),
11648 )
11649 .await;
11650
11651 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11652 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11653 let typescript_language = Arc::new(Language::new(
11654 LanguageConfig {
11655 name: "TypeScript".into(),
11656 matcher: LanguageMatcher {
11657 path_suffixes: vec!["ts".to_string()],
11658 ..LanguageMatcher::default()
11659 },
11660 line_comments: vec!["// ".into()],
11661 ..LanguageConfig::default()
11662 },
11663 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11664 ));
11665 language_registry.add(typescript_language.clone());
11666 let mut fake_servers = language_registry.register_fake_lsp(
11667 "TypeScript",
11668 FakeLspAdapter {
11669 capabilities: lsp::ServerCapabilities {
11670 completion_provider: Some(lsp::CompletionOptions {
11671 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11672 ..lsp::CompletionOptions::default()
11673 }),
11674 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11675 ..lsp::ServerCapabilities::default()
11676 },
11677 // Emulate vtsls label generation
11678 label_for_completion: Some(Box::new(|item, _| {
11679 let text = if let Some(description) = item
11680 .label_details
11681 .as_ref()
11682 .and_then(|label_details| label_details.description.as_ref())
11683 {
11684 format!("{} {}", item.label, description)
11685 } else if let Some(detail) = &item.detail {
11686 format!("{} {}", item.label, detail)
11687 } else {
11688 item.label.clone()
11689 };
11690 let len = text.len();
11691 Some(language::CodeLabel {
11692 text,
11693 runs: Vec::new(),
11694 filter_range: 0..len,
11695 })
11696 })),
11697 ..FakeLspAdapter::default()
11698 },
11699 );
11700 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11701 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11702 let worktree_id = workspace
11703 .update(cx, |workspace, _window, cx| {
11704 workspace.project().update(cx, |project, cx| {
11705 project.worktrees(cx).next().unwrap().read(cx).id()
11706 })
11707 })
11708 .unwrap();
11709 let _buffer = project
11710 .update(cx, |project, cx| {
11711 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11712 })
11713 .await
11714 .unwrap();
11715 let editor = workspace
11716 .update(cx, |workspace, window, cx| {
11717 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11718 })
11719 .unwrap()
11720 .await
11721 .unwrap()
11722 .downcast::<Editor>()
11723 .unwrap();
11724 let fake_server = fake_servers.next().await.unwrap();
11725
11726 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11727 let multiline_label_2 = "a\nb\nc\n";
11728 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11729 let multiline_description = "d\ne\nf\n";
11730 let multiline_detail_2 = "g\nh\ni\n";
11731
11732 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11733 move |params, _| async move {
11734 Ok(Some(lsp::CompletionResponse::Array(vec![
11735 lsp::CompletionItem {
11736 label: multiline_label.to_string(),
11737 text_edit: gen_text_edit(¶ms, "new_text_1"),
11738 ..lsp::CompletionItem::default()
11739 },
11740 lsp::CompletionItem {
11741 label: "single line label 1".to_string(),
11742 detail: Some(multiline_detail.to_string()),
11743 text_edit: gen_text_edit(¶ms, "new_text_2"),
11744 ..lsp::CompletionItem::default()
11745 },
11746 lsp::CompletionItem {
11747 label: "single line label 2".to_string(),
11748 label_details: Some(lsp::CompletionItemLabelDetails {
11749 description: Some(multiline_description.to_string()),
11750 detail: None,
11751 }),
11752 text_edit: gen_text_edit(¶ms, "new_text_2"),
11753 ..lsp::CompletionItem::default()
11754 },
11755 lsp::CompletionItem {
11756 label: multiline_label_2.to_string(),
11757 detail: Some(multiline_detail_2.to_string()),
11758 text_edit: gen_text_edit(¶ms, "new_text_3"),
11759 ..lsp::CompletionItem::default()
11760 },
11761 lsp::CompletionItem {
11762 label: "Label with many spaces and \t but without newlines".to_string(),
11763 detail: Some(
11764 "Details with many spaces and \t but without newlines".to_string(),
11765 ),
11766 text_edit: gen_text_edit(¶ms, "new_text_4"),
11767 ..lsp::CompletionItem::default()
11768 },
11769 ])))
11770 },
11771 );
11772
11773 editor.update_in(cx, |editor, window, cx| {
11774 cx.focus_self(window);
11775 editor.move_to_end(&MoveToEnd, window, cx);
11776 editor.handle_input(".", window, cx);
11777 });
11778 cx.run_until_parked();
11779 completion_handle.next().await.unwrap();
11780
11781 editor.update(cx, |editor, _| {
11782 assert!(editor.context_menu_visible());
11783 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11784 {
11785 let completion_labels = menu
11786 .completions
11787 .borrow()
11788 .iter()
11789 .map(|c| c.label.text.clone())
11790 .collect::<Vec<_>>();
11791 assert_eq!(
11792 completion_labels,
11793 &[
11794 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11795 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11796 "single line label 2 d e f ",
11797 "a b c g h i ",
11798 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11799 ],
11800 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11801 );
11802
11803 for completion in menu
11804 .completions
11805 .borrow()
11806 .iter() {
11807 assert_eq!(
11808 completion.label.filter_range,
11809 0..completion.label.text.len(),
11810 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11811 );
11812 }
11813 } else {
11814 panic!("expected completion menu to be open");
11815 }
11816 });
11817}
11818
11819#[gpui::test]
11820async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11821 init_test(cx, |_| {});
11822 let mut cx = EditorLspTestContext::new_rust(
11823 lsp::ServerCapabilities {
11824 completion_provider: Some(lsp::CompletionOptions {
11825 trigger_characters: Some(vec![".".to_string()]),
11826 ..Default::default()
11827 }),
11828 ..Default::default()
11829 },
11830 cx,
11831 )
11832 .await;
11833 cx.lsp
11834 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11835 Ok(Some(lsp::CompletionResponse::Array(vec![
11836 lsp::CompletionItem {
11837 label: "first".into(),
11838 ..Default::default()
11839 },
11840 lsp::CompletionItem {
11841 label: "last".into(),
11842 ..Default::default()
11843 },
11844 ])))
11845 });
11846 cx.set_state("variableˇ");
11847 cx.simulate_keystroke(".");
11848 cx.executor().run_until_parked();
11849
11850 cx.update_editor(|editor, _, _| {
11851 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11852 {
11853 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11854 } else {
11855 panic!("expected completion menu to be open");
11856 }
11857 });
11858
11859 cx.update_editor(|editor, window, cx| {
11860 editor.move_page_down(&MovePageDown::default(), window, cx);
11861 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11862 {
11863 assert!(
11864 menu.selected_item == 1,
11865 "expected PageDown to select the last item from the context menu"
11866 );
11867 } else {
11868 panic!("expected completion menu to stay open after PageDown");
11869 }
11870 });
11871
11872 cx.update_editor(|editor, window, cx| {
11873 editor.move_page_up(&MovePageUp::default(), window, cx);
11874 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11875 {
11876 assert!(
11877 menu.selected_item == 0,
11878 "expected PageUp to select the first item from the context menu"
11879 );
11880 } else {
11881 panic!("expected completion menu to stay open after PageUp");
11882 }
11883 });
11884}
11885
11886#[gpui::test]
11887async fn test_as_is_completions(cx: &mut TestAppContext) {
11888 init_test(cx, |_| {});
11889 let mut cx = EditorLspTestContext::new_rust(
11890 lsp::ServerCapabilities {
11891 completion_provider: Some(lsp::CompletionOptions {
11892 ..Default::default()
11893 }),
11894 ..Default::default()
11895 },
11896 cx,
11897 )
11898 .await;
11899 cx.lsp
11900 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11901 Ok(Some(lsp::CompletionResponse::Array(vec![
11902 lsp::CompletionItem {
11903 label: "unsafe".into(),
11904 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11905 range: lsp::Range {
11906 start: lsp::Position {
11907 line: 1,
11908 character: 2,
11909 },
11910 end: lsp::Position {
11911 line: 1,
11912 character: 3,
11913 },
11914 },
11915 new_text: "unsafe".to_string(),
11916 })),
11917 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11918 ..Default::default()
11919 },
11920 ])))
11921 });
11922 cx.set_state("fn a() {}\n nˇ");
11923 cx.executor().run_until_parked();
11924 cx.update_editor(|editor, window, cx| {
11925 editor.show_completions(
11926 &ShowCompletions {
11927 trigger: Some("\n".into()),
11928 },
11929 window,
11930 cx,
11931 );
11932 });
11933 cx.executor().run_until_parked();
11934
11935 cx.update_editor(|editor, window, cx| {
11936 editor.confirm_completion(&Default::default(), window, cx)
11937 });
11938 cx.executor().run_until_parked();
11939 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11940}
11941
11942#[gpui::test]
11943async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11944 init_test(cx, |_| {});
11945
11946 let mut cx = EditorLspTestContext::new_rust(
11947 lsp::ServerCapabilities {
11948 completion_provider: Some(lsp::CompletionOptions {
11949 trigger_characters: Some(vec![".".to_string()]),
11950 resolve_provider: Some(true),
11951 ..Default::default()
11952 }),
11953 ..Default::default()
11954 },
11955 cx,
11956 )
11957 .await;
11958
11959 cx.set_state("fn main() { let a = 2ˇ; }");
11960 cx.simulate_keystroke(".");
11961 let completion_item = lsp::CompletionItem {
11962 label: "Some".into(),
11963 kind: Some(lsp::CompletionItemKind::SNIPPET),
11964 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11965 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11966 kind: lsp::MarkupKind::Markdown,
11967 value: "```rust\nSome(2)\n```".to_string(),
11968 })),
11969 deprecated: Some(false),
11970 sort_text: Some("Some".to_string()),
11971 filter_text: Some("Some".to_string()),
11972 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11973 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11974 range: lsp::Range {
11975 start: lsp::Position {
11976 line: 0,
11977 character: 22,
11978 },
11979 end: lsp::Position {
11980 line: 0,
11981 character: 22,
11982 },
11983 },
11984 new_text: "Some(2)".to_string(),
11985 })),
11986 additional_text_edits: Some(vec![lsp::TextEdit {
11987 range: lsp::Range {
11988 start: lsp::Position {
11989 line: 0,
11990 character: 20,
11991 },
11992 end: lsp::Position {
11993 line: 0,
11994 character: 22,
11995 },
11996 },
11997 new_text: "".to_string(),
11998 }]),
11999 ..Default::default()
12000 };
12001
12002 let closure_completion_item = completion_item.clone();
12003 let counter = Arc::new(AtomicUsize::new(0));
12004 let counter_clone = counter.clone();
12005 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12006 let task_completion_item = closure_completion_item.clone();
12007 counter_clone.fetch_add(1, atomic::Ordering::Release);
12008 async move {
12009 Ok(Some(lsp::CompletionResponse::Array(vec![
12010 task_completion_item,
12011 ])))
12012 }
12013 });
12014
12015 cx.condition(|editor, _| editor.context_menu_visible())
12016 .await;
12017 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12018 assert!(request.next().await.is_some());
12019 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12020
12021 cx.simulate_keystrokes("S o m");
12022 cx.condition(|editor, _| editor.context_menu_visible())
12023 .await;
12024 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12025 assert!(request.next().await.is_some());
12026 assert!(request.next().await.is_some());
12027 assert!(request.next().await.is_some());
12028 request.close();
12029 assert!(request.next().await.is_none());
12030 assert_eq!(
12031 counter.load(atomic::Ordering::Acquire),
12032 4,
12033 "With the completions menu open, only one LSP request should happen per input"
12034 );
12035}
12036
12037#[gpui::test]
12038async fn test_toggle_comment(cx: &mut TestAppContext) {
12039 init_test(cx, |_| {});
12040 let mut cx = EditorTestContext::new(cx).await;
12041 let language = Arc::new(Language::new(
12042 LanguageConfig {
12043 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12044 ..Default::default()
12045 },
12046 Some(tree_sitter_rust::LANGUAGE.into()),
12047 ));
12048 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12049
12050 // If multiple selections intersect a line, the line is only toggled once.
12051 cx.set_state(indoc! {"
12052 fn a() {
12053 «//b();
12054 ˇ»// «c();
12055 //ˇ» d();
12056 }
12057 "});
12058
12059 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12060
12061 cx.assert_editor_state(indoc! {"
12062 fn a() {
12063 «b();
12064 c();
12065 ˇ» d();
12066 }
12067 "});
12068
12069 // The comment prefix is inserted at the same column for every line in a
12070 // selection.
12071 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12072
12073 cx.assert_editor_state(indoc! {"
12074 fn a() {
12075 // «b();
12076 // c();
12077 ˇ»// d();
12078 }
12079 "});
12080
12081 // If a selection ends at the beginning of a line, that line is not toggled.
12082 cx.set_selections_state(indoc! {"
12083 fn a() {
12084 // b();
12085 «// c();
12086 ˇ» // d();
12087 }
12088 "});
12089
12090 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12091
12092 cx.assert_editor_state(indoc! {"
12093 fn a() {
12094 // b();
12095 «c();
12096 ˇ» // d();
12097 }
12098 "});
12099
12100 // If a selection span a single line and is empty, the line is toggled.
12101 cx.set_state(indoc! {"
12102 fn a() {
12103 a();
12104 b();
12105 ˇ
12106 }
12107 "});
12108
12109 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12110
12111 cx.assert_editor_state(indoc! {"
12112 fn a() {
12113 a();
12114 b();
12115 //•ˇ
12116 }
12117 "});
12118
12119 // If a selection span multiple lines, empty lines are not toggled.
12120 cx.set_state(indoc! {"
12121 fn a() {
12122 «a();
12123
12124 c();ˇ»
12125 }
12126 "});
12127
12128 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12129
12130 cx.assert_editor_state(indoc! {"
12131 fn a() {
12132 // «a();
12133
12134 // c();ˇ»
12135 }
12136 "});
12137
12138 // If a selection includes multiple comment prefixes, all lines are uncommented.
12139 cx.set_state(indoc! {"
12140 fn a() {
12141 «// a();
12142 /// b();
12143 //! c();ˇ»
12144 }
12145 "});
12146
12147 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12148
12149 cx.assert_editor_state(indoc! {"
12150 fn a() {
12151 «a();
12152 b();
12153 c();ˇ»
12154 }
12155 "});
12156}
12157
12158#[gpui::test]
12159async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12160 init_test(cx, |_| {});
12161 let mut cx = EditorTestContext::new(cx).await;
12162 let language = Arc::new(Language::new(
12163 LanguageConfig {
12164 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12165 ..Default::default()
12166 },
12167 Some(tree_sitter_rust::LANGUAGE.into()),
12168 ));
12169 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12170
12171 let toggle_comments = &ToggleComments {
12172 advance_downwards: false,
12173 ignore_indent: true,
12174 };
12175
12176 // If multiple selections intersect a line, the line is only toggled once.
12177 cx.set_state(indoc! {"
12178 fn a() {
12179 // «b();
12180 // c();
12181 // ˇ» d();
12182 }
12183 "});
12184
12185 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12186
12187 cx.assert_editor_state(indoc! {"
12188 fn a() {
12189 «b();
12190 c();
12191 ˇ» d();
12192 }
12193 "});
12194
12195 // The comment prefix is inserted at the beginning of each line
12196 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12197
12198 cx.assert_editor_state(indoc! {"
12199 fn a() {
12200 // «b();
12201 // c();
12202 // ˇ» d();
12203 }
12204 "});
12205
12206 // If a selection ends at the beginning of a line, that line is not toggled.
12207 cx.set_selections_state(indoc! {"
12208 fn a() {
12209 // b();
12210 // «c();
12211 ˇ»// d();
12212 }
12213 "});
12214
12215 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12216
12217 cx.assert_editor_state(indoc! {"
12218 fn a() {
12219 // b();
12220 «c();
12221 ˇ»// d();
12222 }
12223 "});
12224
12225 // If a selection span a single line and is empty, the line is toggled.
12226 cx.set_state(indoc! {"
12227 fn a() {
12228 a();
12229 b();
12230 ˇ
12231 }
12232 "});
12233
12234 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12235
12236 cx.assert_editor_state(indoc! {"
12237 fn a() {
12238 a();
12239 b();
12240 //ˇ
12241 }
12242 "});
12243
12244 // If a selection span multiple lines, empty lines are not toggled.
12245 cx.set_state(indoc! {"
12246 fn a() {
12247 «a();
12248
12249 c();ˇ»
12250 }
12251 "});
12252
12253 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12254
12255 cx.assert_editor_state(indoc! {"
12256 fn a() {
12257 // «a();
12258
12259 // c();ˇ»
12260 }
12261 "});
12262
12263 // If a selection includes multiple comment prefixes, all lines are uncommented.
12264 cx.set_state(indoc! {"
12265 fn a() {
12266 // «a();
12267 /// b();
12268 //! c();ˇ»
12269 }
12270 "});
12271
12272 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12273
12274 cx.assert_editor_state(indoc! {"
12275 fn a() {
12276 «a();
12277 b();
12278 c();ˇ»
12279 }
12280 "});
12281}
12282
12283#[gpui::test]
12284async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12285 init_test(cx, |_| {});
12286
12287 let language = Arc::new(Language::new(
12288 LanguageConfig {
12289 line_comments: vec!["// ".into()],
12290 ..Default::default()
12291 },
12292 Some(tree_sitter_rust::LANGUAGE.into()),
12293 ));
12294
12295 let mut cx = EditorTestContext::new(cx).await;
12296
12297 cx.language_registry().add(language.clone());
12298 cx.update_buffer(|buffer, cx| {
12299 buffer.set_language(Some(language), cx);
12300 });
12301
12302 let toggle_comments = &ToggleComments {
12303 advance_downwards: true,
12304 ignore_indent: false,
12305 };
12306
12307 // Single cursor on one line -> advance
12308 // Cursor moves horizontally 3 characters as well on non-blank line
12309 cx.set_state(indoc!(
12310 "fn a() {
12311 ˇdog();
12312 cat();
12313 }"
12314 ));
12315 cx.update_editor(|editor, window, cx| {
12316 editor.toggle_comments(toggle_comments, window, cx);
12317 });
12318 cx.assert_editor_state(indoc!(
12319 "fn a() {
12320 // dog();
12321 catˇ();
12322 }"
12323 ));
12324
12325 // Single selection on one line -> don't advance
12326 cx.set_state(indoc!(
12327 "fn a() {
12328 «dog()ˇ»;
12329 cat();
12330 }"
12331 ));
12332 cx.update_editor(|editor, window, cx| {
12333 editor.toggle_comments(toggle_comments, window, cx);
12334 });
12335 cx.assert_editor_state(indoc!(
12336 "fn a() {
12337 // «dog()ˇ»;
12338 cat();
12339 }"
12340 ));
12341
12342 // Multiple cursors on one line -> advance
12343 cx.set_state(indoc!(
12344 "fn a() {
12345 ˇdˇog();
12346 cat();
12347 }"
12348 ));
12349 cx.update_editor(|editor, window, cx| {
12350 editor.toggle_comments(toggle_comments, window, cx);
12351 });
12352 cx.assert_editor_state(indoc!(
12353 "fn a() {
12354 // dog();
12355 catˇ(ˇ);
12356 }"
12357 ));
12358
12359 // Multiple cursors on one line, with selection -> don't advance
12360 cx.set_state(indoc!(
12361 "fn a() {
12362 ˇdˇog«()ˇ»;
12363 cat();
12364 }"
12365 ));
12366 cx.update_editor(|editor, window, cx| {
12367 editor.toggle_comments(toggle_comments, window, cx);
12368 });
12369 cx.assert_editor_state(indoc!(
12370 "fn a() {
12371 // ˇdˇog«()ˇ»;
12372 cat();
12373 }"
12374 ));
12375
12376 // Single cursor on one line -> advance
12377 // Cursor moves to column 0 on blank line
12378 cx.set_state(indoc!(
12379 "fn a() {
12380 ˇdog();
12381
12382 cat();
12383 }"
12384 ));
12385 cx.update_editor(|editor, window, cx| {
12386 editor.toggle_comments(toggle_comments, window, cx);
12387 });
12388 cx.assert_editor_state(indoc!(
12389 "fn a() {
12390 // dog();
12391 ˇ
12392 cat();
12393 }"
12394 ));
12395
12396 // Single cursor on one line -> advance
12397 // Cursor starts and ends at column 0
12398 cx.set_state(indoc!(
12399 "fn a() {
12400 ˇ dog();
12401 cat();
12402 }"
12403 ));
12404 cx.update_editor(|editor, window, cx| {
12405 editor.toggle_comments(toggle_comments, window, cx);
12406 });
12407 cx.assert_editor_state(indoc!(
12408 "fn a() {
12409 // dog();
12410 ˇ cat();
12411 }"
12412 ));
12413}
12414
12415#[gpui::test]
12416async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12417 init_test(cx, |_| {});
12418
12419 let mut cx = EditorTestContext::new(cx).await;
12420
12421 let html_language = Arc::new(
12422 Language::new(
12423 LanguageConfig {
12424 name: "HTML".into(),
12425 block_comment: Some(("<!-- ".into(), " -->".into())),
12426 ..Default::default()
12427 },
12428 Some(tree_sitter_html::LANGUAGE.into()),
12429 )
12430 .with_injection_query(
12431 r#"
12432 (script_element
12433 (raw_text) @injection.content
12434 (#set! injection.language "javascript"))
12435 "#,
12436 )
12437 .unwrap(),
12438 );
12439
12440 let javascript_language = Arc::new(Language::new(
12441 LanguageConfig {
12442 name: "JavaScript".into(),
12443 line_comments: vec!["// ".into()],
12444 ..Default::default()
12445 },
12446 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12447 ));
12448
12449 cx.language_registry().add(html_language.clone());
12450 cx.language_registry().add(javascript_language.clone());
12451 cx.update_buffer(|buffer, cx| {
12452 buffer.set_language(Some(html_language), cx);
12453 });
12454
12455 // Toggle comments for empty selections
12456 cx.set_state(
12457 &r#"
12458 <p>A</p>ˇ
12459 <p>B</p>ˇ
12460 <p>C</p>ˇ
12461 "#
12462 .unindent(),
12463 );
12464 cx.update_editor(|editor, window, cx| {
12465 editor.toggle_comments(&ToggleComments::default(), window, cx)
12466 });
12467 cx.assert_editor_state(
12468 &r#"
12469 <!-- <p>A</p>ˇ -->
12470 <!-- <p>B</p>ˇ -->
12471 <!-- <p>C</p>ˇ -->
12472 "#
12473 .unindent(),
12474 );
12475 cx.update_editor(|editor, window, cx| {
12476 editor.toggle_comments(&ToggleComments::default(), window, cx)
12477 });
12478 cx.assert_editor_state(
12479 &r#"
12480 <p>A</p>ˇ
12481 <p>B</p>ˇ
12482 <p>C</p>ˇ
12483 "#
12484 .unindent(),
12485 );
12486
12487 // Toggle comments for mixture of empty and non-empty selections, where
12488 // multiple selections occupy a given line.
12489 cx.set_state(
12490 &r#"
12491 <p>A«</p>
12492 <p>ˇ»B</p>ˇ
12493 <p>C«</p>
12494 <p>ˇ»D</p>ˇ
12495 "#
12496 .unindent(),
12497 );
12498
12499 cx.update_editor(|editor, window, cx| {
12500 editor.toggle_comments(&ToggleComments::default(), window, cx)
12501 });
12502 cx.assert_editor_state(
12503 &r#"
12504 <!-- <p>A«</p>
12505 <p>ˇ»B</p>ˇ -->
12506 <!-- <p>C«</p>
12507 <p>ˇ»D</p>ˇ -->
12508 "#
12509 .unindent(),
12510 );
12511 cx.update_editor(|editor, window, cx| {
12512 editor.toggle_comments(&ToggleComments::default(), window, cx)
12513 });
12514 cx.assert_editor_state(
12515 &r#"
12516 <p>A«</p>
12517 <p>ˇ»B</p>ˇ
12518 <p>C«</p>
12519 <p>ˇ»D</p>ˇ
12520 "#
12521 .unindent(),
12522 );
12523
12524 // Toggle comments when different languages are active for different
12525 // selections.
12526 cx.set_state(
12527 &r#"
12528 ˇ<script>
12529 ˇvar x = new Y();
12530 ˇ</script>
12531 "#
12532 .unindent(),
12533 );
12534 cx.executor().run_until_parked();
12535 cx.update_editor(|editor, window, cx| {
12536 editor.toggle_comments(&ToggleComments::default(), window, cx)
12537 });
12538 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12539 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12540 cx.assert_editor_state(
12541 &r#"
12542 <!-- ˇ<script> -->
12543 // ˇvar x = new Y();
12544 <!-- ˇ</script> -->
12545 "#
12546 .unindent(),
12547 );
12548}
12549
12550#[gpui::test]
12551fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12552 init_test(cx, |_| {});
12553
12554 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12555 let multibuffer = cx.new(|cx| {
12556 let mut multibuffer = MultiBuffer::new(ReadWrite);
12557 multibuffer.push_excerpts(
12558 buffer.clone(),
12559 [
12560 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12561 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12562 ],
12563 cx,
12564 );
12565 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12566 multibuffer
12567 });
12568
12569 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12570 editor.update_in(cx, |editor, window, cx| {
12571 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12572 editor.change_selections(None, window, cx, |s| {
12573 s.select_ranges([
12574 Point::new(0, 0)..Point::new(0, 0),
12575 Point::new(1, 0)..Point::new(1, 0),
12576 ])
12577 });
12578
12579 editor.handle_input("X", window, cx);
12580 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12581 assert_eq!(
12582 editor.selections.ranges(cx),
12583 [
12584 Point::new(0, 1)..Point::new(0, 1),
12585 Point::new(1, 1)..Point::new(1, 1),
12586 ]
12587 );
12588
12589 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12590 editor.change_selections(None, window, cx, |s| {
12591 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12592 });
12593 editor.backspace(&Default::default(), window, cx);
12594 assert_eq!(editor.text(cx), "Xa\nbbb");
12595 assert_eq!(
12596 editor.selections.ranges(cx),
12597 [Point::new(1, 0)..Point::new(1, 0)]
12598 );
12599
12600 editor.change_selections(None, window, cx, |s| {
12601 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12602 });
12603 editor.backspace(&Default::default(), window, cx);
12604 assert_eq!(editor.text(cx), "X\nbb");
12605 assert_eq!(
12606 editor.selections.ranges(cx),
12607 [Point::new(0, 1)..Point::new(0, 1)]
12608 );
12609 });
12610}
12611
12612#[gpui::test]
12613fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12614 init_test(cx, |_| {});
12615
12616 let markers = vec![('[', ']').into(), ('(', ')').into()];
12617 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12618 indoc! {"
12619 [aaaa
12620 (bbbb]
12621 cccc)",
12622 },
12623 markers.clone(),
12624 );
12625 let excerpt_ranges = markers.into_iter().map(|marker| {
12626 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12627 ExcerptRange::new(context.clone())
12628 });
12629 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12630 let multibuffer = cx.new(|cx| {
12631 let mut multibuffer = MultiBuffer::new(ReadWrite);
12632 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12633 multibuffer
12634 });
12635
12636 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12637 editor.update_in(cx, |editor, window, cx| {
12638 let (expected_text, selection_ranges) = marked_text_ranges(
12639 indoc! {"
12640 aaaa
12641 bˇbbb
12642 bˇbbˇb
12643 cccc"
12644 },
12645 true,
12646 );
12647 assert_eq!(editor.text(cx), expected_text);
12648 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12649
12650 editor.handle_input("X", window, cx);
12651
12652 let (expected_text, expected_selections) = marked_text_ranges(
12653 indoc! {"
12654 aaaa
12655 bXˇbbXb
12656 bXˇbbXˇb
12657 cccc"
12658 },
12659 false,
12660 );
12661 assert_eq!(editor.text(cx), expected_text);
12662 assert_eq!(editor.selections.ranges(cx), expected_selections);
12663
12664 editor.newline(&Newline, window, cx);
12665 let (expected_text, expected_selections) = marked_text_ranges(
12666 indoc! {"
12667 aaaa
12668 bX
12669 ˇbbX
12670 b
12671 bX
12672 ˇbbX
12673 ˇb
12674 cccc"
12675 },
12676 false,
12677 );
12678 assert_eq!(editor.text(cx), expected_text);
12679 assert_eq!(editor.selections.ranges(cx), expected_selections);
12680 });
12681}
12682
12683#[gpui::test]
12684fn test_refresh_selections(cx: &mut TestAppContext) {
12685 init_test(cx, |_| {});
12686
12687 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12688 let mut excerpt1_id = None;
12689 let multibuffer = cx.new(|cx| {
12690 let mut multibuffer = MultiBuffer::new(ReadWrite);
12691 excerpt1_id = multibuffer
12692 .push_excerpts(
12693 buffer.clone(),
12694 [
12695 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12696 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12697 ],
12698 cx,
12699 )
12700 .into_iter()
12701 .next();
12702 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12703 multibuffer
12704 });
12705
12706 let editor = cx.add_window(|window, cx| {
12707 let mut editor = build_editor(multibuffer.clone(), window, cx);
12708 let snapshot = editor.snapshot(window, cx);
12709 editor.change_selections(None, window, cx, |s| {
12710 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12711 });
12712 editor.begin_selection(
12713 Point::new(2, 1).to_display_point(&snapshot),
12714 true,
12715 1,
12716 window,
12717 cx,
12718 );
12719 assert_eq!(
12720 editor.selections.ranges(cx),
12721 [
12722 Point::new(1, 3)..Point::new(1, 3),
12723 Point::new(2, 1)..Point::new(2, 1),
12724 ]
12725 );
12726 editor
12727 });
12728
12729 // Refreshing selections is a no-op when excerpts haven't changed.
12730 _ = editor.update(cx, |editor, window, cx| {
12731 editor.change_selections(None, window, cx, |s| s.refresh());
12732 assert_eq!(
12733 editor.selections.ranges(cx),
12734 [
12735 Point::new(1, 3)..Point::new(1, 3),
12736 Point::new(2, 1)..Point::new(2, 1),
12737 ]
12738 );
12739 });
12740
12741 multibuffer.update(cx, |multibuffer, cx| {
12742 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12743 });
12744 _ = editor.update(cx, |editor, window, cx| {
12745 // Removing an excerpt causes the first selection to become degenerate.
12746 assert_eq!(
12747 editor.selections.ranges(cx),
12748 [
12749 Point::new(0, 0)..Point::new(0, 0),
12750 Point::new(0, 1)..Point::new(0, 1)
12751 ]
12752 );
12753
12754 // Refreshing selections will relocate the first selection to the original buffer
12755 // location.
12756 editor.change_selections(None, window, cx, |s| s.refresh());
12757 assert_eq!(
12758 editor.selections.ranges(cx),
12759 [
12760 Point::new(0, 1)..Point::new(0, 1),
12761 Point::new(0, 3)..Point::new(0, 3)
12762 ]
12763 );
12764 assert!(editor.selections.pending_anchor().is_some());
12765 });
12766}
12767
12768#[gpui::test]
12769fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12770 init_test(cx, |_| {});
12771
12772 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12773 let mut excerpt1_id = None;
12774 let multibuffer = cx.new(|cx| {
12775 let mut multibuffer = MultiBuffer::new(ReadWrite);
12776 excerpt1_id = multibuffer
12777 .push_excerpts(
12778 buffer.clone(),
12779 [
12780 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12781 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12782 ],
12783 cx,
12784 )
12785 .into_iter()
12786 .next();
12787 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12788 multibuffer
12789 });
12790
12791 let editor = cx.add_window(|window, cx| {
12792 let mut editor = build_editor(multibuffer.clone(), window, cx);
12793 let snapshot = editor.snapshot(window, cx);
12794 editor.begin_selection(
12795 Point::new(1, 3).to_display_point(&snapshot),
12796 false,
12797 1,
12798 window,
12799 cx,
12800 );
12801 assert_eq!(
12802 editor.selections.ranges(cx),
12803 [Point::new(1, 3)..Point::new(1, 3)]
12804 );
12805 editor
12806 });
12807
12808 multibuffer.update(cx, |multibuffer, cx| {
12809 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12810 });
12811 _ = editor.update(cx, |editor, window, cx| {
12812 assert_eq!(
12813 editor.selections.ranges(cx),
12814 [Point::new(0, 0)..Point::new(0, 0)]
12815 );
12816
12817 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12818 editor.change_selections(None, window, cx, |s| s.refresh());
12819 assert_eq!(
12820 editor.selections.ranges(cx),
12821 [Point::new(0, 3)..Point::new(0, 3)]
12822 );
12823 assert!(editor.selections.pending_anchor().is_some());
12824 });
12825}
12826
12827#[gpui::test]
12828async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12829 init_test(cx, |_| {});
12830
12831 let language = Arc::new(
12832 Language::new(
12833 LanguageConfig {
12834 brackets: BracketPairConfig {
12835 pairs: vec![
12836 BracketPair {
12837 start: "{".to_string(),
12838 end: "}".to_string(),
12839 close: true,
12840 surround: true,
12841 newline: true,
12842 },
12843 BracketPair {
12844 start: "/* ".to_string(),
12845 end: " */".to_string(),
12846 close: true,
12847 surround: true,
12848 newline: true,
12849 },
12850 ],
12851 ..Default::default()
12852 },
12853 ..Default::default()
12854 },
12855 Some(tree_sitter_rust::LANGUAGE.into()),
12856 )
12857 .with_indents_query("")
12858 .unwrap(),
12859 );
12860
12861 let text = concat!(
12862 "{ }\n", //
12863 " x\n", //
12864 " /* */\n", //
12865 "x\n", //
12866 "{{} }\n", //
12867 );
12868
12869 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12870 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12871 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12872 editor
12873 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12874 .await;
12875
12876 editor.update_in(cx, |editor, window, cx| {
12877 editor.change_selections(None, window, cx, |s| {
12878 s.select_display_ranges([
12879 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12880 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12881 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12882 ])
12883 });
12884 editor.newline(&Newline, window, cx);
12885
12886 assert_eq!(
12887 editor.buffer().read(cx).read(cx).text(),
12888 concat!(
12889 "{ \n", // Suppress rustfmt
12890 "\n", //
12891 "}\n", //
12892 " x\n", //
12893 " /* \n", //
12894 " \n", //
12895 " */\n", //
12896 "x\n", //
12897 "{{} \n", //
12898 "}\n", //
12899 )
12900 );
12901 });
12902}
12903
12904#[gpui::test]
12905fn test_highlighted_ranges(cx: &mut TestAppContext) {
12906 init_test(cx, |_| {});
12907
12908 let editor = cx.add_window(|window, cx| {
12909 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12910 build_editor(buffer.clone(), window, cx)
12911 });
12912
12913 _ = editor.update(cx, |editor, window, cx| {
12914 struct Type1;
12915 struct Type2;
12916
12917 let buffer = editor.buffer.read(cx).snapshot(cx);
12918
12919 let anchor_range =
12920 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12921
12922 editor.highlight_background::<Type1>(
12923 &[
12924 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12925 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12926 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12927 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12928 ],
12929 |_| Hsla::red(),
12930 cx,
12931 );
12932 editor.highlight_background::<Type2>(
12933 &[
12934 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12935 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12936 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12937 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12938 ],
12939 |_| Hsla::green(),
12940 cx,
12941 );
12942
12943 let snapshot = editor.snapshot(window, cx);
12944 let mut highlighted_ranges = editor.background_highlights_in_range(
12945 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12946 &snapshot,
12947 cx.theme().colors(),
12948 );
12949 // Enforce a consistent ordering based on color without relying on the ordering of the
12950 // highlight's `TypeId` which is non-executor.
12951 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12952 assert_eq!(
12953 highlighted_ranges,
12954 &[
12955 (
12956 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12957 Hsla::red(),
12958 ),
12959 (
12960 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12961 Hsla::red(),
12962 ),
12963 (
12964 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12965 Hsla::green(),
12966 ),
12967 (
12968 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12969 Hsla::green(),
12970 ),
12971 ]
12972 );
12973 assert_eq!(
12974 editor.background_highlights_in_range(
12975 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12976 &snapshot,
12977 cx.theme().colors(),
12978 ),
12979 &[(
12980 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12981 Hsla::red(),
12982 )]
12983 );
12984 });
12985}
12986
12987#[gpui::test]
12988async fn test_following(cx: &mut TestAppContext) {
12989 init_test(cx, |_| {});
12990
12991 let fs = FakeFs::new(cx.executor());
12992 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12993
12994 let buffer = project.update(cx, |project, cx| {
12995 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12996 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12997 });
12998 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12999 let follower = cx.update(|cx| {
13000 cx.open_window(
13001 WindowOptions {
13002 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13003 gpui::Point::new(px(0.), px(0.)),
13004 gpui::Point::new(px(10.), px(80.)),
13005 ))),
13006 ..Default::default()
13007 },
13008 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13009 )
13010 .unwrap()
13011 });
13012
13013 let is_still_following = Rc::new(RefCell::new(true));
13014 let follower_edit_event_count = Rc::new(RefCell::new(0));
13015 let pending_update = Rc::new(RefCell::new(None));
13016 let leader_entity = leader.root(cx).unwrap();
13017 let follower_entity = follower.root(cx).unwrap();
13018 _ = follower.update(cx, {
13019 let update = pending_update.clone();
13020 let is_still_following = is_still_following.clone();
13021 let follower_edit_event_count = follower_edit_event_count.clone();
13022 |_, window, cx| {
13023 cx.subscribe_in(
13024 &leader_entity,
13025 window,
13026 move |_, leader, event, window, cx| {
13027 leader.read(cx).add_event_to_update_proto(
13028 event,
13029 &mut update.borrow_mut(),
13030 window,
13031 cx,
13032 );
13033 },
13034 )
13035 .detach();
13036
13037 cx.subscribe_in(
13038 &follower_entity,
13039 window,
13040 move |_, _, event: &EditorEvent, _window, _cx| {
13041 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13042 *is_still_following.borrow_mut() = false;
13043 }
13044
13045 if let EditorEvent::BufferEdited = event {
13046 *follower_edit_event_count.borrow_mut() += 1;
13047 }
13048 },
13049 )
13050 .detach();
13051 }
13052 });
13053
13054 // Update the selections only
13055 _ = leader.update(cx, |leader, window, cx| {
13056 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13057 });
13058 follower
13059 .update(cx, |follower, window, cx| {
13060 follower.apply_update_proto(
13061 &project,
13062 pending_update.borrow_mut().take().unwrap(),
13063 window,
13064 cx,
13065 )
13066 })
13067 .unwrap()
13068 .await
13069 .unwrap();
13070 _ = follower.update(cx, |follower, _, cx| {
13071 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13072 });
13073 assert!(*is_still_following.borrow());
13074 assert_eq!(*follower_edit_event_count.borrow(), 0);
13075
13076 // Update the scroll position only
13077 _ = leader.update(cx, |leader, window, cx| {
13078 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13079 });
13080 follower
13081 .update(cx, |follower, window, cx| {
13082 follower.apply_update_proto(
13083 &project,
13084 pending_update.borrow_mut().take().unwrap(),
13085 window,
13086 cx,
13087 )
13088 })
13089 .unwrap()
13090 .await
13091 .unwrap();
13092 assert_eq!(
13093 follower
13094 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13095 .unwrap(),
13096 gpui::Point::new(1.5, 3.5)
13097 );
13098 assert!(*is_still_following.borrow());
13099 assert_eq!(*follower_edit_event_count.borrow(), 0);
13100
13101 // Update the selections and scroll position. The follower's scroll position is updated
13102 // via autoscroll, not via the leader's exact scroll position.
13103 _ = leader.update(cx, |leader, window, cx| {
13104 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13105 leader.request_autoscroll(Autoscroll::newest(), cx);
13106 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13107 });
13108 follower
13109 .update(cx, |follower, window, cx| {
13110 follower.apply_update_proto(
13111 &project,
13112 pending_update.borrow_mut().take().unwrap(),
13113 window,
13114 cx,
13115 )
13116 })
13117 .unwrap()
13118 .await
13119 .unwrap();
13120 _ = follower.update(cx, |follower, _, cx| {
13121 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13122 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13123 });
13124 assert!(*is_still_following.borrow());
13125
13126 // Creating a pending selection that precedes another selection
13127 _ = leader.update(cx, |leader, window, cx| {
13128 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13129 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13130 });
13131 follower
13132 .update(cx, |follower, window, cx| {
13133 follower.apply_update_proto(
13134 &project,
13135 pending_update.borrow_mut().take().unwrap(),
13136 window,
13137 cx,
13138 )
13139 })
13140 .unwrap()
13141 .await
13142 .unwrap();
13143 _ = follower.update(cx, |follower, _, cx| {
13144 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13145 });
13146 assert!(*is_still_following.borrow());
13147
13148 // Extend the pending selection so that it surrounds another selection
13149 _ = leader.update(cx, |leader, window, cx| {
13150 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13151 });
13152 follower
13153 .update(cx, |follower, window, cx| {
13154 follower.apply_update_proto(
13155 &project,
13156 pending_update.borrow_mut().take().unwrap(),
13157 window,
13158 cx,
13159 )
13160 })
13161 .unwrap()
13162 .await
13163 .unwrap();
13164 _ = follower.update(cx, |follower, _, cx| {
13165 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13166 });
13167
13168 // Scrolling locally breaks the follow
13169 _ = follower.update(cx, |follower, window, cx| {
13170 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13171 follower.set_scroll_anchor(
13172 ScrollAnchor {
13173 anchor: top_anchor,
13174 offset: gpui::Point::new(0.0, 0.5),
13175 },
13176 window,
13177 cx,
13178 );
13179 });
13180 assert!(!(*is_still_following.borrow()));
13181}
13182
13183#[gpui::test]
13184async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13185 init_test(cx, |_| {});
13186
13187 let fs = FakeFs::new(cx.executor());
13188 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13189 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13190 let pane = workspace
13191 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13192 .unwrap();
13193
13194 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13195
13196 let leader = pane.update_in(cx, |_, window, cx| {
13197 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13198 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13199 });
13200
13201 // Start following the editor when it has no excerpts.
13202 let mut state_message =
13203 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13204 let workspace_entity = workspace.root(cx).unwrap();
13205 let follower_1 = cx
13206 .update_window(*workspace.deref(), |_, window, cx| {
13207 Editor::from_state_proto(
13208 workspace_entity,
13209 ViewId {
13210 creator: CollaboratorId::PeerId(PeerId::default()),
13211 id: 0,
13212 },
13213 &mut state_message,
13214 window,
13215 cx,
13216 )
13217 })
13218 .unwrap()
13219 .unwrap()
13220 .await
13221 .unwrap();
13222
13223 let update_message = Rc::new(RefCell::new(None));
13224 follower_1.update_in(cx, {
13225 let update = update_message.clone();
13226 |_, window, cx| {
13227 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13228 leader.read(cx).add_event_to_update_proto(
13229 event,
13230 &mut update.borrow_mut(),
13231 window,
13232 cx,
13233 );
13234 })
13235 .detach();
13236 }
13237 });
13238
13239 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13240 (
13241 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13242 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13243 )
13244 });
13245
13246 // Insert some excerpts.
13247 leader.update(cx, |leader, cx| {
13248 leader.buffer.update(cx, |multibuffer, cx| {
13249 multibuffer.set_excerpts_for_path(
13250 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13251 buffer_1.clone(),
13252 vec![
13253 Point::row_range(0..3),
13254 Point::row_range(1..6),
13255 Point::row_range(12..15),
13256 ],
13257 0,
13258 cx,
13259 );
13260 multibuffer.set_excerpts_for_path(
13261 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13262 buffer_2.clone(),
13263 vec![Point::row_range(0..6), Point::row_range(8..12)],
13264 0,
13265 cx,
13266 );
13267 });
13268 });
13269
13270 // Apply the update of adding the excerpts.
13271 follower_1
13272 .update_in(cx, |follower, window, cx| {
13273 follower.apply_update_proto(
13274 &project,
13275 update_message.borrow().clone().unwrap(),
13276 window,
13277 cx,
13278 )
13279 })
13280 .await
13281 .unwrap();
13282 assert_eq!(
13283 follower_1.update(cx, |editor, cx| editor.text(cx)),
13284 leader.update(cx, |editor, cx| editor.text(cx))
13285 );
13286 update_message.borrow_mut().take();
13287
13288 // Start following separately after it already has excerpts.
13289 let mut state_message =
13290 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13291 let workspace_entity = workspace.root(cx).unwrap();
13292 let follower_2 = cx
13293 .update_window(*workspace.deref(), |_, window, cx| {
13294 Editor::from_state_proto(
13295 workspace_entity,
13296 ViewId {
13297 creator: CollaboratorId::PeerId(PeerId::default()),
13298 id: 0,
13299 },
13300 &mut state_message,
13301 window,
13302 cx,
13303 )
13304 })
13305 .unwrap()
13306 .unwrap()
13307 .await
13308 .unwrap();
13309 assert_eq!(
13310 follower_2.update(cx, |editor, cx| editor.text(cx)),
13311 leader.update(cx, |editor, cx| editor.text(cx))
13312 );
13313
13314 // Remove some excerpts.
13315 leader.update(cx, |leader, cx| {
13316 leader.buffer.update(cx, |multibuffer, cx| {
13317 let excerpt_ids = multibuffer.excerpt_ids();
13318 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13319 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13320 });
13321 });
13322
13323 // Apply the update of removing the excerpts.
13324 follower_1
13325 .update_in(cx, |follower, window, cx| {
13326 follower.apply_update_proto(
13327 &project,
13328 update_message.borrow().clone().unwrap(),
13329 window,
13330 cx,
13331 )
13332 })
13333 .await
13334 .unwrap();
13335 follower_2
13336 .update_in(cx, |follower, window, cx| {
13337 follower.apply_update_proto(
13338 &project,
13339 update_message.borrow().clone().unwrap(),
13340 window,
13341 cx,
13342 )
13343 })
13344 .await
13345 .unwrap();
13346 update_message.borrow_mut().take();
13347 assert_eq!(
13348 follower_1.update(cx, |editor, cx| editor.text(cx)),
13349 leader.update(cx, |editor, cx| editor.text(cx))
13350 );
13351}
13352
13353#[gpui::test]
13354async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13355 init_test(cx, |_| {});
13356
13357 let mut cx = EditorTestContext::new(cx).await;
13358 let lsp_store =
13359 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13360
13361 cx.set_state(indoc! {"
13362 ˇfn func(abc def: i32) -> u32 {
13363 }
13364 "});
13365
13366 cx.update(|_, cx| {
13367 lsp_store.update(cx, |lsp_store, cx| {
13368 lsp_store
13369 .update_diagnostics(
13370 LanguageServerId(0),
13371 lsp::PublishDiagnosticsParams {
13372 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13373 version: None,
13374 diagnostics: vec![
13375 lsp::Diagnostic {
13376 range: lsp::Range::new(
13377 lsp::Position::new(0, 11),
13378 lsp::Position::new(0, 12),
13379 ),
13380 severity: Some(lsp::DiagnosticSeverity::ERROR),
13381 ..Default::default()
13382 },
13383 lsp::Diagnostic {
13384 range: lsp::Range::new(
13385 lsp::Position::new(0, 12),
13386 lsp::Position::new(0, 15),
13387 ),
13388 severity: Some(lsp::DiagnosticSeverity::ERROR),
13389 ..Default::default()
13390 },
13391 lsp::Diagnostic {
13392 range: lsp::Range::new(
13393 lsp::Position::new(0, 25),
13394 lsp::Position::new(0, 28),
13395 ),
13396 severity: Some(lsp::DiagnosticSeverity::ERROR),
13397 ..Default::default()
13398 },
13399 ],
13400 },
13401 &[],
13402 cx,
13403 )
13404 .unwrap()
13405 });
13406 });
13407
13408 executor.run_until_parked();
13409
13410 cx.update_editor(|editor, window, cx| {
13411 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13412 });
13413
13414 cx.assert_editor_state(indoc! {"
13415 fn func(abc def: i32) -> ˇu32 {
13416 }
13417 "});
13418
13419 cx.update_editor(|editor, window, cx| {
13420 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13421 });
13422
13423 cx.assert_editor_state(indoc! {"
13424 fn func(abc ˇdef: i32) -> u32 {
13425 }
13426 "});
13427
13428 cx.update_editor(|editor, window, cx| {
13429 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13430 });
13431
13432 cx.assert_editor_state(indoc! {"
13433 fn func(abcˇ def: i32) -> u32 {
13434 }
13435 "});
13436
13437 cx.update_editor(|editor, window, cx| {
13438 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13439 });
13440
13441 cx.assert_editor_state(indoc! {"
13442 fn func(abc def: i32) -> ˇu32 {
13443 }
13444 "});
13445}
13446
13447#[gpui::test]
13448async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13449 init_test(cx, |_| {});
13450
13451 let mut cx = EditorTestContext::new(cx).await;
13452
13453 let diff_base = r#"
13454 use some::mod;
13455
13456 const A: u32 = 42;
13457
13458 fn main() {
13459 println!("hello");
13460
13461 println!("world");
13462 }
13463 "#
13464 .unindent();
13465
13466 // Edits are modified, removed, modified, added
13467 cx.set_state(
13468 &r#"
13469 use some::modified;
13470
13471 ˇ
13472 fn main() {
13473 println!("hello there");
13474
13475 println!("around the");
13476 println!("world");
13477 }
13478 "#
13479 .unindent(),
13480 );
13481
13482 cx.set_head_text(&diff_base);
13483 executor.run_until_parked();
13484
13485 cx.update_editor(|editor, window, cx| {
13486 //Wrap around the bottom of the buffer
13487 for _ in 0..3 {
13488 editor.go_to_next_hunk(&GoToHunk, window, cx);
13489 }
13490 });
13491
13492 cx.assert_editor_state(
13493 &r#"
13494 ˇuse some::modified;
13495
13496
13497 fn main() {
13498 println!("hello there");
13499
13500 println!("around the");
13501 println!("world");
13502 }
13503 "#
13504 .unindent(),
13505 );
13506
13507 cx.update_editor(|editor, window, cx| {
13508 //Wrap around the top of the buffer
13509 for _ in 0..2 {
13510 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13511 }
13512 });
13513
13514 cx.assert_editor_state(
13515 &r#"
13516 use some::modified;
13517
13518
13519 fn main() {
13520 ˇ println!("hello there");
13521
13522 println!("around the");
13523 println!("world");
13524 }
13525 "#
13526 .unindent(),
13527 );
13528
13529 cx.update_editor(|editor, window, cx| {
13530 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13531 });
13532
13533 cx.assert_editor_state(
13534 &r#"
13535 use some::modified;
13536
13537 ˇ
13538 fn main() {
13539 println!("hello there");
13540
13541 println!("around the");
13542 println!("world");
13543 }
13544 "#
13545 .unindent(),
13546 );
13547
13548 cx.update_editor(|editor, window, cx| {
13549 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13550 });
13551
13552 cx.assert_editor_state(
13553 &r#"
13554 ˇuse some::modified;
13555
13556
13557 fn main() {
13558 println!("hello there");
13559
13560 println!("around the");
13561 println!("world");
13562 }
13563 "#
13564 .unindent(),
13565 );
13566
13567 cx.update_editor(|editor, window, cx| {
13568 for _ in 0..2 {
13569 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13570 }
13571 });
13572
13573 cx.assert_editor_state(
13574 &r#"
13575 use some::modified;
13576
13577
13578 fn main() {
13579 ˇ println!("hello there");
13580
13581 println!("around the");
13582 println!("world");
13583 }
13584 "#
13585 .unindent(),
13586 );
13587
13588 cx.update_editor(|editor, window, cx| {
13589 editor.fold(&Fold, window, cx);
13590 });
13591
13592 cx.update_editor(|editor, window, cx| {
13593 editor.go_to_next_hunk(&GoToHunk, window, cx);
13594 });
13595
13596 cx.assert_editor_state(
13597 &r#"
13598 ˇuse some::modified;
13599
13600
13601 fn main() {
13602 println!("hello there");
13603
13604 println!("around the");
13605 println!("world");
13606 }
13607 "#
13608 .unindent(),
13609 );
13610}
13611
13612#[test]
13613fn test_split_words() {
13614 fn split(text: &str) -> Vec<&str> {
13615 split_words(text).collect()
13616 }
13617
13618 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13619 assert_eq!(split("hello_world"), &["hello_", "world"]);
13620 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13621 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13622 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13623 assert_eq!(split("helloworld"), &["helloworld"]);
13624
13625 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13626}
13627
13628#[gpui::test]
13629async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13630 init_test(cx, |_| {});
13631
13632 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13633 let mut assert = |before, after| {
13634 let _state_context = cx.set_state(before);
13635 cx.run_until_parked();
13636 cx.update_editor(|editor, window, cx| {
13637 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13638 });
13639 cx.run_until_parked();
13640 cx.assert_editor_state(after);
13641 };
13642
13643 // Outside bracket jumps to outside of matching bracket
13644 assert("console.logˇ(var);", "console.log(var)ˇ;");
13645 assert("console.log(var)ˇ;", "console.logˇ(var);");
13646
13647 // Inside bracket jumps to inside of matching bracket
13648 assert("console.log(ˇvar);", "console.log(varˇ);");
13649 assert("console.log(varˇ);", "console.log(ˇvar);");
13650
13651 // When outside a bracket and inside, favor jumping to the inside bracket
13652 assert(
13653 "console.log('foo', [1, 2, 3]ˇ);",
13654 "console.log(ˇ'foo', [1, 2, 3]);",
13655 );
13656 assert(
13657 "console.log(ˇ'foo', [1, 2, 3]);",
13658 "console.log('foo', [1, 2, 3]ˇ);",
13659 );
13660
13661 // Bias forward if two options are equally likely
13662 assert(
13663 "let result = curried_fun()ˇ();",
13664 "let result = curried_fun()()ˇ;",
13665 );
13666
13667 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13668 assert(
13669 indoc! {"
13670 function test() {
13671 console.log('test')ˇ
13672 }"},
13673 indoc! {"
13674 function test() {
13675 console.logˇ('test')
13676 }"},
13677 );
13678}
13679
13680#[gpui::test]
13681async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13682 init_test(cx, |_| {});
13683
13684 let fs = FakeFs::new(cx.executor());
13685 fs.insert_tree(
13686 path!("/a"),
13687 json!({
13688 "main.rs": "fn main() { let a = 5; }",
13689 "other.rs": "// Test file",
13690 }),
13691 )
13692 .await;
13693 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13694
13695 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13696 language_registry.add(Arc::new(Language::new(
13697 LanguageConfig {
13698 name: "Rust".into(),
13699 matcher: LanguageMatcher {
13700 path_suffixes: vec!["rs".to_string()],
13701 ..Default::default()
13702 },
13703 brackets: BracketPairConfig {
13704 pairs: vec![BracketPair {
13705 start: "{".to_string(),
13706 end: "}".to_string(),
13707 close: true,
13708 surround: true,
13709 newline: true,
13710 }],
13711 disabled_scopes_by_bracket_ix: Vec::new(),
13712 },
13713 ..Default::default()
13714 },
13715 Some(tree_sitter_rust::LANGUAGE.into()),
13716 )));
13717 let mut fake_servers = language_registry.register_fake_lsp(
13718 "Rust",
13719 FakeLspAdapter {
13720 capabilities: lsp::ServerCapabilities {
13721 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13722 first_trigger_character: "{".to_string(),
13723 more_trigger_character: None,
13724 }),
13725 ..Default::default()
13726 },
13727 ..Default::default()
13728 },
13729 );
13730
13731 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13732
13733 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13734
13735 let worktree_id = workspace
13736 .update(cx, |workspace, _, cx| {
13737 workspace.project().update(cx, |project, cx| {
13738 project.worktrees(cx).next().unwrap().read(cx).id()
13739 })
13740 })
13741 .unwrap();
13742
13743 let buffer = project
13744 .update(cx, |project, cx| {
13745 project.open_local_buffer(path!("/a/main.rs"), cx)
13746 })
13747 .await
13748 .unwrap();
13749 let editor_handle = workspace
13750 .update(cx, |workspace, window, cx| {
13751 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13752 })
13753 .unwrap()
13754 .await
13755 .unwrap()
13756 .downcast::<Editor>()
13757 .unwrap();
13758
13759 cx.executor().start_waiting();
13760 let fake_server = fake_servers.next().await.unwrap();
13761
13762 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13763 |params, _| async move {
13764 assert_eq!(
13765 params.text_document_position.text_document.uri,
13766 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13767 );
13768 assert_eq!(
13769 params.text_document_position.position,
13770 lsp::Position::new(0, 21),
13771 );
13772
13773 Ok(Some(vec![lsp::TextEdit {
13774 new_text: "]".to_string(),
13775 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13776 }]))
13777 },
13778 );
13779
13780 editor_handle.update_in(cx, |editor, window, cx| {
13781 window.focus(&editor.focus_handle(cx));
13782 editor.change_selections(None, window, cx, |s| {
13783 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13784 });
13785 editor.handle_input("{", window, cx);
13786 });
13787
13788 cx.executor().run_until_parked();
13789
13790 buffer.update(cx, |buffer, _| {
13791 assert_eq!(
13792 buffer.text(),
13793 "fn main() { let a = {5}; }",
13794 "No extra braces from on type formatting should appear in the buffer"
13795 )
13796 });
13797}
13798
13799#[gpui::test]
13800async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13801 init_test(cx, |_| {});
13802
13803 let fs = FakeFs::new(cx.executor());
13804 fs.insert_tree(
13805 path!("/a"),
13806 json!({
13807 "main.rs": "fn main() { let a = 5; }",
13808 "other.rs": "// Test file",
13809 }),
13810 )
13811 .await;
13812
13813 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13814
13815 let server_restarts = Arc::new(AtomicUsize::new(0));
13816 let closure_restarts = Arc::clone(&server_restarts);
13817 let language_server_name = "test language server";
13818 let language_name: LanguageName = "Rust".into();
13819
13820 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13821 language_registry.add(Arc::new(Language::new(
13822 LanguageConfig {
13823 name: language_name.clone(),
13824 matcher: LanguageMatcher {
13825 path_suffixes: vec!["rs".to_string()],
13826 ..Default::default()
13827 },
13828 ..Default::default()
13829 },
13830 Some(tree_sitter_rust::LANGUAGE.into()),
13831 )));
13832 let mut fake_servers = language_registry.register_fake_lsp(
13833 "Rust",
13834 FakeLspAdapter {
13835 name: language_server_name,
13836 initialization_options: Some(json!({
13837 "testOptionValue": true
13838 })),
13839 initializer: Some(Box::new(move |fake_server| {
13840 let task_restarts = Arc::clone(&closure_restarts);
13841 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13842 task_restarts.fetch_add(1, atomic::Ordering::Release);
13843 futures::future::ready(Ok(()))
13844 });
13845 })),
13846 ..Default::default()
13847 },
13848 );
13849
13850 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13851 let _buffer = project
13852 .update(cx, |project, cx| {
13853 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13854 })
13855 .await
13856 .unwrap();
13857 let _fake_server = fake_servers.next().await.unwrap();
13858 update_test_language_settings(cx, |language_settings| {
13859 language_settings.languages.insert(
13860 language_name.clone(),
13861 LanguageSettingsContent {
13862 tab_size: NonZeroU32::new(8),
13863 ..Default::default()
13864 },
13865 );
13866 });
13867 cx.executor().run_until_parked();
13868 assert_eq!(
13869 server_restarts.load(atomic::Ordering::Acquire),
13870 0,
13871 "Should not restart LSP server on an unrelated change"
13872 );
13873
13874 update_test_project_settings(cx, |project_settings| {
13875 project_settings.lsp.insert(
13876 "Some other server name".into(),
13877 LspSettings {
13878 binary: None,
13879 settings: None,
13880 initialization_options: Some(json!({
13881 "some other init value": false
13882 })),
13883 enable_lsp_tasks: false,
13884 },
13885 );
13886 });
13887 cx.executor().run_until_parked();
13888 assert_eq!(
13889 server_restarts.load(atomic::Ordering::Acquire),
13890 0,
13891 "Should not restart LSP server on an unrelated LSP settings change"
13892 );
13893
13894 update_test_project_settings(cx, |project_settings| {
13895 project_settings.lsp.insert(
13896 language_server_name.into(),
13897 LspSettings {
13898 binary: None,
13899 settings: None,
13900 initialization_options: Some(json!({
13901 "anotherInitValue": false
13902 })),
13903 enable_lsp_tasks: false,
13904 },
13905 );
13906 });
13907 cx.executor().run_until_parked();
13908 assert_eq!(
13909 server_restarts.load(atomic::Ordering::Acquire),
13910 1,
13911 "Should restart LSP server on a related LSP settings change"
13912 );
13913
13914 update_test_project_settings(cx, |project_settings| {
13915 project_settings.lsp.insert(
13916 language_server_name.into(),
13917 LspSettings {
13918 binary: None,
13919 settings: None,
13920 initialization_options: Some(json!({
13921 "anotherInitValue": false
13922 })),
13923 enable_lsp_tasks: false,
13924 },
13925 );
13926 });
13927 cx.executor().run_until_parked();
13928 assert_eq!(
13929 server_restarts.load(atomic::Ordering::Acquire),
13930 1,
13931 "Should not restart LSP server on a related LSP settings change that is the same"
13932 );
13933
13934 update_test_project_settings(cx, |project_settings| {
13935 project_settings.lsp.insert(
13936 language_server_name.into(),
13937 LspSettings {
13938 binary: None,
13939 settings: None,
13940 initialization_options: None,
13941 enable_lsp_tasks: false,
13942 },
13943 );
13944 });
13945 cx.executor().run_until_parked();
13946 assert_eq!(
13947 server_restarts.load(atomic::Ordering::Acquire),
13948 2,
13949 "Should restart LSP server on another related LSP settings change"
13950 );
13951}
13952
13953#[gpui::test]
13954async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13955 init_test(cx, |_| {});
13956
13957 let mut cx = EditorLspTestContext::new_rust(
13958 lsp::ServerCapabilities {
13959 completion_provider: Some(lsp::CompletionOptions {
13960 trigger_characters: Some(vec![".".to_string()]),
13961 resolve_provider: Some(true),
13962 ..Default::default()
13963 }),
13964 ..Default::default()
13965 },
13966 cx,
13967 )
13968 .await;
13969
13970 cx.set_state("fn main() { let a = 2ˇ; }");
13971 cx.simulate_keystroke(".");
13972 let completion_item = lsp::CompletionItem {
13973 label: "some".into(),
13974 kind: Some(lsp::CompletionItemKind::SNIPPET),
13975 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13976 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13977 kind: lsp::MarkupKind::Markdown,
13978 value: "```rust\nSome(2)\n```".to_string(),
13979 })),
13980 deprecated: Some(false),
13981 sort_text: Some("fffffff2".to_string()),
13982 filter_text: Some("some".to_string()),
13983 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13984 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13985 range: lsp::Range {
13986 start: lsp::Position {
13987 line: 0,
13988 character: 22,
13989 },
13990 end: lsp::Position {
13991 line: 0,
13992 character: 22,
13993 },
13994 },
13995 new_text: "Some(2)".to_string(),
13996 })),
13997 additional_text_edits: Some(vec![lsp::TextEdit {
13998 range: lsp::Range {
13999 start: lsp::Position {
14000 line: 0,
14001 character: 20,
14002 },
14003 end: lsp::Position {
14004 line: 0,
14005 character: 22,
14006 },
14007 },
14008 new_text: "".to_string(),
14009 }]),
14010 ..Default::default()
14011 };
14012
14013 let closure_completion_item = completion_item.clone();
14014 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14015 let task_completion_item = closure_completion_item.clone();
14016 async move {
14017 Ok(Some(lsp::CompletionResponse::Array(vec![
14018 task_completion_item,
14019 ])))
14020 }
14021 });
14022
14023 request.next().await;
14024
14025 cx.condition(|editor, _| editor.context_menu_visible())
14026 .await;
14027 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14028 editor
14029 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14030 .unwrap()
14031 });
14032 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14033
14034 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14035 let task_completion_item = completion_item.clone();
14036 async move { Ok(task_completion_item) }
14037 })
14038 .next()
14039 .await
14040 .unwrap();
14041 apply_additional_edits.await.unwrap();
14042 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14043}
14044
14045#[gpui::test]
14046async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14047 init_test(cx, |_| {});
14048
14049 let mut cx = EditorLspTestContext::new_rust(
14050 lsp::ServerCapabilities {
14051 completion_provider: Some(lsp::CompletionOptions {
14052 trigger_characters: Some(vec![".".to_string()]),
14053 resolve_provider: Some(true),
14054 ..Default::default()
14055 }),
14056 ..Default::default()
14057 },
14058 cx,
14059 )
14060 .await;
14061
14062 cx.set_state("fn main() { let a = 2ˇ; }");
14063 cx.simulate_keystroke(".");
14064
14065 let item1 = lsp::CompletionItem {
14066 label: "method id()".to_string(),
14067 filter_text: Some("id".to_string()),
14068 detail: None,
14069 documentation: None,
14070 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14071 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14072 new_text: ".id".to_string(),
14073 })),
14074 ..lsp::CompletionItem::default()
14075 };
14076
14077 let item2 = lsp::CompletionItem {
14078 label: "other".to_string(),
14079 filter_text: Some("other".to_string()),
14080 detail: None,
14081 documentation: None,
14082 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14083 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14084 new_text: ".other".to_string(),
14085 })),
14086 ..lsp::CompletionItem::default()
14087 };
14088
14089 let item1 = item1.clone();
14090 cx.set_request_handler::<lsp::request::Completion, _, _>({
14091 let item1 = item1.clone();
14092 move |_, _, _| {
14093 let item1 = item1.clone();
14094 let item2 = item2.clone();
14095 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14096 }
14097 })
14098 .next()
14099 .await;
14100
14101 cx.condition(|editor, _| editor.context_menu_visible())
14102 .await;
14103 cx.update_editor(|editor, _, _| {
14104 let context_menu = editor.context_menu.borrow_mut();
14105 let context_menu = context_menu
14106 .as_ref()
14107 .expect("Should have the context menu deployed");
14108 match context_menu {
14109 CodeContextMenu::Completions(completions_menu) => {
14110 let completions = completions_menu.completions.borrow_mut();
14111 assert_eq!(
14112 completions
14113 .iter()
14114 .map(|completion| &completion.label.text)
14115 .collect::<Vec<_>>(),
14116 vec!["method id()", "other"]
14117 )
14118 }
14119 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14120 }
14121 });
14122
14123 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14124 let item1 = item1.clone();
14125 move |_, item_to_resolve, _| {
14126 let item1 = item1.clone();
14127 async move {
14128 if item1 == item_to_resolve {
14129 Ok(lsp::CompletionItem {
14130 label: "method id()".to_string(),
14131 filter_text: Some("id".to_string()),
14132 detail: Some("Now resolved!".to_string()),
14133 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14134 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14135 range: lsp::Range::new(
14136 lsp::Position::new(0, 22),
14137 lsp::Position::new(0, 22),
14138 ),
14139 new_text: ".id".to_string(),
14140 })),
14141 ..lsp::CompletionItem::default()
14142 })
14143 } else {
14144 Ok(item_to_resolve)
14145 }
14146 }
14147 }
14148 })
14149 .next()
14150 .await
14151 .unwrap();
14152 cx.run_until_parked();
14153
14154 cx.update_editor(|editor, window, cx| {
14155 editor.context_menu_next(&Default::default(), window, cx);
14156 });
14157
14158 cx.update_editor(|editor, _, _| {
14159 let context_menu = editor.context_menu.borrow_mut();
14160 let context_menu = context_menu
14161 .as_ref()
14162 .expect("Should have the context menu deployed");
14163 match context_menu {
14164 CodeContextMenu::Completions(completions_menu) => {
14165 let completions = completions_menu.completions.borrow_mut();
14166 assert_eq!(
14167 completions
14168 .iter()
14169 .map(|completion| &completion.label.text)
14170 .collect::<Vec<_>>(),
14171 vec!["method id() Now resolved!", "other"],
14172 "Should update first completion label, but not second as the filter text did not match."
14173 );
14174 }
14175 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14176 }
14177 });
14178}
14179
14180#[gpui::test]
14181async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14182 init_test(cx, |_| {});
14183 let mut cx = EditorLspTestContext::new_rust(
14184 lsp::ServerCapabilities {
14185 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14186 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14187 completion_provider: Some(lsp::CompletionOptions {
14188 resolve_provider: Some(true),
14189 ..Default::default()
14190 }),
14191 ..Default::default()
14192 },
14193 cx,
14194 )
14195 .await;
14196 cx.set_state(indoc! {"
14197 struct TestStruct {
14198 field: i32
14199 }
14200
14201 fn mainˇ() {
14202 let unused_var = 42;
14203 let test_struct = TestStruct { field: 42 };
14204 }
14205 "});
14206 let symbol_range = cx.lsp_range(indoc! {"
14207 struct TestStruct {
14208 field: i32
14209 }
14210
14211 «fn main»() {
14212 let unused_var = 42;
14213 let test_struct = TestStruct { field: 42 };
14214 }
14215 "});
14216 let mut hover_requests =
14217 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14218 Ok(Some(lsp::Hover {
14219 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14220 kind: lsp::MarkupKind::Markdown,
14221 value: "Function documentation".to_string(),
14222 }),
14223 range: Some(symbol_range),
14224 }))
14225 });
14226
14227 // Case 1: Test that code action menu hide hover popover
14228 cx.dispatch_action(Hover);
14229 hover_requests.next().await;
14230 cx.condition(|editor, _| editor.hover_state.visible()).await;
14231 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14232 move |_, _, _| async move {
14233 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14234 lsp::CodeAction {
14235 title: "Remove unused variable".to_string(),
14236 kind: Some(CodeActionKind::QUICKFIX),
14237 edit: Some(lsp::WorkspaceEdit {
14238 changes: Some(
14239 [(
14240 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14241 vec![lsp::TextEdit {
14242 range: lsp::Range::new(
14243 lsp::Position::new(5, 4),
14244 lsp::Position::new(5, 27),
14245 ),
14246 new_text: "".to_string(),
14247 }],
14248 )]
14249 .into_iter()
14250 .collect(),
14251 ),
14252 ..Default::default()
14253 }),
14254 ..Default::default()
14255 },
14256 )]))
14257 },
14258 );
14259 cx.update_editor(|editor, window, cx| {
14260 editor.toggle_code_actions(
14261 &ToggleCodeActions {
14262 deployed_from: None,
14263 quick_launch: false,
14264 },
14265 window,
14266 cx,
14267 );
14268 });
14269 code_action_requests.next().await;
14270 cx.run_until_parked();
14271 cx.condition(|editor, _| editor.context_menu_visible())
14272 .await;
14273 cx.update_editor(|editor, _, _| {
14274 assert!(
14275 !editor.hover_state.visible(),
14276 "Hover popover should be hidden when code action menu is shown"
14277 );
14278 // Hide code actions
14279 editor.context_menu.take();
14280 });
14281
14282 // Case 2: Test that code completions hide hover popover
14283 cx.dispatch_action(Hover);
14284 hover_requests.next().await;
14285 cx.condition(|editor, _| editor.hover_state.visible()).await;
14286 let counter = Arc::new(AtomicUsize::new(0));
14287 let mut completion_requests =
14288 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14289 let counter = counter.clone();
14290 async move {
14291 counter.fetch_add(1, atomic::Ordering::Release);
14292 Ok(Some(lsp::CompletionResponse::Array(vec![
14293 lsp::CompletionItem {
14294 label: "main".into(),
14295 kind: Some(lsp::CompletionItemKind::FUNCTION),
14296 detail: Some("() -> ()".to_string()),
14297 ..Default::default()
14298 },
14299 lsp::CompletionItem {
14300 label: "TestStruct".into(),
14301 kind: Some(lsp::CompletionItemKind::STRUCT),
14302 detail: Some("struct TestStruct".to_string()),
14303 ..Default::default()
14304 },
14305 ])))
14306 }
14307 });
14308 cx.update_editor(|editor, window, cx| {
14309 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14310 });
14311 completion_requests.next().await;
14312 cx.condition(|editor, _| editor.context_menu_visible())
14313 .await;
14314 cx.update_editor(|editor, _, _| {
14315 assert!(
14316 !editor.hover_state.visible(),
14317 "Hover popover should be hidden when completion menu is shown"
14318 );
14319 });
14320}
14321
14322#[gpui::test]
14323async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14324 init_test(cx, |_| {});
14325
14326 let mut cx = EditorLspTestContext::new_rust(
14327 lsp::ServerCapabilities {
14328 completion_provider: Some(lsp::CompletionOptions {
14329 trigger_characters: Some(vec![".".to_string()]),
14330 resolve_provider: Some(true),
14331 ..Default::default()
14332 }),
14333 ..Default::default()
14334 },
14335 cx,
14336 )
14337 .await;
14338
14339 cx.set_state("fn main() { let a = 2ˇ; }");
14340 cx.simulate_keystroke(".");
14341
14342 let unresolved_item_1 = lsp::CompletionItem {
14343 label: "id".to_string(),
14344 filter_text: Some("id".to_string()),
14345 detail: None,
14346 documentation: None,
14347 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14348 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14349 new_text: ".id".to_string(),
14350 })),
14351 ..lsp::CompletionItem::default()
14352 };
14353 let resolved_item_1 = lsp::CompletionItem {
14354 additional_text_edits: Some(vec![lsp::TextEdit {
14355 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14356 new_text: "!!".to_string(),
14357 }]),
14358 ..unresolved_item_1.clone()
14359 };
14360 let unresolved_item_2 = lsp::CompletionItem {
14361 label: "other".to_string(),
14362 filter_text: Some("other".to_string()),
14363 detail: None,
14364 documentation: None,
14365 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14366 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14367 new_text: ".other".to_string(),
14368 })),
14369 ..lsp::CompletionItem::default()
14370 };
14371 let resolved_item_2 = lsp::CompletionItem {
14372 additional_text_edits: Some(vec![lsp::TextEdit {
14373 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14374 new_text: "??".to_string(),
14375 }]),
14376 ..unresolved_item_2.clone()
14377 };
14378
14379 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14380 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14381 cx.lsp
14382 .server
14383 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14384 let unresolved_item_1 = unresolved_item_1.clone();
14385 let resolved_item_1 = resolved_item_1.clone();
14386 let unresolved_item_2 = unresolved_item_2.clone();
14387 let resolved_item_2 = resolved_item_2.clone();
14388 let resolve_requests_1 = resolve_requests_1.clone();
14389 let resolve_requests_2 = resolve_requests_2.clone();
14390 move |unresolved_request, _| {
14391 let unresolved_item_1 = unresolved_item_1.clone();
14392 let resolved_item_1 = resolved_item_1.clone();
14393 let unresolved_item_2 = unresolved_item_2.clone();
14394 let resolved_item_2 = resolved_item_2.clone();
14395 let resolve_requests_1 = resolve_requests_1.clone();
14396 let resolve_requests_2 = resolve_requests_2.clone();
14397 async move {
14398 if unresolved_request == unresolved_item_1 {
14399 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14400 Ok(resolved_item_1.clone())
14401 } else if unresolved_request == unresolved_item_2 {
14402 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14403 Ok(resolved_item_2.clone())
14404 } else {
14405 panic!("Unexpected completion item {unresolved_request:?}")
14406 }
14407 }
14408 }
14409 })
14410 .detach();
14411
14412 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14413 let unresolved_item_1 = unresolved_item_1.clone();
14414 let unresolved_item_2 = unresolved_item_2.clone();
14415 async move {
14416 Ok(Some(lsp::CompletionResponse::Array(vec![
14417 unresolved_item_1,
14418 unresolved_item_2,
14419 ])))
14420 }
14421 })
14422 .next()
14423 .await;
14424
14425 cx.condition(|editor, _| editor.context_menu_visible())
14426 .await;
14427 cx.update_editor(|editor, _, _| {
14428 let context_menu = editor.context_menu.borrow_mut();
14429 let context_menu = context_menu
14430 .as_ref()
14431 .expect("Should have the context menu deployed");
14432 match context_menu {
14433 CodeContextMenu::Completions(completions_menu) => {
14434 let completions = completions_menu.completions.borrow_mut();
14435 assert_eq!(
14436 completions
14437 .iter()
14438 .map(|completion| &completion.label.text)
14439 .collect::<Vec<_>>(),
14440 vec!["id", "other"]
14441 )
14442 }
14443 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14444 }
14445 });
14446 cx.run_until_parked();
14447
14448 cx.update_editor(|editor, window, cx| {
14449 editor.context_menu_next(&ContextMenuNext, window, cx);
14450 });
14451 cx.run_until_parked();
14452 cx.update_editor(|editor, window, cx| {
14453 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14454 });
14455 cx.run_until_parked();
14456 cx.update_editor(|editor, window, cx| {
14457 editor.context_menu_next(&ContextMenuNext, window, cx);
14458 });
14459 cx.run_until_parked();
14460 cx.update_editor(|editor, window, cx| {
14461 editor
14462 .compose_completion(&ComposeCompletion::default(), window, cx)
14463 .expect("No task returned")
14464 })
14465 .await
14466 .expect("Completion failed");
14467 cx.run_until_parked();
14468
14469 cx.update_editor(|editor, _, cx| {
14470 assert_eq!(
14471 resolve_requests_1.load(atomic::Ordering::Acquire),
14472 1,
14473 "Should always resolve once despite multiple selections"
14474 );
14475 assert_eq!(
14476 resolve_requests_2.load(atomic::Ordering::Acquire),
14477 1,
14478 "Should always resolve once after multiple selections and applying the completion"
14479 );
14480 assert_eq!(
14481 editor.text(cx),
14482 "fn main() { let a = ??.other; }",
14483 "Should use resolved data when applying the completion"
14484 );
14485 });
14486}
14487
14488#[gpui::test]
14489async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14490 init_test(cx, |_| {});
14491
14492 let item_0 = lsp::CompletionItem {
14493 label: "abs".into(),
14494 insert_text: Some("abs".into()),
14495 data: Some(json!({ "very": "special"})),
14496 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14497 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14498 lsp::InsertReplaceEdit {
14499 new_text: "abs".to_string(),
14500 insert: lsp::Range::default(),
14501 replace: lsp::Range::default(),
14502 },
14503 )),
14504 ..lsp::CompletionItem::default()
14505 };
14506 let items = iter::once(item_0.clone())
14507 .chain((11..51).map(|i| lsp::CompletionItem {
14508 label: format!("item_{}", i),
14509 insert_text: Some(format!("item_{}", i)),
14510 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14511 ..lsp::CompletionItem::default()
14512 }))
14513 .collect::<Vec<_>>();
14514
14515 let default_commit_characters = vec!["?".to_string()];
14516 let default_data = json!({ "default": "data"});
14517 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14518 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14519 let default_edit_range = lsp::Range {
14520 start: lsp::Position {
14521 line: 0,
14522 character: 5,
14523 },
14524 end: lsp::Position {
14525 line: 0,
14526 character: 5,
14527 },
14528 };
14529
14530 let mut cx = EditorLspTestContext::new_rust(
14531 lsp::ServerCapabilities {
14532 completion_provider: Some(lsp::CompletionOptions {
14533 trigger_characters: Some(vec![".".to_string()]),
14534 resolve_provider: Some(true),
14535 ..Default::default()
14536 }),
14537 ..Default::default()
14538 },
14539 cx,
14540 )
14541 .await;
14542
14543 cx.set_state("fn main() { let a = 2ˇ; }");
14544 cx.simulate_keystroke(".");
14545
14546 let completion_data = default_data.clone();
14547 let completion_characters = default_commit_characters.clone();
14548 let completion_items = items.clone();
14549 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14550 let default_data = completion_data.clone();
14551 let default_commit_characters = completion_characters.clone();
14552 let items = completion_items.clone();
14553 async move {
14554 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14555 items,
14556 item_defaults: Some(lsp::CompletionListItemDefaults {
14557 data: Some(default_data.clone()),
14558 commit_characters: Some(default_commit_characters.clone()),
14559 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14560 default_edit_range,
14561 )),
14562 insert_text_format: Some(default_insert_text_format),
14563 insert_text_mode: Some(default_insert_text_mode),
14564 }),
14565 ..lsp::CompletionList::default()
14566 })))
14567 }
14568 })
14569 .next()
14570 .await;
14571
14572 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14573 cx.lsp
14574 .server
14575 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14576 let closure_resolved_items = resolved_items.clone();
14577 move |item_to_resolve, _| {
14578 let closure_resolved_items = closure_resolved_items.clone();
14579 async move {
14580 closure_resolved_items.lock().push(item_to_resolve.clone());
14581 Ok(item_to_resolve)
14582 }
14583 }
14584 })
14585 .detach();
14586
14587 cx.condition(|editor, _| editor.context_menu_visible())
14588 .await;
14589 cx.run_until_parked();
14590 cx.update_editor(|editor, _, _| {
14591 let menu = editor.context_menu.borrow_mut();
14592 match menu.as_ref().expect("should have the completions menu") {
14593 CodeContextMenu::Completions(completions_menu) => {
14594 assert_eq!(
14595 completions_menu
14596 .entries
14597 .borrow()
14598 .iter()
14599 .map(|mat| mat.string.clone())
14600 .collect::<Vec<String>>(),
14601 items
14602 .iter()
14603 .map(|completion| completion.label.clone())
14604 .collect::<Vec<String>>()
14605 );
14606 }
14607 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14608 }
14609 });
14610 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14611 // with 4 from the end.
14612 assert_eq!(
14613 *resolved_items.lock(),
14614 [&items[0..16], &items[items.len() - 4..items.len()]]
14615 .concat()
14616 .iter()
14617 .cloned()
14618 .map(|mut item| {
14619 if item.data.is_none() {
14620 item.data = Some(default_data.clone());
14621 }
14622 item
14623 })
14624 .collect::<Vec<lsp::CompletionItem>>(),
14625 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14626 );
14627 resolved_items.lock().clear();
14628
14629 cx.update_editor(|editor, window, cx| {
14630 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14631 });
14632 cx.run_until_parked();
14633 // Completions that have already been resolved are skipped.
14634 assert_eq!(
14635 *resolved_items.lock(),
14636 items[items.len() - 16..items.len() - 4]
14637 .iter()
14638 .cloned()
14639 .map(|mut item| {
14640 if item.data.is_none() {
14641 item.data = Some(default_data.clone());
14642 }
14643 item
14644 })
14645 .collect::<Vec<lsp::CompletionItem>>()
14646 );
14647 resolved_items.lock().clear();
14648}
14649
14650#[gpui::test]
14651async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14652 init_test(cx, |_| {});
14653
14654 let mut cx = EditorLspTestContext::new(
14655 Language::new(
14656 LanguageConfig {
14657 matcher: LanguageMatcher {
14658 path_suffixes: vec!["jsx".into()],
14659 ..Default::default()
14660 },
14661 overrides: [(
14662 "element".into(),
14663 LanguageConfigOverride {
14664 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14665 ..Default::default()
14666 },
14667 )]
14668 .into_iter()
14669 .collect(),
14670 ..Default::default()
14671 },
14672 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14673 )
14674 .with_override_query("(jsx_self_closing_element) @element")
14675 .unwrap(),
14676 lsp::ServerCapabilities {
14677 completion_provider: Some(lsp::CompletionOptions {
14678 trigger_characters: Some(vec![":".to_string()]),
14679 ..Default::default()
14680 }),
14681 ..Default::default()
14682 },
14683 cx,
14684 )
14685 .await;
14686
14687 cx.lsp
14688 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14689 Ok(Some(lsp::CompletionResponse::Array(vec![
14690 lsp::CompletionItem {
14691 label: "bg-blue".into(),
14692 ..Default::default()
14693 },
14694 lsp::CompletionItem {
14695 label: "bg-red".into(),
14696 ..Default::default()
14697 },
14698 lsp::CompletionItem {
14699 label: "bg-yellow".into(),
14700 ..Default::default()
14701 },
14702 ])))
14703 });
14704
14705 cx.set_state(r#"<p class="bgˇ" />"#);
14706
14707 // Trigger completion when typing a dash, because the dash is an extra
14708 // word character in the 'element' scope, which contains the cursor.
14709 cx.simulate_keystroke("-");
14710 cx.executor().run_until_parked();
14711 cx.update_editor(|editor, _, _| {
14712 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14713 {
14714 assert_eq!(
14715 completion_menu_entries(&menu),
14716 &["bg-red", "bg-blue", "bg-yellow"]
14717 );
14718 } else {
14719 panic!("expected completion menu to be open");
14720 }
14721 });
14722
14723 cx.simulate_keystroke("l");
14724 cx.executor().run_until_parked();
14725 cx.update_editor(|editor, _, _| {
14726 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14727 {
14728 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14729 } else {
14730 panic!("expected completion menu to be open");
14731 }
14732 });
14733
14734 // When filtering completions, consider the character after the '-' to
14735 // be the start of a subword.
14736 cx.set_state(r#"<p class="yelˇ" />"#);
14737 cx.simulate_keystroke("l");
14738 cx.executor().run_until_parked();
14739 cx.update_editor(|editor, _, _| {
14740 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14741 {
14742 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14743 } else {
14744 panic!("expected completion menu to be open");
14745 }
14746 });
14747}
14748
14749fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14750 let entries = menu.entries.borrow();
14751 entries.iter().map(|mat| mat.string.clone()).collect()
14752}
14753
14754#[gpui::test]
14755async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14756 init_test(cx, |settings| {
14757 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14758 FormatterList(vec![Formatter::Prettier].into()),
14759 ))
14760 });
14761
14762 let fs = FakeFs::new(cx.executor());
14763 fs.insert_file(path!("/file.ts"), Default::default()).await;
14764
14765 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14766 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14767
14768 language_registry.add(Arc::new(Language::new(
14769 LanguageConfig {
14770 name: "TypeScript".into(),
14771 matcher: LanguageMatcher {
14772 path_suffixes: vec!["ts".to_string()],
14773 ..Default::default()
14774 },
14775 ..Default::default()
14776 },
14777 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14778 )));
14779 update_test_language_settings(cx, |settings| {
14780 settings.defaults.prettier = Some(PrettierSettings {
14781 allowed: true,
14782 ..PrettierSettings::default()
14783 });
14784 });
14785
14786 let test_plugin = "test_plugin";
14787 let _ = language_registry.register_fake_lsp(
14788 "TypeScript",
14789 FakeLspAdapter {
14790 prettier_plugins: vec![test_plugin],
14791 ..Default::default()
14792 },
14793 );
14794
14795 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14796 let buffer = project
14797 .update(cx, |project, cx| {
14798 project.open_local_buffer(path!("/file.ts"), cx)
14799 })
14800 .await
14801 .unwrap();
14802
14803 let buffer_text = "one\ntwo\nthree\n";
14804 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14805 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14806 editor.update_in(cx, |editor, window, cx| {
14807 editor.set_text(buffer_text, window, cx)
14808 });
14809
14810 editor
14811 .update_in(cx, |editor, window, cx| {
14812 editor.perform_format(
14813 project.clone(),
14814 FormatTrigger::Manual,
14815 FormatTarget::Buffers,
14816 window,
14817 cx,
14818 )
14819 })
14820 .unwrap()
14821 .await;
14822 assert_eq!(
14823 editor.update(cx, |editor, cx| editor.text(cx)),
14824 buffer_text.to_string() + prettier_format_suffix,
14825 "Test prettier formatting was not applied to the original buffer text",
14826 );
14827
14828 update_test_language_settings(cx, |settings| {
14829 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14830 });
14831 let format = editor.update_in(cx, |editor, window, cx| {
14832 editor.perform_format(
14833 project.clone(),
14834 FormatTrigger::Manual,
14835 FormatTarget::Buffers,
14836 window,
14837 cx,
14838 )
14839 });
14840 format.await.unwrap();
14841 assert_eq!(
14842 editor.update(cx, |editor, cx| editor.text(cx)),
14843 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14844 "Autoformatting (via test prettier) was not applied to the original buffer text",
14845 );
14846}
14847
14848#[gpui::test]
14849async fn test_addition_reverts(cx: &mut TestAppContext) {
14850 init_test(cx, |_| {});
14851 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14852 let base_text = indoc! {r#"
14853 struct Row;
14854 struct Row1;
14855 struct Row2;
14856
14857 struct Row4;
14858 struct Row5;
14859 struct Row6;
14860
14861 struct Row8;
14862 struct Row9;
14863 struct Row10;"#};
14864
14865 // When addition hunks are not adjacent to carets, no hunk revert is performed
14866 assert_hunk_revert(
14867 indoc! {r#"struct Row;
14868 struct Row1;
14869 struct Row1.1;
14870 struct Row1.2;
14871 struct Row2;ˇ
14872
14873 struct Row4;
14874 struct Row5;
14875 struct Row6;
14876
14877 struct Row8;
14878 ˇstruct Row9;
14879 struct Row9.1;
14880 struct Row9.2;
14881 struct Row9.3;
14882 struct Row10;"#},
14883 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14884 indoc! {r#"struct Row;
14885 struct Row1;
14886 struct Row1.1;
14887 struct Row1.2;
14888 struct Row2;ˇ
14889
14890 struct Row4;
14891 struct Row5;
14892 struct Row6;
14893
14894 struct Row8;
14895 ˇstruct Row9;
14896 struct Row9.1;
14897 struct Row9.2;
14898 struct Row9.3;
14899 struct Row10;"#},
14900 base_text,
14901 &mut cx,
14902 );
14903 // Same for selections
14904 assert_hunk_revert(
14905 indoc! {r#"struct Row;
14906 struct Row1;
14907 struct Row2;
14908 struct Row2.1;
14909 struct Row2.2;
14910 «ˇ
14911 struct Row4;
14912 struct» Row5;
14913 «struct Row6;
14914 ˇ»
14915 struct Row9.1;
14916 struct Row9.2;
14917 struct Row9.3;
14918 struct Row8;
14919 struct Row9;
14920 struct Row10;"#},
14921 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14922 indoc! {r#"struct Row;
14923 struct Row1;
14924 struct Row2;
14925 struct Row2.1;
14926 struct Row2.2;
14927 «ˇ
14928 struct Row4;
14929 struct» Row5;
14930 «struct Row6;
14931 ˇ»
14932 struct Row9.1;
14933 struct Row9.2;
14934 struct Row9.3;
14935 struct Row8;
14936 struct Row9;
14937 struct Row10;"#},
14938 base_text,
14939 &mut cx,
14940 );
14941
14942 // When carets and selections intersect the addition hunks, those are reverted.
14943 // Adjacent carets got merged.
14944 assert_hunk_revert(
14945 indoc! {r#"struct Row;
14946 ˇ// something on the top
14947 struct Row1;
14948 struct Row2;
14949 struct Roˇw3.1;
14950 struct Row2.2;
14951 struct Row2.3;ˇ
14952
14953 struct Row4;
14954 struct ˇRow5.1;
14955 struct Row5.2;
14956 struct «Rowˇ»5.3;
14957 struct Row5;
14958 struct Row6;
14959 ˇ
14960 struct Row9.1;
14961 struct «Rowˇ»9.2;
14962 struct «ˇRow»9.3;
14963 struct Row8;
14964 struct Row9;
14965 «ˇ// something on bottom»
14966 struct Row10;"#},
14967 vec![
14968 DiffHunkStatusKind::Added,
14969 DiffHunkStatusKind::Added,
14970 DiffHunkStatusKind::Added,
14971 DiffHunkStatusKind::Added,
14972 DiffHunkStatusKind::Added,
14973 ],
14974 indoc! {r#"struct Row;
14975 ˇstruct Row1;
14976 struct Row2;
14977 ˇ
14978 struct Row4;
14979 ˇstruct Row5;
14980 struct Row6;
14981 ˇ
14982 ˇstruct Row8;
14983 struct Row9;
14984 ˇstruct Row10;"#},
14985 base_text,
14986 &mut cx,
14987 );
14988}
14989
14990#[gpui::test]
14991async fn test_modification_reverts(cx: &mut TestAppContext) {
14992 init_test(cx, |_| {});
14993 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14994 let base_text = indoc! {r#"
14995 struct Row;
14996 struct Row1;
14997 struct Row2;
14998
14999 struct Row4;
15000 struct Row5;
15001 struct Row6;
15002
15003 struct Row8;
15004 struct Row9;
15005 struct Row10;"#};
15006
15007 // Modification hunks behave the same as the addition ones.
15008 assert_hunk_revert(
15009 indoc! {r#"struct Row;
15010 struct Row1;
15011 struct Row33;
15012 ˇ
15013 struct Row4;
15014 struct Row5;
15015 struct Row6;
15016 ˇ
15017 struct Row99;
15018 struct Row9;
15019 struct Row10;"#},
15020 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15021 indoc! {r#"struct Row;
15022 struct Row1;
15023 struct Row33;
15024 ˇ
15025 struct Row4;
15026 struct Row5;
15027 struct Row6;
15028 ˇ
15029 struct Row99;
15030 struct Row9;
15031 struct Row10;"#},
15032 base_text,
15033 &mut cx,
15034 );
15035 assert_hunk_revert(
15036 indoc! {r#"struct Row;
15037 struct Row1;
15038 struct Row33;
15039 «ˇ
15040 struct Row4;
15041 struct» Row5;
15042 «struct Row6;
15043 ˇ»
15044 struct Row99;
15045 struct Row9;
15046 struct Row10;"#},
15047 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15048 indoc! {r#"struct Row;
15049 struct Row1;
15050 struct Row33;
15051 «ˇ
15052 struct Row4;
15053 struct» Row5;
15054 «struct Row6;
15055 ˇ»
15056 struct Row99;
15057 struct Row9;
15058 struct Row10;"#},
15059 base_text,
15060 &mut cx,
15061 );
15062
15063 assert_hunk_revert(
15064 indoc! {r#"ˇstruct Row1.1;
15065 struct Row1;
15066 «ˇstr»uct Row22;
15067
15068 struct ˇRow44;
15069 struct Row5;
15070 struct «Rˇ»ow66;ˇ
15071
15072 «struˇ»ct Row88;
15073 struct Row9;
15074 struct Row1011;ˇ"#},
15075 vec![
15076 DiffHunkStatusKind::Modified,
15077 DiffHunkStatusKind::Modified,
15078 DiffHunkStatusKind::Modified,
15079 DiffHunkStatusKind::Modified,
15080 DiffHunkStatusKind::Modified,
15081 DiffHunkStatusKind::Modified,
15082 ],
15083 indoc! {r#"struct Row;
15084 ˇstruct Row1;
15085 struct Row2;
15086 ˇ
15087 struct Row4;
15088 ˇstruct Row5;
15089 struct Row6;
15090 ˇ
15091 struct Row8;
15092 ˇstruct Row9;
15093 struct Row10;ˇ"#},
15094 base_text,
15095 &mut cx,
15096 );
15097}
15098
15099#[gpui::test]
15100async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15101 init_test(cx, |_| {});
15102 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15103 let base_text = indoc! {r#"
15104 one
15105
15106 two
15107 three
15108 "#};
15109
15110 cx.set_head_text(base_text);
15111 cx.set_state("\nˇ\n");
15112 cx.executor().run_until_parked();
15113 cx.update_editor(|editor, _window, cx| {
15114 editor.expand_selected_diff_hunks(cx);
15115 });
15116 cx.executor().run_until_parked();
15117 cx.update_editor(|editor, window, cx| {
15118 editor.backspace(&Default::default(), window, cx);
15119 });
15120 cx.run_until_parked();
15121 cx.assert_state_with_diff(
15122 indoc! {r#"
15123
15124 - two
15125 - threeˇ
15126 +
15127 "#}
15128 .to_string(),
15129 );
15130}
15131
15132#[gpui::test]
15133async fn test_deletion_reverts(cx: &mut TestAppContext) {
15134 init_test(cx, |_| {});
15135 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15136 let base_text = indoc! {r#"struct Row;
15137struct Row1;
15138struct Row2;
15139
15140struct Row4;
15141struct Row5;
15142struct Row6;
15143
15144struct Row8;
15145struct Row9;
15146struct Row10;"#};
15147
15148 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15149 assert_hunk_revert(
15150 indoc! {r#"struct Row;
15151 struct Row2;
15152
15153 ˇstruct Row4;
15154 struct Row5;
15155 struct Row6;
15156 ˇ
15157 struct Row8;
15158 struct Row10;"#},
15159 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15160 indoc! {r#"struct Row;
15161 struct Row2;
15162
15163 ˇstruct Row4;
15164 struct Row5;
15165 struct Row6;
15166 ˇ
15167 struct Row8;
15168 struct Row10;"#},
15169 base_text,
15170 &mut cx,
15171 );
15172 assert_hunk_revert(
15173 indoc! {r#"struct Row;
15174 struct Row2;
15175
15176 «ˇstruct Row4;
15177 struct» Row5;
15178 «struct Row6;
15179 ˇ»
15180 struct Row8;
15181 struct Row10;"#},
15182 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15183 indoc! {r#"struct Row;
15184 struct Row2;
15185
15186 «ˇstruct Row4;
15187 struct» Row5;
15188 «struct Row6;
15189 ˇ»
15190 struct Row8;
15191 struct Row10;"#},
15192 base_text,
15193 &mut cx,
15194 );
15195
15196 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15197 assert_hunk_revert(
15198 indoc! {r#"struct Row;
15199 ˇstruct Row2;
15200
15201 struct Row4;
15202 struct Row5;
15203 struct Row6;
15204
15205 struct Row8;ˇ
15206 struct Row10;"#},
15207 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15208 indoc! {r#"struct Row;
15209 struct Row1;
15210 ˇstruct Row2;
15211
15212 struct Row4;
15213 struct Row5;
15214 struct Row6;
15215
15216 struct Row8;ˇ
15217 struct Row9;
15218 struct Row10;"#},
15219 base_text,
15220 &mut cx,
15221 );
15222 assert_hunk_revert(
15223 indoc! {r#"struct Row;
15224 struct Row2«ˇ;
15225 struct Row4;
15226 struct» Row5;
15227 «struct Row6;
15228
15229 struct Row8;ˇ»
15230 struct Row10;"#},
15231 vec![
15232 DiffHunkStatusKind::Deleted,
15233 DiffHunkStatusKind::Deleted,
15234 DiffHunkStatusKind::Deleted,
15235 ],
15236 indoc! {r#"struct Row;
15237 struct Row1;
15238 struct Row2«ˇ;
15239
15240 struct Row4;
15241 struct» Row5;
15242 «struct Row6;
15243
15244 struct Row8;ˇ»
15245 struct Row9;
15246 struct Row10;"#},
15247 base_text,
15248 &mut cx,
15249 );
15250}
15251
15252#[gpui::test]
15253async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15254 init_test(cx, |_| {});
15255
15256 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15257 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15258 let base_text_3 =
15259 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15260
15261 let text_1 = edit_first_char_of_every_line(base_text_1);
15262 let text_2 = edit_first_char_of_every_line(base_text_2);
15263 let text_3 = edit_first_char_of_every_line(base_text_3);
15264
15265 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15266 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15267 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15268
15269 let multibuffer = cx.new(|cx| {
15270 let mut multibuffer = MultiBuffer::new(ReadWrite);
15271 multibuffer.push_excerpts(
15272 buffer_1.clone(),
15273 [
15274 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15275 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15276 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15277 ],
15278 cx,
15279 );
15280 multibuffer.push_excerpts(
15281 buffer_2.clone(),
15282 [
15283 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15284 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15285 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15286 ],
15287 cx,
15288 );
15289 multibuffer.push_excerpts(
15290 buffer_3.clone(),
15291 [
15292 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15293 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15294 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15295 ],
15296 cx,
15297 );
15298 multibuffer
15299 });
15300
15301 let fs = FakeFs::new(cx.executor());
15302 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15303 let (editor, cx) = cx
15304 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15305 editor.update_in(cx, |editor, _window, cx| {
15306 for (buffer, diff_base) in [
15307 (buffer_1.clone(), base_text_1),
15308 (buffer_2.clone(), base_text_2),
15309 (buffer_3.clone(), base_text_3),
15310 ] {
15311 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15312 editor
15313 .buffer
15314 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15315 }
15316 });
15317 cx.executor().run_until_parked();
15318
15319 editor.update_in(cx, |editor, window, cx| {
15320 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}");
15321 editor.select_all(&SelectAll, window, cx);
15322 editor.git_restore(&Default::default(), window, cx);
15323 });
15324 cx.executor().run_until_parked();
15325
15326 // When all ranges are selected, all buffer hunks are reverted.
15327 editor.update(cx, |editor, cx| {
15328 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");
15329 });
15330 buffer_1.update(cx, |buffer, _| {
15331 assert_eq!(buffer.text(), base_text_1);
15332 });
15333 buffer_2.update(cx, |buffer, _| {
15334 assert_eq!(buffer.text(), base_text_2);
15335 });
15336 buffer_3.update(cx, |buffer, _| {
15337 assert_eq!(buffer.text(), base_text_3);
15338 });
15339
15340 editor.update_in(cx, |editor, window, cx| {
15341 editor.undo(&Default::default(), window, cx);
15342 });
15343
15344 editor.update_in(cx, |editor, window, cx| {
15345 editor.change_selections(None, window, cx, |s| {
15346 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15347 });
15348 editor.git_restore(&Default::default(), window, cx);
15349 });
15350
15351 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15352 // but not affect buffer_2 and its related excerpts.
15353 editor.update(cx, |editor, cx| {
15354 assert_eq!(
15355 editor.text(cx),
15356 "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}"
15357 );
15358 });
15359 buffer_1.update(cx, |buffer, _| {
15360 assert_eq!(buffer.text(), base_text_1);
15361 });
15362 buffer_2.update(cx, |buffer, _| {
15363 assert_eq!(
15364 buffer.text(),
15365 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15366 );
15367 });
15368 buffer_3.update(cx, |buffer, _| {
15369 assert_eq!(
15370 buffer.text(),
15371 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15372 );
15373 });
15374
15375 fn edit_first_char_of_every_line(text: &str) -> String {
15376 text.split('\n')
15377 .map(|line| format!("X{}", &line[1..]))
15378 .collect::<Vec<_>>()
15379 .join("\n")
15380 }
15381}
15382
15383#[gpui::test]
15384async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15385 init_test(cx, |_| {});
15386
15387 let cols = 4;
15388 let rows = 10;
15389 let sample_text_1 = sample_text(rows, cols, 'a');
15390 assert_eq!(
15391 sample_text_1,
15392 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15393 );
15394 let sample_text_2 = sample_text(rows, cols, 'l');
15395 assert_eq!(
15396 sample_text_2,
15397 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15398 );
15399 let sample_text_3 = sample_text(rows, cols, 'v');
15400 assert_eq!(
15401 sample_text_3,
15402 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15403 );
15404
15405 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15406 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15407 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15408
15409 let multi_buffer = cx.new(|cx| {
15410 let mut multibuffer = MultiBuffer::new(ReadWrite);
15411 multibuffer.push_excerpts(
15412 buffer_1.clone(),
15413 [
15414 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15415 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15416 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15417 ],
15418 cx,
15419 );
15420 multibuffer.push_excerpts(
15421 buffer_2.clone(),
15422 [
15423 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15424 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15425 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15426 ],
15427 cx,
15428 );
15429 multibuffer.push_excerpts(
15430 buffer_3.clone(),
15431 [
15432 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15433 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15434 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15435 ],
15436 cx,
15437 );
15438 multibuffer
15439 });
15440
15441 let fs = FakeFs::new(cx.executor());
15442 fs.insert_tree(
15443 "/a",
15444 json!({
15445 "main.rs": sample_text_1,
15446 "other.rs": sample_text_2,
15447 "lib.rs": sample_text_3,
15448 }),
15449 )
15450 .await;
15451 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15452 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15453 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15454 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15455 Editor::new(
15456 EditorMode::full(),
15457 multi_buffer,
15458 Some(project.clone()),
15459 window,
15460 cx,
15461 )
15462 });
15463 let multibuffer_item_id = workspace
15464 .update(cx, |workspace, window, cx| {
15465 assert!(
15466 workspace.active_item(cx).is_none(),
15467 "active item should be None before the first item is added"
15468 );
15469 workspace.add_item_to_active_pane(
15470 Box::new(multi_buffer_editor.clone()),
15471 None,
15472 true,
15473 window,
15474 cx,
15475 );
15476 let active_item = workspace
15477 .active_item(cx)
15478 .expect("should have an active item after adding the multi buffer");
15479 assert!(
15480 !active_item.is_singleton(cx),
15481 "A multi buffer was expected to active after adding"
15482 );
15483 active_item.item_id()
15484 })
15485 .unwrap();
15486 cx.executor().run_until_parked();
15487
15488 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15489 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15490 s.select_ranges(Some(1..2))
15491 });
15492 editor.open_excerpts(&OpenExcerpts, window, cx);
15493 });
15494 cx.executor().run_until_parked();
15495 let first_item_id = workspace
15496 .update(cx, |workspace, window, cx| {
15497 let active_item = workspace
15498 .active_item(cx)
15499 .expect("should have an active item after navigating into the 1st buffer");
15500 let first_item_id = active_item.item_id();
15501 assert_ne!(
15502 first_item_id, multibuffer_item_id,
15503 "Should navigate into the 1st buffer and activate it"
15504 );
15505 assert!(
15506 active_item.is_singleton(cx),
15507 "New active item should be a singleton buffer"
15508 );
15509 assert_eq!(
15510 active_item
15511 .act_as::<Editor>(cx)
15512 .expect("should have navigated into an editor for the 1st buffer")
15513 .read(cx)
15514 .text(cx),
15515 sample_text_1
15516 );
15517
15518 workspace
15519 .go_back(workspace.active_pane().downgrade(), window, cx)
15520 .detach_and_log_err(cx);
15521
15522 first_item_id
15523 })
15524 .unwrap();
15525 cx.executor().run_until_parked();
15526 workspace
15527 .update(cx, |workspace, _, cx| {
15528 let active_item = workspace
15529 .active_item(cx)
15530 .expect("should have an active item after navigating back");
15531 assert_eq!(
15532 active_item.item_id(),
15533 multibuffer_item_id,
15534 "Should navigate back to the multi buffer"
15535 );
15536 assert!(!active_item.is_singleton(cx));
15537 })
15538 .unwrap();
15539
15540 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15541 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15542 s.select_ranges(Some(39..40))
15543 });
15544 editor.open_excerpts(&OpenExcerpts, window, cx);
15545 });
15546 cx.executor().run_until_parked();
15547 let second_item_id = workspace
15548 .update(cx, |workspace, window, cx| {
15549 let active_item = workspace
15550 .active_item(cx)
15551 .expect("should have an active item after navigating into the 2nd buffer");
15552 let second_item_id = active_item.item_id();
15553 assert_ne!(
15554 second_item_id, multibuffer_item_id,
15555 "Should navigate away from the multibuffer"
15556 );
15557 assert_ne!(
15558 second_item_id, first_item_id,
15559 "Should navigate into the 2nd buffer and activate it"
15560 );
15561 assert!(
15562 active_item.is_singleton(cx),
15563 "New active item should be a singleton buffer"
15564 );
15565 assert_eq!(
15566 active_item
15567 .act_as::<Editor>(cx)
15568 .expect("should have navigated into an editor")
15569 .read(cx)
15570 .text(cx),
15571 sample_text_2
15572 );
15573
15574 workspace
15575 .go_back(workspace.active_pane().downgrade(), window, cx)
15576 .detach_and_log_err(cx);
15577
15578 second_item_id
15579 })
15580 .unwrap();
15581 cx.executor().run_until_parked();
15582 workspace
15583 .update(cx, |workspace, _, cx| {
15584 let active_item = workspace
15585 .active_item(cx)
15586 .expect("should have an active item after navigating back from the 2nd buffer");
15587 assert_eq!(
15588 active_item.item_id(),
15589 multibuffer_item_id,
15590 "Should navigate back from the 2nd buffer to the multi buffer"
15591 );
15592 assert!(!active_item.is_singleton(cx));
15593 })
15594 .unwrap();
15595
15596 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15597 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15598 s.select_ranges(Some(70..70))
15599 });
15600 editor.open_excerpts(&OpenExcerpts, window, cx);
15601 });
15602 cx.executor().run_until_parked();
15603 workspace
15604 .update(cx, |workspace, window, cx| {
15605 let active_item = workspace
15606 .active_item(cx)
15607 .expect("should have an active item after navigating into the 3rd buffer");
15608 let third_item_id = active_item.item_id();
15609 assert_ne!(
15610 third_item_id, multibuffer_item_id,
15611 "Should navigate into the 3rd buffer and activate it"
15612 );
15613 assert_ne!(third_item_id, first_item_id);
15614 assert_ne!(third_item_id, second_item_id);
15615 assert!(
15616 active_item.is_singleton(cx),
15617 "New active item should be a singleton buffer"
15618 );
15619 assert_eq!(
15620 active_item
15621 .act_as::<Editor>(cx)
15622 .expect("should have navigated into an editor")
15623 .read(cx)
15624 .text(cx),
15625 sample_text_3
15626 );
15627
15628 workspace
15629 .go_back(workspace.active_pane().downgrade(), window, cx)
15630 .detach_and_log_err(cx);
15631 })
15632 .unwrap();
15633 cx.executor().run_until_parked();
15634 workspace
15635 .update(cx, |workspace, _, cx| {
15636 let active_item = workspace
15637 .active_item(cx)
15638 .expect("should have an active item after navigating back from the 3rd buffer");
15639 assert_eq!(
15640 active_item.item_id(),
15641 multibuffer_item_id,
15642 "Should navigate back from the 3rd buffer to the multi buffer"
15643 );
15644 assert!(!active_item.is_singleton(cx));
15645 })
15646 .unwrap();
15647}
15648
15649#[gpui::test]
15650async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15651 init_test(cx, |_| {});
15652
15653 let mut cx = EditorTestContext::new(cx).await;
15654
15655 let diff_base = r#"
15656 use some::mod;
15657
15658 const A: u32 = 42;
15659
15660 fn main() {
15661 println!("hello");
15662
15663 println!("world");
15664 }
15665 "#
15666 .unindent();
15667
15668 cx.set_state(
15669 &r#"
15670 use some::modified;
15671
15672 ˇ
15673 fn main() {
15674 println!("hello there");
15675
15676 println!("around the");
15677 println!("world");
15678 }
15679 "#
15680 .unindent(),
15681 );
15682
15683 cx.set_head_text(&diff_base);
15684 executor.run_until_parked();
15685
15686 cx.update_editor(|editor, window, cx| {
15687 editor.go_to_next_hunk(&GoToHunk, window, cx);
15688 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15689 });
15690 executor.run_until_parked();
15691 cx.assert_state_with_diff(
15692 r#"
15693 use some::modified;
15694
15695
15696 fn main() {
15697 - println!("hello");
15698 + ˇ println!("hello there");
15699
15700 println!("around the");
15701 println!("world");
15702 }
15703 "#
15704 .unindent(),
15705 );
15706
15707 cx.update_editor(|editor, window, cx| {
15708 for _ in 0..2 {
15709 editor.go_to_next_hunk(&GoToHunk, window, cx);
15710 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15711 }
15712 });
15713 executor.run_until_parked();
15714 cx.assert_state_with_diff(
15715 r#"
15716 - use some::mod;
15717 + ˇuse some::modified;
15718
15719
15720 fn main() {
15721 - println!("hello");
15722 + println!("hello there");
15723
15724 + println!("around the");
15725 println!("world");
15726 }
15727 "#
15728 .unindent(),
15729 );
15730
15731 cx.update_editor(|editor, window, cx| {
15732 editor.go_to_next_hunk(&GoToHunk, window, cx);
15733 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15734 });
15735 executor.run_until_parked();
15736 cx.assert_state_with_diff(
15737 r#"
15738 - use some::mod;
15739 + use some::modified;
15740
15741 - const A: u32 = 42;
15742 ˇ
15743 fn main() {
15744 - println!("hello");
15745 + println!("hello there");
15746
15747 + println!("around the");
15748 println!("world");
15749 }
15750 "#
15751 .unindent(),
15752 );
15753
15754 cx.update_editor(|editor, window, cx| {
15755 editor.cancel(&Cancel, window, cx);
15756 });
15757
15758 cx.assert_state_with_diff(
15759 r#"
15760 use some::modified;
15761
15762 ˇ
15763 fn main() {
15764 println!("hello there");
15765
15766 println!("around the");
15767 println!("world");
15768 }
15769 "#
15770 .unindent(),
15771 );
15772}
15773
15774#[gpui::test]
15775async fn test_diff_base_change_with_expanded_diff_hunks(
15776 executor: BackgroundExecutor,
15777 cx: &mut TestAppContext,
15778) {
15779 init_test(cx, |_| {});
15780
15781 let mut cx = EditorTestContext::new(cx).await;
15782
15783 let diff_base = r#"
15784 use some::mod1;
15785 use some::mod2;
15786
15787 const A: u32 = 42;
15788 const B: u32 = 42;
15789 const C: u32 = 42;
15790
15791 fn main() {
15792 println!("hello");
15793
15794 println!("world");
15795 }
15796 "#
15797 .unindent();
15798
15799 cx.set_state(
15800 &r#"
15801 use some::mod2;
15802
15803 const A: u32 = 42;
15804 const C: u32 = 42;
15805
15806 fn main(ˇ) {
15807 //println!("hello");
15808
15809 println!("world");
15810 //
15811 //
15812 }
15813 "#
15814 .unindent(),
15815 );
15816
15817 cx.set_head_text(&diff_base);
15818 executor.run_until_parked();
15819
15820 cx.update_editor(|editor, window, cx| {
15821 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15822 });
15823 executor.run_until_parked();
15824 cx.assert_state_with_diff(
15825 r#"
15826 - use some::mod1;
15827 use some::mod2;
15828
15829 const A: u32 = 42;
15830 - const B: u32 = 42;
15831 const C: u32 = 42;
15832
15833 fn main(ˇ) {
15834 - println!("hello");
15835 + //println!("hello");
15836
15837 println!("world");
15838 + //
15839 + //
15840 }
15841 "#
15842 .unindent(),
15843 );
15844
15845 cx.set_head_text("new diff base!");
15846 executor.run_until_parked();
15847 cx.assert_state_with_diff(
15848 r#"
15849 - new diff base!
15850 + use some::mod2;
15851 +
15852 + const A: u32 = 42;
15853 + const C: u32 = 42;
15854 +
15855 + fn main(ˇ) {
15856 + //println!("hello");
15857 +
15858 + println!("world");
15859 + //
15860 + //
15861 + }
15862 "#
15863 .unindent(),
15864 );
15865}
15866
15867#[gpui::test]
15868async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15869 init_test(cx, |_| {});
15870
15871 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15872 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15873 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15874 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15875 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15876 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15877
15878 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15879 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15880 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15881
15882 let multi_buffer = cx.new(|cx| {
15883 let mut multibuffer = MultiBuffer::new(ReadWrite);
15884 multibuffer.push_excerpts(
15885 buffer_1.clone(),
15886 [
15887 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15888 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15889 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15890 ],
15891 cx,
15892 );
15893 multibuffer.push_excerpts(
15894 buffer_2.clone(),
15895 [
15896 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15897 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15898 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15899 ],
15900 cx,
15901 );
15902 multibuffer.push_excerpts(
15903 buffer_3.clone(),
15904 [
15905 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15906 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15907 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15908 ],
15909 cx,
15910 );
15911 multibuffer
15912 });
15913
15914 let editor =
15915 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15916 editor
15917 .update(cx, |editor, _window, cx| {
15918 for (buffer, diff_base) in [
15919 (buffer_1.clone(), file_1_old),
15920 (buffer_2.clone(), file_2_old),
15921 (buffer_3.clone(), file_3_old),
15922 ] {
15923 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15924 editor
15925 .buffer
15926 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15927 }
15928 })
15929 .unwrap();
15930
15931 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15932 cx.run_until_parked();
15933
15934 cx.assert_editor_state(
15935 &"
15936 ˇaaa
15937 ccc
15938 ddd
15939
15940 ggg
15941 hhh
15942
15943
15944 lll
15945 mmm
15946 NNN
15947
15948 qqq
15949 rrr
15950
15951 uuu
15952 111
15953 222
15954 333
15955
15956 666
15957 777
15958
15959 000
15960 !!!"
15961 .unindent(),
15962 );
15963
15964 cx.update_editor(|editor, window, cx| {
15965 editor.select_all(&SelectAll, window, cx);
15966 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15967 });
15968 cx.executor().run_until_parked();
15969
15970 cx.assert_state_with_diff(
15971 "
15972 «aaa
15973 - bbb
15974 ccc
15975 ddd
15976
15977 ggg
15978 hhh
15979
15980
15981 lll
15982 mmm
15983 - nnn
15984 + NNN
15985
15986 qqq
15987 rrr
15988
15989 uuu
15990 111
15991 222
15992 333
15993
15994 + 666
15995 777
15996
15997 000
15998 !!!ˇ»"
15999 .unindent(),
16000 );
16001}
16002
16003#[gpui::test]
16004async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
16005 init_test(cx, |_| {});
16006
16007 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
16008 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
16009
16010 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
16011 let multi_buffer = cx.new(|cx| {
16012 let mut multibuffer = MultiBuffer::new(ReadWrite);
16013 multibuffer.push_excerpts(
16014 buffer.clone(),
16015 [
16016 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16017 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16018 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16019 ],
16020 cx,
16021 );
16022 multibuffer
16023 });
16024
16025 let editor =
16026 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16027 editor
16028 .update(cx, |editor, _window, cx| {
16029 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16030 editor
16031 .buffer
16032 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16033 })
16034 .unwrap();
16035
16036 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16037 cx.run_until_parked();
16038
16039 cx.update_editor(|editor, window, cx| {
16040 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16041 });
16042 cx.executor().run_until_parked();
16043
16044 // When the start of a hunk coincides with the start of its excerpt,
16045 // the hunk is expanded. When the start of a a hunk is earlier than
16046 // the start of its excerpt, the hunk is not expanded.
16047 cx.assert_state_with_diff(
16048 "
16049 ˇaaa
16050 - bbb
16051 + BBB
16052
16053 - ddd
16054 - eee
16055 + DDD
16056 + EEE
16057 fff
16058
16059 iii
16060 "
16061 .unindent(),
16062 );
16063}
16064
16065#[gpui::test]
16066async fn test_edits_around_expanded_insertion_hunks(
16067 executor: BackgroundExecutor,
16068 cx: &mut TestAppContext,
16069) {
16070 init_test(cx, |_| {});
16071
16072 let mut cx = EditorTestContext::new(cx).await;
16073
16074 let diff_base = r#"
16075 use some::mod1;
16076 use some::mod2;
16077
16078 const A: u32 = 42;
16079
16080 fn main() {
16081 println!("hello");
16082
16083 println!("world");
16084 }
16085 "#
16086 .unindent();
16087 executor.run_until_parked();
16088 cx.set_state(
16089 &r#"
16090 use some::mod1;
16091 use some::mod2;
16092
16093 const A: u32 = 42;
16094 const B: u32 = 42;
16095 const C: u32 = 42;
16096 ˇ
16097
16098 fn main() {
16099 println!("hello");
16100
16101 println!("world");
16102 }
16103 "#
16104 .unindent(),
16105 );
16106
16107 cx.set_head_text(&diff_base);
16108 executor.run_until_parked();
16109
16110 cx.update_editor(|editor, window, cx| {
16111 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16112 });
16113 executor.run_until_parked();
16114
16115 cx.assert_state_with_diff(
16116 r#"
16117 use some::mod1;
16118 use some::mod2;
16119
16120 const A: u32 = 42;
16121 + const B: u32 = 42;
16122 + const C: u32 = 42;
16123 + ˇ
16124
16125 fn main() {
16126 println!("hello");
16127
16128 println!("world");
16129 }
16130 "#
16131 .unindent(),
16132 );
16133
16134 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16135 executor.run_until_parked();
16136
16137 cx.assert_state_with_diff(
16138 r#"
16139 use some::mod1;
16140 use some::mod2;
16141
16142 const A: u32 = 42;
16143 + const B: u32 = 42;
16144 + const C: u32 = 42;
16145 + const D: u32 = 42;
16146 + ˇ
16147
16148 fn main() {
16149 println!("hello");
16150
16151 println!("world");
16152 }
16153 "#
16154 .unindent(),
16155 );
16156
16157 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16158 executor.run_until_parked();
16159
16160 cx.assert_state_with_diff(
16161 r#"
16162 use some::mod1;
16163 use some::mod2;
16164
16165 const A: u32 = 42;
16166 + const B: u32 = 42;
16167 + const C: u32 = 42;
16168 + const D: u32 = 42;
16169 + const E: u32 = 42;
16170 + ˇ
16171
16172 fn main() {
16173 println!("hello");
16174
16175 println!("world");
16176 }
16177 "#
16178 .unindent(),
16179 );
16180
16181 cx.update_editor(|editor, window, cx| {
16182 editor.delete_line(&DeleteLine, window, cx);
16183 });
16184 executor.run_until_parked();
16185
16186 cx.assert_state_with_diff(
16187 r#"
16188 use some::mod1;
16189 use some::mod2;
16190
16191 const A: u32 = 42;
16192 + const B: u32 = 42;
16193 + const C: u32 = 42;
16194 + const D: u32 = 42;
16195 + const E: u32 = 42;
16196 ˇ
16197 fn main() {
16198 println!("hello");
16199
16200 println!("world");
16201 }
16202 "#
16203 .unindent(),
16204 );
16205
16206 cx.update_editor(|editor, window, cx| {
16207 editor.move_up(&MoveUp, window, cx);
16208 editor.delete_line(&DeleteLine, window, cx);
16209 editor.move_up(&MoveUp, window, cx);
16210 editor.delete_line(&DeleteLine, window, cx);
16211 editor.move_up(&MoveUp, window, cx);
16212 editor.delete_line(&DeleteLine, window, cx);
16213 });
16214 executor.run_until_parked();
16215 cx.assert_state_with_diff(
16216 r#"
16217 use some::mod1;
16218 use some::mod2;
16219
16220 const A: u32 = 42;
16221 + const B: u32 = 42;
16222 ˇ
16223 fn main() {
16224 println!("hello");
16225
16226 println!("world");
16227 }
16228 "#
16229 .unindent(),
16230 );
16231
16232 cx.update_editor(|editor, window, cx| {
16233 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16234 editor.delete_line(&DeleteLine, window, cx);
16235 });
16236 executor.run_until_parked();
16237 cx.assert_state_with_diff(
16238 r#"
16239 ˇ
16240 fn main() {
16241 println!("hello");
16242
16243 println!("world");
16244 }
16245 "#
16246 .unindent(),
16247 );
16248}
16249
16250#[gpui::test]
16251async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16252 init_test(cx, |_| {});
16253
16254 let mut cx = EditorTestContext::new(cx).await;
16255 cx.set_head_text(indoc! { "
16256 one
16257 two
16258 three
16259 four
16260 five
16261 "
16262 });
16263 cx.set_state(indoc! { "
16264 one
16265 ˇthree
16266 five
16267 "});
16268 cx.run_until_parked();
16269 cx.update_editor(|editor, window, cx| {
16270 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16271 });
16272 cx.assert_state_with_diff(
16273 indoc! { "
16274 one
16275 - two
16276 ˇthree
16277 - four
16278 five
16279 "}
16280 .to_string(),
16281 );
16282 cx.update_editor(|editor, window, cx| {
16283 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16284 });
16285
16286 cx.assert_state_with_diff(
16287 indoc! { "
16288 one
16289 ˇthree
16290 five
16291 "}
16292 .to_string(),
16293 );
16294
16295 cx.set_state(indoc! { "
16296 one
16297 ˇTWO
16298 three
16299 four
16300 five
16301 "});
16302 cx.run_until_parked();
16303 cx.update_editor(|editor, window, cx| {
16304 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16305 });
16306
16307 cx.assert_state_with_diff(
16308 indoc! { "
16309 one
16310 - two
16311 + ˇTWO
16312 three
16313 four
16314 five
16315 "}
16316 .to_string(),
16317 );
16318 cx.update_editor(|editor, window, cx| {
16319 editor.move_up(&Default::default(), window, cx);
16320 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16321 });
16322 cx.assert_state_with_diff(
16323 indoc! { "
16324 one
16325 ˇTWO
16326 three
16327 four
16328 five
16329 "}
16330 .to_string(),
16331 );
16332}
16333
16334#[gpui::test]
16335async fn test_edits_around_expanded_deletion_hunks(
16336 executor: BackgroundExecutor,
16337 cx: &mut TestAppContext,
16338) {
16339 init_test(cx, |_| {});
16340
16341 let mut cx = EditorTestContext::new(cx).await;
16342
16343 let diff_base = r#"
16344 use some::mod1;
16345 use some::mod2;
16346
16347 const A: u32 = 42;
16348 const B: u32 = 42;
16349 const C: u32 = 42;
16350
16351
16352 fn main() {
16353 println!("hello");
16354
16355 println!("world");
16356 }
16357 "#
16358 .unindent();
16359 executor.run_until_parked();
16360 cx.set_state(
16361 &r#"
16362 use some::mod1;
16363 use some::mod2;
16364
16365 ˇconst B: u32 = 42;
16366 const C: u32 = 42;
16367
16368
16369 fn main() {
16370 println!("hello");
16371
16372 println!("world");
16373 }
16374 "#
16375 .unindent(),
16376 );
16377
16378 cx.set_head_text(&diff_base);
16379 executor.run_until_parked();
16380
16381 cx.update_editor(|editor, window, cx| {
16382 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16383 });
16384 executor.run_until_parked();
16385
16386 cx.assert_state_with_diff(
16387 r#"
16388 use some::mod1;
16389 use some::mod2;
16390
16391 - const A: u32 = 42;
16392 ˇconst B: u32 = 42;
16393 const C: u32 = 42;
16394
16395
16396 fn main() {
16397 println!("hello");
16398
16399 println!("world");
16400 }
16401 "#
16402 .unindent(),
16403 );
16404
16405 cx.update_editor(|editor, window, cx| {
16406 editor.delete_line(&DeleteLine, window, cx);
16407 });
16408 executor.run_until_parked();
16409 cx.assert_state_with_diff(
16410 r#"
16411 use some::mod1;
16412 use some::mod2;
16413
16414 - const A: u32 = 42;
16415 - const B: u32 = 42;
16416 ˇconst C: u32 = 42;
16417
16418
16419 fn main() {
16420 println!("hello");
16421
16422 println!("world");
16423 }
16424 "#
16425 .unindent(),
16426 );
16427
16428 cx.update_editor(|editor, window, cx| {
16429 editor.delete_line(&DeleteLine, window, cx);
16430 });
16431 executor.run_until_parked();
16432 cx.assert_state_with_diff(
16433 r#"
16434 use some::mod1;
16435 use some::mod2;
16436
16437 - const A: u32 = 42;
16438 - const B: u32 = 42;
16439 - const C: u32 = 42;
16440 ˇ
16441
16442 fn main() {
16443 println!("hello");
16444
16445 println!("world");
16446 }
16447 "#
16448 .unindent(),
16449 );
16450
16451 cx.update_editor(|editor, window, cx| {
16452 editor.handle_input("replacement", window, cx);
16453 });
16454 executor.run_until_parked();
16455 cx.assert_state_with_diff(
16456 r#"
16457 use some::mod1;
16458 use some::mod2;
16459
16460 - const A: u32 = 42;
16461 - const B: u32 = 42;
16462 - const C: u32 = 42;
16463 -
16464 + replacementˇ
16465
16466 fn main() {
16467 println!("hello");
16468
16469 println!("world");
16470 }
16471 "#
16472 .unindent(),
16473 );
16474}
16475
16476#[gpui::test]
16477async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16478 init_test(cx, |_| {});
16479
16480 let mut cx = EditorTestContext::new(cx).await;
16481
16482 let base_text = r#"
16483 one
16484 two
16485 three
16486 four
16487 five
16488 "#
16489 .unindent();
16490 executor.run_until_parked();
16491 cx.set_state(
16492 &r#"
16493 one
16494 two
16495 fˇour
16496 five
16497 "#
16498 .unindent(),
16499 );
16500
16501 cx.set_head_text(&base_text);
16502 executor.run_until_parked();
16503
16504 cx.update_editor(|editor, window, cx| {
16505 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16506 });
16507 executor.run_until_parked();
16508
16509 cx.assert_state_with_diff(
16510 r#"
16511 one
16512 two
16513 - three
16514 fˇour
16515 five
16516 "#
16517 .unindent(),
16518 );
16519
16520 cx.update_editor(|editor, window, cx| {
16521 editor.backspace(&Backspace, window, cx);
16522 editor.backspace(&Backspace, window, cx);
16523 });
16524 executor.run_until_parked();
16525 cx.assert_state_with_diff(
16526 r#"
16527 one
16528 two
16529 - threeˇ
16530 - four
16531 + our
16532 five
16533 "#
16534 .unindent(),
16535 );
16536}
16537
16538#[gpui::test]
16539async fn test_edit_after_expanded_modification_hunk(
16540 executor: BackgroundExecutor,
16541 cx: &mut TestAppContext,
16542) {
16543 init_test(cx, |_| {});
16544
16545 let mut cx = EditorTestContext::new(cx).await;
16546
16547 let diff_base = r#"
16548 use some::mod1;
16549 use some::mod2;
16550
16551 const A: u32 = 42;
16552 const B: u32 = 42;
16553 const C: u32 = 42;
16554 const D: u32 = 42;
16555
16556
16557 fn main() {
16558 println!("hello");
16559
16560 println!("world");
16561 }"#
16562 .unindent();
16563
16564 cx.set_state(
16565 &r#"
16566 use some::mod1;
16567 use some::mod2;
16568
16569 const A: u32 = 42;
16570 const B: u32 = 42;
16571 const C: u32 = 43ˇ
16572 const D: u32 = 42;
16573
16574
16575 fn main() {
16576 println!("hello");
16577
16578 println!("world");
16579 }"#
16580 .unindent(),
16581 );
16582
16583 cx.set_head_text(&diff_base);
16584 executor.run_until_parked();
16585 cx.update_editor(|editor, window, cx| {
16586 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16587 });
16588 executor.run_until_parked();
16589
16590 cx.assert_state_with_diff(
16591 r#"
16592 use some::mod1;
16593 use some::mod2;
16594
16595 const A: u32 = 42;
16596 const B: u32 = 42;
16597 - const C: u32 = 42;
16598 + const C: u32 = 43ˇ
16599 const D: u32 = 42;
16600
16601
16602 fn main() {
16603 println!("hello");
16604
16605 println!("world");
16606 }"#
16607 .unindent(),
16608 );
16609
16610 cx.update_editor(|editor, window, cx| {
16611 editor.handle_input("\nnew_line\n", window, cx);
16612 });
16613 executor.run_until_parked();
16614
16615 cx.assert_state_with_diff(
16616 r#"
16617 use some::mod1;
16618 use some::mod2;
16619
16620 const A: u32 = 42;
16621 const B: u32 = 42;
16622 - const C: u32 = 42;
16623 + const C: u32 = 43
16624 + new_line
16625 + ˇ
16626 const D: u32 = 42;
16627
16628
16629 fn main() {
16630 println!("hello");
16631
16632 println!("world");
16633 }"#
16634 .unindent(),
16635 );
16636}
16637
16638#[gpui::test]
16639async fn test_stage_and_unstage_added_file_hunk(
16640 executor: BackgroundExecutor,
16641 cx: &mut TestAppContext,
16642) {
16643 init_test(cx, |_| {});
16644
16645 let mut cx = EditorTestContext::new(cx).await;
16646 cx.update_editor(|editor, _, cx| {
16647 editor.set_expand_all_diff_hunks(cx);
16648 });
16649
16650 let working_copy = r#"
16651 ˇfn main() {
16652 println!("hello, world!");
16653 }
16654 "#
16655 .unindent();
16656
16657 cx.set_state(&working_copy);
16658 executor.run_until_parked();
16659
16660 cx.assert_state_with_diff(
16661 r#"
16662 + ˇfn main() {
16663 + println!("hello, world!");
16664 + }
16665 "#
16666 .unindent(),
16667 );
16668 cx.assert_index_text(None);
16669
16670 cx.update_editor(|editor, window, cx| {
16671 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16672 });
16673 executor.run_until_parked();
16674 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16675 cx.assert_state_with_diff(
16676 r#"
16677 + ˇfn main() {
16678 + println!("hello, world!");
16679 + }
16680 "#
16681 .unindent(),
16682 );
16683
16684 cx.update_editor(|editor, window, cx| {
16685 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16686 });
16687 executor.run_until_parked();
16688 cx.assert_index_text(None);
16689}
16690
16691async fn setup_indent_guides_editor(
16692 text: &str,
16693 cx: &mut TestAppContext,
16694) -> (BufferId, EditorTestContext) {
16695 init_test(cx, |_| {});
16696
16697 let mut cx = EditorTestContext::new(cx).await;
16698
16699 let buffer_id = cx.update_editor(|editor, window, cx| {
16700 editor.set_text(text, window, cx);
16701 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16702
16703 buffer_ids[0]
16704 });
16705
16706 (buffer_id, cx)
16707}
16708
16709fn assert_indent_guides(
16710 range: Range<u32>,
16711 expected: Vec<IndentGuide>,
16712 active_indices: Option<Vec<usize>>,
16713 cx: &mut EditorTestContext,
16714) {
16715 let indent_guides = cx.update_editor(|editor, window, cx| {
16716 let snapshot = editor.snapshot(window, cx).display_snapshot;
16717 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16718 editor,
16719 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16720 true,
16721 &snapshot,
16722 cx,
16723 );
16724
16725 indent_guides.sort_by(|a, b| {
16726 a.depth.cmp(&b.depth).then(
16727 a.start_row
16728 .cmp(&b.start_row)
16729 .then(a.end_row.cmp(&b.end_row)),
16730 )
16731 });
16732 indent_guides
16733 });
16734
16735 if let Some(expected) = active_indices {
16736 let active_indices = cx.update_editor(|editor, window, cx| {
16737 let snapshot = editor.snapshot(window, cx).display_snapshot;
16738 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16739 });
16740
16741 assert_eq!(
16742 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16743 expected,
16744 "Active indent guide indices do not match"
16745 );
16746 }
16747
16748 assert_eq!(indent_guides, expected, "Indent guides do not match");
16749}
16750
16751fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16752 IndentGuide {
16753 buffer_id,
16754 start_row: MultiBufferRow(start_row),
16755 end_row: MultiBufferRow(end_row),
16756 depth,
16757 tab_size: 4,
16758 settings: IndentGuideSettings {
16759 enabled: true,
16760 line_width: 1,
16761 active_line_width: 1,
16762 ..Default::default()
16763 },
16764 }
16765}
16766
16767#[gpui::test]
16768async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16769 let (buffer_id, mut cx) = setup_indent_guides_editor(
16770 &"
16771 fn main() {
16772 let a = 1;
16773 }"
16774 .unindent(),
16775 cx,
16776 )
16777 .await;
16778
16779 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16780}
16781
16782#[gpui::test]
16783async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16784 let (buffer_id, mut cx) = setup_indent_guides_editor(
16785 &"
16786 fn main() {
16787 let a = 1;
16788 let b = 2;
16789 }"
16790 .unindent(),
16791 cx,
16792 )
16793 .await;
16794
16795 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16796}
16797
16798#[gpui::test]
16799async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16800 let (buffer_id, mut cx) = setup_indent_guides_editor(
16801 &"
16802 fn main() {
16803 let a = 1;
16804 if a == 3 {
16805 let b = 2;
16806 } else {
16807 let c = 3;
16808 }
16809 }"
16810 .unindent(),
16811 cx,
16812 )
16813 .await;
16814
16815 assert_indent_guides(
16816 0..8,
16817 vec![
16818 indent_guide(buffer_id, 1, 6, 0),
16819 indent_guide(buffer_id, 3, 3, 1),
16820 indent_guide(buffer_id, 5, 5, 1),
16821 ],
16822 None,
16823 &mut cx,
16824 );
16825}
16826
16827#[gpui::test]
16828async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16829 let (buffer_id, mut cx) = setup_indent_guides_editor(
16830 &"
16831 fn main() {
16832 let a = 1;
16833 let b = 2;
16834 let c = 3;
16835 }"
16836 .unindent(),
16837 cx,
16838 )
16839 .await;
16840
16841 assert_indent_guides(
16842 0..5,
16843 vec![
16844 indent_guide(buffer_id, 1, 3, 0),
16845 indent_guide(buffer_id, 2, 2, 1),
16846 ],
16847 None,
16848 &mut cx,
16849 );
16850}
16851
16852#[gpui::test]
16853async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16854 let (buffer_id, mut cx) = setup_indent_guides_editor(
16855 &"
16856 fn main() {
16857 let a = 1;
16858
16859 let c = 3;
16860 }"
16861 .unindent(),
16862 cx,
16863 )
16864 .await;
16865
16866 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16867}
16868
16869#[gpui::test]
16870async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16871 let (buffer_id, mut cx) = setup_indent_guides_editor(
16872 &"
16873 fn main() {
16874 let a = 1;
16875
16876 let c = 3;
16877
16878 if a == 3 {
16879 let b = 2;
16880 } else {
16881 let c = 3;
16882 }
16883 }"
16884 .unindent(),
16885 cx,
16886 )
16887 .await;
16888
16889 assert_indent_guides(
16890 0..11,
16891 vec![
16892 indent_guide(buffer_id, 1, 9, 0),
16893 indent_guide(buffer_id, 6, 6, 1),
16894 indent_guide(buffer_id, 8, 8, 1),
16895 ],
16896 None,
16897 &mut cx,
16898 );
16899}
16900
16901#[gpui::test]
16902async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16903 let (buffer_id, mut cx) = setup_indent_guides_editor(
16904 &"
16905 fn main() {
16906 let a = 1;
16907
16908 let c = 3;
16909
16910 if a == 3 {
16911 let b = 2;
16912 } else {
16913 let c = 3;
16914 }
16915 }"
16916 .unindent(),
16917 cx,
16918 )
16919 .await;
16920
16921 assert_indent_guides(
16922 1..11,
16923 vec![
16924 indent_guide(buffer_id, 1, 9, 0),
16925 indent_guide(buffer_id, 6, 6, 1),
16926 indent_guide(buffer_id, 8, 8, 1),
16927 ],
16928 None,
16929 &mut cx,
16930 );
16931}
16932
16933#[gpui::test]
16934async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16935 let (buffer_id, mut cx) = setup_indent_guides_editor(
16936 &"
16937 fn main() {
16938 let a = 1;
16939
16940 let c = 3;
16941
16942 if a == 3 {
16943 let b = 2;
16944 } else {
16945 let c = 3;
16946 }
16947 }"
16948 .unindent(),
16949 cx,
16950 )
16951 .await;
16952
16953 assert_indent_guides(
16954 1..10,
16955 vec![
16956 indent_guide(buffer_id, 1, 9, 0),
16957 indent_guide(buffer_id, 6, 6, 1),
16958 indent_guide(buffer_id, 8, 8, 1),
16959 ],
16960 None,
16961 &mut cx,
16962 );
16963}
16964
16965#[gpui::test]
16966async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
16967 let (buffer_id, mut cx) = setup_indent_guides_editor(
16968 &"
16969 fn main() {
16970 if a {
16971 b(
16972 c,
16973 d,
16974 )
16975 } else {
16976 e(
16977 f
16978 )
16979 }
16980 }"
16981 .unindent(),
16982 cx,
16983 )
16984 .await;
16985
16986 assert_indent_guides(
16987 0..11,
16988 vec![
16989 indent_guide(buffer_id, 1, 10, 0),
16990 indent_guide(buffer_id, 2, 5, 1),
16991 indent_guide(buffer_id, 7, 9, 1),
16992 indent_guide(buffer_id, 3, 4, 2),
16993 indent_guide(buffer_id, 8, 8, 2),
16994 ],
16995 None,
16996 &mut cx,
16997 );
16998
16999 cx.update_editor(|editor, window, cx| {
17000 editor.fold_at(MultiBufferRow(2), window, cx);
17001 assert_eq!(
17002 editor.display_text(cx),
17003 "
17004 fn main() {
17005 if a {
17006 b(⋯
17007 )
17008 } else {
17009 e(
17010 f
17011 )
17012 }
17013 }"
17014 .unindent()
17015 );
17016 });
17017
17018 assert_indent_guides(
17019 0..11,
17020 vec![
17021 indent_guide(buffer_id, 1, 10, 0),
17022 indent_guide(buffer_id, 2, 5, 1),
17023 indent_guide(buffer_id, 7, 9, 1),
17024 indent_guide(buffer_id, 8, 8, 2),
17025 ],
17026 None,
17027 &mut cx,
17028 );
17029}
17030
17031#[gpui::test]
17032async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
17033 let (buffer_id, mut cx) = setup_indent_guides_editor(
17034 &"
17035 block1
17036 block2
17037 block3
17038 block4
17039 block2
17040 block1
17041 block1"
17042 .unindent(),
17043 cx,
17044 )
17045 .await;
17046
17047 assert_indent_guides(
17048 1..10,
17049 vec![
17050 indent_guide(buffer_id, 1, 4, 0),
17051 indent_guide(buffer_id, 2, 3, 1),
17052 indent_guide(buffer_id, 3, 3, 2),
17053 ],
17054 None,
17055 &mut cx,
17056 );
17057}
17058
17059#[gpui::test]
17060async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
17061 let (buffer_id, mut cx) = setup_indent_guides_editor(
17062 &"
17063 block1
17064 block2
17065 block3
17066
17067 block1
17068 block1"
17069 .unindent(),
17070 cx,
17071 )
17072 .await;
17073
17074 assert_indent_guides(
17075 0..6,
17076 vec![
17077 indent_guide(buffer_id, 1, 2, 0),
17078 indent_guide(buffer_id, 2, 2, 1),
17079 ],
17080 None,
17081 &mut cx,
17082 );
17083}
17084
17085#[gpui::test]
17086async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17087 let (buffer_id, mut cx) = setup_indent_guides_editor(
17088 &"
17089 block1
17090
17091
17092
17093 block2
17094 "
17095 .unindent(),
17096 cx,
17097 )
17098 .await;
17099
17100 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17101}
17102
17103#[gpui::test]
17104async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17105 let (buffer_id, mut cx) = setup_indent_guides_editor(
17106 &"
17107 def a:
17108 \tb = 3
17109 \tif True:
17110 \t\tc = 4
17111 \t\td = 5
17112 \tprint(b)
17113 "
17114 .unindent(),
17115 cx,
17116 )
17117 .await;
17118
17119 assert_indent_guides(
17120 0..6,
17121 vec![
17122 indent_guide(buffer_id, 1, 5, 0),
17123 indent_guide(buffer_id, 3, 4, 1),
17124 ],
17125 None,
17126 &mut cx,
17127 );
17128}
17129
17130#[gpui::test]
17131async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17132 let (buffer_id, mut cx) = setup_indent_guides_editor(
17133 &"
17134 fn main() {
17135 let a = 1;
17136 }"
17137 .unindent(),
17138 cx,
17139 )
17140 .await;
17141
17142 cx.update_editor(|editor, window, cx| {
17143 editor.change_selections(None, window, cx, |s| {
17144 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17145 });
17146 });
17147
17148 assert_indent_guides(
17149 0..3,
17150 vec![indent_guide(buffer_id, 1, 1, 0)],
17151 Some(vec![0]),
17152 &mut cx,
17153 );
17154}
17155
17156#[gpui::test]
17157async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17158 let (buffer_id, mut cx) = setup_indent_guides_editor(
17159 &"
17160 fn main() {
17161 if 1 == 2 {
17162 let a = 1;
17163 }
17164 }"
17165 .unindent(),
17166 cx,
17167 )
17168 .await;
17169
17170 cx.update_editor(|editor, window, cx| {
17171 editor.change_selections(None, window, cx, |s| {
17172 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17173 });
17174 });
17175
17176 assert_indent_guides(
17177 0..4,
17178 vec![
17179 indent_guide(buffer_id, 1, 3, 0),
17180 indent_guide(buffer_id, 2, 2, 1),
17181 ],
17182 Some(vec![1]),
17183 &mut cx,
17184 );
17185
17186 cx.update_editor(|editor, window, cx| {
17187 editor.change_selections(None, window, cx, |s| {
17188 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17189 });
17190 });
17191
17192 assert_indent_guides(
17193 0..4,
17194 vec![
17195 indent_guide(buffer_id, 1, 3, 0),
17196 indent_guide(buffer_id, 2, 2, 1),
17197 ],
17198 Some(vec![1]),
17199 &mut cx,
17200 );
17201
17202 cx.update_editor(|editor, window, cx| {
17203 editor.change_selections(None, window, cx, |s| {
17204 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17205 });
17206 });
17207
17208 assert_indent_guides(
17209 0..4,
17210 vec![
17211 indent_guide(buffer_id, 1, 3, 0),
17212 indent_guide(buffer_id, 2, 2, 1),
17213 ],
17214 Some(vec![0]),
17215 &mut cx,
17216 );
17217}
17218
17219#[gpui::test]
17220async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17221 let (buffer_id, mut cx) = setup_indent_guides_editor(
17222 &"
17223 fn main() {
17224 let a = 1;
17225
17226 let b = 2;
17227 }"
17228 .unindent(),
17229 cx,
17230 )
17231 .await;
17232
17233 cx.update_editor(|editor, window, cx| {
17234 editor.change_selections(None, window, cx, |s| {
17235 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17236 });
17237 });
17238
17239 assert_indent_guides(
17240 0..5,
17241 vec![indent_guide(buffer_id, 1, 3, 0)],
17242 Some(vec![0]),
17243 &mut cx,
17244 );
17245}
17246
17247#[gpui::test]
17248async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17249 let (buffer_id, mut cx) = setup_indent_guides_editor(
17250 &"
17251 def m:
17252 a = 1
17253 pass"
17254 .unindent(),
17255 cx,
17256 )
17257 .await;
17258
17259 cx.update_editor(|editor, window, cx| {
17260 editor.change_selections(None, window, cx, |s| {
17261 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17262 });
17263 });
17264
17265 assert_indent_guides(
17266 0..3,
17267 vec![indent_guide(buffer_id, 1, 2, 0)],
17268 Some(vec![0]),
17269 &mut cx,
17270 );
17271}
17272
17273#[gpui::test]
17274async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17275 init_test(cx, |_| {});
17276 let mut cx = EditorTestContext::new(cx).await;
17277 let text = indoc! {
17278 "
17279 impl A {
17280 fn b() {
17281 0;
17282 3;
17283 5;
17284 6;
17285 7;
17286 }
17287 }
17288 "
17289 };
17290 let base_text = indoc! {
17291 "
17292 impl A {
17293 fn b() {
17294 0;
17295 1;
17296 2;
17297 3;
17298 4;
17299 }
17300 fn c() {
17301 5;
17302 6;
17303 7;
17304 }
17305 }
17306 "
17307 };
17308
17309 cx.update_editor(|editor, window, cx| {
17310 editor.set_text(text, window, cx);
17311
17312 editor.buffer().update(cx, |multibuffer, cx| {
17313 let buffer = multibuffer.as_singleton().unwrap();
17314 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17315
17316 multibuffer.set_all_diff_hunks_expanded(cx);
17317 multibuffer.add_diff(diff, cx);
17318
17319 buffer.read(cx).remote_id()
17320 })
17321 });
17322 cx.run_until_parked();
17323
17324 cx.assert_state_with_diff(
17325 indoc! { "
17326 impl A {
17327 fn b() {
17328 0;
17329 - 1;
17330 - 2;
17331 3;
17332 - 4;
17333 - }
17334 - fn c() {
17335 5;
17336 6;
17337 7;
17338 }
17339 }
17340 ˇ"
17341 }
17342 .to_string(),
17343 );
17344
17345 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17346 editor
17347 .snapshot(window, cx)
17348 .buffer_snapshot
17349 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17350 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17351 .collect::<Vec<_>>()
17352 });
17353 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17354 assert_eq!(
17355 actual_guides,
17356 vec![
17357 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17358 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17359 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17360 ]
17361 );
17362}
17363
17364#[gpui::test]
17365async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17366 init_test(cx, |_| {});
17367 let mut cx = EditorTestContext::new(cx).await;
17368
17369 let diff_base = r#"
17370 a
17371 b
17372 c
17373 "#
17374 .unindent();
17375
17376 cx.set_state(
17377 &r#"
17378 ˇA
17379 b
17380 C
17381 "#
17382 .unindent(),
17383 );
17384 cx.set_head_text(&diff_base);
17385 cx.update_editor(|editor, window, cx| {
17386 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17387 });
17388 executor.run_until_parked();
17389
17390 let both_hunks_expanded = r#"
17391 - a
17392 + ˇA
17393 b
17394 - c
17395 + C
17396 "#
17397 .unindent();
17398
17399 cx.assert_state_with_diff(both_hunks_expanded.clone());
17400
17401 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17402 let snapshot = editor.snapshot(window, cx);
17403 let hunks = editor
17404 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17405 .collect::<Vec<_>>();
17406 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17407 let buffer_id = hunks[0].buffer_id;
17408 hunks
17409 .into_iter()
17410 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17411 .collect::<Vec<_>>()
17412 });
17413 assert_eq!(hunk_ranges.len(), 2);
17414
17415 cx.update_editor(|editor, _, cx| {
17416 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17417 });
17418 executor.run_until_parked();
17419
17420 let second_hunk_expanded = r#"
17421 ˇA
17422 b
17423 - c
17424 + C
17425 "#
17426 .unindent();
17427
17428 cx.assert_state_with_diff(second_hunk_expanded);
17429
17430 cx.update_editor(|editor, _, cx| {
17431 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17432 });
17433 executor.run_until_parked();
17434
17435 cx.assert_state_with_diff(both_hunks_expanded.clone());
17436
17437 cx.update_editor(|editor, _, cx| {
17438 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17439 });
17440 executor.run_until_parked();
17441
17442 let first_hunk_expanded = r#"
17443 - a
17444 + ˇA
17445 b
17446 C
17447 "#
17448 .unindent();
17449
17450 cx.assert_state_with_diff(first_hunk_expanded);
17451
17452 cx.update_editor(|editor, _, cx| {
17453 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17454 });
17455 executor.run_until_parked();
17456
17457 cx.assert_state_with_diff(both_hunks_expanded);
17458
17459 cx.set_state(
17460 &r#"
17461 ˇA
17462 b
17463 "#
17464 .unindent(),
17465 );
17466 cx.run_until_parked();
17467
17468 // TODO this cursor position seems bad
17469 cx.assert_state_with_diff(
17470 r#"
17471 - ˇa
17472 + A
17473 b
17474 "#
17475 .unindent(),
17476 );
17477
17478 cx.update_editor(|editor, window, cx| {
17479 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17480 });
17481
17482 cx.assert_state_with_diff(
17483 r#"
17484 - ˇa
17485 + A
17486 b
17487 - c
17488 "#
17489 .unindent(),
17490 );
17491
17492 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17493 let snapshot = editor.snapshot(window, cx);
17494 let hunks = editor
17495 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17496 .collect::<Vec<_>>();
17497 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17498 let buffer_id = hunks[0].buffer_id;
17499 hunks
17500 .into_iter()
17501 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17502 .collect::<Vec<_>>()
17503 });
17504 assert_eq!(hunk_ranges.len(), 2);
17505
17506 cx.update_editor(|editor, _, cx| {
17507 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17508 });
17509 executor.run_until_parked();
17510
17511 cx.assert_state_with_diff(
17512 r#"
17513 - ˇa
17514 + A
17515 b
17516 "#
17517 .unindent(),
17518 );
17519}
17520
17521#[gpui::test]
17522async fn test_toggle_deletion_hunk_at_start_of_file(
17523 executor: BackgroundExecutor,
17524 cx: &mut TestAppContext,
17525) {
17526 init_test(cx, |_| {});
17527 let mut cx = EditorTestContext::new(cx).await;
17528
17529 let diff_base = r#"
17530 a
17531 b
17532 c
17533 "#
17534 .unindent();
17535
17536 cx.set_state(
17537 &r#"
17538 ˇb
17539 c
17540 "#
17541 .unindent(),
17542 );
17543 cx.set_head_text(&diff_base);
17544 cx.update_editor(|editor, window, cx| {
17545 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17546 });
17547 executor.run_until_parked();
17548
17549 let hunk_expanded = r#"
17550 - a
17551 ˇb
17552 c
17553 "#
17554 .unindent();
17555
17556 cx.assert_state_with_diff(hunk_expanded.clone());
17557
17558 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17559 let snapshot = editor.snapshot(window, cx);
17560 let hunks = editor
17561 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17562 .collect::<Vec<_>>();
17563 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17564 let buffer_id = hunks[0].buffer_id;
17565 hunks
17566 .into_iter()
17567 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17568 .collect::<Vec<_>>()
17569 });
17570 assert_eq!(hunk_ranges.len(), 1);
17571
17572 cx.update_editor(|editor, _, cx| {
17573 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17574 });
17575 executor.run_until_parked();
17576
17577 let hunk_collapsed = r#"
17578 ˇb
17579 c
17580 "#
17581 .unindent();
17582
17583 cx.assert_state_with_diff(hunk_collapsed);
17584
17585 cx.update_editor(|editor, _, cx| {
17586 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17587 });
17588 executor.run_until_parked();
17589
17590 cx.assert_state_with_diff(hunk_expanded.clone());
17591}
17592
17593#[gpui::test]
17594async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17595 init_test(cx, |_| {});
17596
17597 let fs = FakeFs::new(cx.executor());
17598 fs.insert_tree(
17599 path!("/test"),
17600 json!({
17601 ".git": {},
17602 "file-1": "ONE\n",
17603 "file-2": "TWO\n",
17604 "file-3": "THREE\n",
17605 }),
17606 )
17607 .await;
17608
17609 fs.set_head_for_repo(
17610 path!("/test/.git").as_ref(),
17611 &[
17612 ("file-1".into(), "one\n".into()),
17613 ("file-2".into(), "two\n".into()),
17614 ("file-3".into(), "three\n".into()),
17615 ],
17616 );
17617
17618 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17619 let mut buffers = vec![];
17620 for i in 1..=3 {
17621 let buffer = project
17622 .update(cx, |project, cx| {
17623 let path = format!(path!("/test/file-{}"), i);
17624 project.open_local_buffer(path, cx)
17625 })
17626 .await
17627 .unwrap();
17628 buffers.push(buffer);
17629 }
17630
17631 let multibuffer = cx.new(|cx| {
17632 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17633 multibuffer.set_all_diff_hunks_expanded(cx);
17634 for buffer in &buffers {
17635 let snapshot = buffer.read(cx).snapshot();
17636 multibuffer.set_excerpts_for_path(
17637 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17638 buffer.clone(),
17639 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17640 DEFAULT_MULTIBUFFER_CONTEXT,
17641 cx,
17642 );
17643 }
17644 multibuffer
17645 });
17646
17647 let editor = cx.add_window(|window, cx| {
17648 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17649 });
17650 cx.run_until_parked();
17651
17652 let snapshot = editor
17653 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17654 .unwrap();
17655 let hunks = snapshot
17656 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17657 .map(|hunk| match hunk {
17658 DisplayDiffHunk::Unfolded {
17659 display_row_range, ..
17660 } => display_row_range,
17661 DisplayDiffHunk::Folded { .. } => unreachable!(),
17662 })
17663 .collect::<Vec<_>>();
17664 assert_eq!(
17665 hunks,
17666 [
17667 DisplayRow(2)..DisplayRow(4),
17668 DisplayRow(7)..DisplayRow(9),
17669 DisplayRow(12)..DisplayRow(14),
17670 ]
17671 );
17672}
17673
17674#[gpui::test]
17675async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17676 init_test(cx, |_| {});
17677
17678 let mut cx = EditorTestContext::new(cx).await;
17679 cx.set_head_text(indoc! { "
17680 one
17681 two
17682 three
17683 four
17684 five
17685 "
17686 });
17687 cx.set_index_text(indoc! { "
17688 one
17689 two
17690 three
17691 four
17692 five
17693 "
17694 });
17695 cx.set_state(indoc! {"
17696 one
17697 TWO
17698 ˇTHREE
17699 FOUR
17700 five
17701 "});
17702 cx.run_until_parked();
17703 cx.update_editor(|editor, window, cx| {
17704 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17705 });
17706 cx.run_until_parked();
17707 cx.assert_index_text(Some(indoc! {"
17708 one
17709 TWO
17710 THREE
17711 FOUR
17712 five
17713 "}));
17714 cx.set_state(indoc! { "
17715 one
17716 TWO
17717 ˇTHREE-HUNDRED
17718 FOUR
17719 five
17720 "});
17721 cx.run_until_parked();
17722 cx.update_editor(|editor, window, cx| {
17723 let snapshot = editor.snapshot(window, cx);
17724 let hunks = editor
17725 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17726 .collect::<Vec<_>>();
17727 assert_eq!(hunks.len(), 1);
17728 assert_eq!(
17729 hunks[0].status(),
17730 DiffHunkStatus {
17731 kind: DiffHunkStatusKind::Modified,
17732 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17733 }
17734 );
17735
17736 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17737 });
17738 cx.run_until_parked();
17739 cx.assert_index_text(Some(indoc! {"
17740 one
17741 TWO
17742 THREE-HUNDRED
17743 FOUR
17744 five
17745 "}));
17746}
17747
17748#[gpui::test]
17749fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17750 init_test(cx, |_| {});
17751
17752 let editor = cx.add_window(|window, cx| {
17753 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17754 build_editor(buffer, window, cx)
17755 });
17756
17757 let render_args = Arc::new(Mutex::new(None));
17758 let snapshot = editor
17759 .update(cx, |editor, window, cx| {
17760 let snapshot = editor.buffer().read(cx).snapshot(cx);
17761 let range =
17762 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17763
17764 struct RenderArgs {
17765 row: MultiBufferRow,
17766 folded: bool,
17767 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17768 }
17769
17770 let crease = Crease::inline(
17771 range,
17772 FoldPlaceholder::test(),
17773 {
17774 let toggle_callback = render_args.clone();
17775 move |row, folded, callback, _window, _cx| {
17776 *toggle_callback.lock() = Some(RenderArgs {
17777 row,
17778 folded,
17779 callback,
17780 });
17781 div()
17782 }
17783 },
17784 |_row, _folded, _window, _cx| div(),
17785 );
17786
17787 editor.insert_creases(Some(crease), cx);
17788 let snapshot = editor.snapshot(window, cx);
17789 let _div = snapshot.render_crease_toggle(
17790 MultiBufferRow(1),
17791 false,
17792 cx.entity().clone(),
17793 window,
17794 cx,
17795 );
17796 snapshot
17797 })
17798 .unwrap();
17799
17800 let render_args = render_args.lock().take().unwrap();
17801 assert_eq!(render_args.row, MultiBufferRow(1));
17802 assert!(!render_args.folded);
17803 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17804
17805 cx.update_window(*editor, |_, window, cx| {
17806 (render_args.callback)(true, window, cx)
17807 })
17808 .unwrap();
17809 let snapshot = editor
17810 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17811 .unwrap();
17812 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17813
17814 cx.update_window(*editor, |_, window, cx| {
17815 (render_args.callback)(false, window, cx)
17816 })
17817 .unwrap();
17818 let snapshot = editor
17819 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17820 .unwrap();
17821 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17822}
17823
17824#[gpui::test]
17825async fn test_input_text(cx: &mut TestAppContext) {
17826 init_test(cx, |_| {});
17827 let mut cx = EditorTestContext::new(cx).await;
17828
17829 cx.set_state(
17830 &r#"ˇone
17831 two
17832
17833 three
17834 fourˇ
17835 five
17836
17837 siˇx"#
17838 .unindent(),
17839 );
17840
17841 cx.dispatch_action(HandleInput(String::new()));
17842 cx.assert_editor_state(
17843 &r#"ˇone
17844 two
17845
17846 three
17847 fourˇ
17848 five
17849
17850 siˇx"#
17851 .unindent(),
17852 );
17853
17854 cx.dispatch_action(HandleInput("AAAA".to_string()));
17855 cx.assert_editor_state(
17856 &r#"AAAAˇone
17857 two
17858
17859 three
17860 fourAAAAˇ
17861 five
17862
17863 siAAAAˇx"#
17864 .unindent(),
17865 );
17866}
17867
17868#[gpui::test]
17869async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17870 init_test(cx, |_| {});
17871
17872 let mut cx = EditorTestContext::new(cx).await;
17873 cx.set_state(
17874 r#"let foo = 1;
17875let foo = 2;
17876let foo = 3;
17877let fooˇ = 4;
17878let foo = 5;
17879let foo = 6;
17880let foo = 7;
17881let foo = 8;
17882let foo = 9;
17883let foo = 10;
17884let foo = 11;
17885let foo = 12;
17886let foo = 13;
17887let foo = 14;
17888let foo = 15;"#,
17889 );
17890
17891 cx.update_editor(|e, window, cx| {
17892 assert_eq!(
17893 e.next_scroll_position,
17894 NextScrollCursorCenterTopBottom::Center,
17895 "Default next scroll direction is center",
17896 );
17897
17898 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17899 assert_eq!(
17900 e.next_scroll_position,
17901 NextScrollCursorCenterTopBottom::Top,
17902 "After center, next scroll direction should be top",
17903 );
17904
17905 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17906 assert_eq!(
17907 e.next_scroll_position,
17908 NextScrollCursorCenterTopBottom::Bottom,
17909 "After top, next scroll direction should be bottom",
17910 );
17911
17912 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17913 assert_eq!(
17914 e.next_scroll_position,
17915 NextScrollCursorCenterTopBottom::Center,
17916 "After bottom, scrolling should start over",
17917 );
17918
17919 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17920 assert_eq!(
17921 e.next_scroll_position,
17922 NextScrollCursorCenterTopBottom::Top,
17923 "Scrolling continues if retriggered fast enough"
17924 );
17925 });
17926
17927 cx.executor()
17928 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17929 cx.executor().run_until_parked();
17930 cx.update_editor(|e, _, _| {
17931 assert_eq!(
17932 e.next_scroll_position,
17933 NextScrollCursorCenterTopBottom::Center,
17934 "If scrolling is not triggered fast enough, it should reset"
17935 );
17936 });
17937}
17938
17939#[gpui::test]
17940async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17941 init_test(cx, |_| {});
17942 let mut cx = EditorLspTestContext::new_rust(
17943 lsp::ServerCapabilities {
17944 definition_provider: Some(lsp::OneOf::Left(true)),
17945 references_provider: Some(lsp::OneOf::Left(true)),
17946 ..lsp::ServerCapabilities::default()
17947 },
17948 cx,
17949 )
17950 .await;
17951
17952 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17953 let go_to_definition = cx
17954 .lsp
17955 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17956 move |params, _| async move {
17957 if empty_go_to_definition {
17958 Ok(None)
17959 } else {
17960 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17961 uri: params.text_document_position_params.text_document.uri,
17962 range: lsp::Range::new(
17963 lsp::Position::new(4, 3),
17964 lsp::Position::new(4, 6),
17965 ),
17966 })))
17967 }
17968 },
17969 );
17970 let references = cx
17971 .lsp
17972 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17973 Ok(Some(vec![lsp::Location {
17974 uri: params.text_document_position.text_document.uri,
17975 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17976 }]))
17977 });
17978 (go_to_definition, references)
17979 };
17980
17981 cx.set_state(
17982 &r#"fn one() {
17983 let mut a = ˇtwo();
17984 }
17985
17986 fn two() {}"#
17987 .unindent(),
17988 );
17989 set_up_lsp_handlers(false, &mut cx);
17990 let navigated = cx
17991 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17992 .await
17993 .expect("Failed to navigate to definition");
17994 assert_eq!(
17995 navigated,
17996 Navigated::Yes,
17997 "Should have navigated to definition from the GetDefinition response"
17998 );
17999 cx.assert_editor_state(
18000 &r#"fn one() {
18001 let mut a = two();
18002 }
18003
18004 fn «twoˇ»() {}"#
18005 .unindent(),
18006 );
18007
18008 let editors = cx.update_workspace(|workspace, _, cx| {
18009 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18010 });
18011 cx.update_editor(|_, _, test_editor_cx| {
18012 assert_eq!(
18013 editors.len(),
18014 1,
18015 "Initially, only one, test, editor should be open in the workspace"
18016 );
18017 assert_eq!(
18018 test_editor_cx.entity(),
18019 editors.last().expect("Asserted len is 1").clone()
18020 );
18021 });
18022
18023 set_up_lsp_handlers(true, &mut cx);
18024 let navigated = cx
18025 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18026 .await
18027 .expect("Failed to navigate to lookup references");
18028 assert_eq!(
18029 navigated,
18030 Navigated::Yes,
18031 "Should have navigated to references as a fallback after empty GoToDefinition response"
18032 );
18033 // We should not change the selections in the existing file,
18034 // if opening another milti buffer with the references
18035 cx.assert_editor_state(
18036 &r#"fn one() {
18037 let mut a = two();
18038 }
18039
18040 fn «twoˇ»() {}"#
18041 .unindent(),
18042 );
18043 let editors = cx.update_workspace(|workspace, _, cx| {
18044 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18045 });
18046 cx.update_editor(|_, _, test_editor_cx| {
18047 assert_eq!(
18048 editors.len(),
18049 2,
18050 "After falling back to references search, we open a new editor with the results"
18051 );
18052 let references_fallback_text = editors
18053 .into_iter()
18054 .find(|new_editor| *new_editor != test_editor_cx.entity())
18055 .expect("Should have one non-test editor now")
18056 .read(test_editor_cx)
18057 .text(test_editor_cx);
18058 assert_eq!(
18059 references_fallback_text, "fn one() {\n let mut a = two();\n}",
18060 "Should use the range from the references response and not the GoToDefinition one"
18061 );
18062 });
18063}
18064
18065#[gpui::test]
18066async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
18067 init_test(cx, |_| {});
18068 cx.update(|cx| {
18069 let mut editor_settings = EditorSettings::get_global(cx).clone();
18070 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
18071 EditorSettings::override_global(editor_settings, cx);
18072 });
18073 let mut cx = EditorLspTestContext::new_rust(
18074 lsp::ServerCapabilities {
18075 definition_provider: Some(lsp::OneOf::Left(true)),
18076 references_provider: Some(lsp::OneOf::Left(true)),
18077 ..lsp::ServerCapabilities::default()
18078 },
18079 cx,
18080 )
18081 .await;
18082 let original_state = r#"fn one() {
18083 let mut a = ˇtwo();
18084 }
18085
18086 fn two() {}"#
18087 .unindent();
18088 cx.set_state(&original_state);
18089
18090 let mut go_to_definition = cx
18091 .lsp
18092 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18093 move |_, _| async move { Ok(None) },
18094 );
18095 let _references = cx
18096 .lsp
18097 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18098 panic!("Should not call for references with no go to definition fallback")
18099 });
18100
18101 let navigated = cx
18102 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18103 .await
18104 .expect("Failed to navigate to lookup references");
18105 go_to_definition
18106 .next()
18107 .await
18108 .expect("Should have called the go_to_definition handler");
18109
18110 assert_eq!(
18111 navigated,
18112 Navigated::No,
18113 "Should have navigated to references as a fallback after empty GoToDefinition response"
18114 );
18115 cx.assert_editor_state(&original_state);
18116 let editors = cx.update_workspace(|workspace, _, cx| {
18117 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18118 });
18119 cx.update_editor(|_, _, _| {
18120 assert_eq!(
18121 editors.len(),
18122 1,
18123 "After unsuccessful fallback, no other editor should have been opened"
18124 );
18125 });
18126}
18127
18128#[gpui::test]
18129async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18130 init_test(cx, |_| {});
18131
18132 let language = Arc::new(Language::new(
18133 LanguageConfig::default(),
18134 Some(tree_sitter_rust::LANGUAGE.into()),
18135 ));
18136
18137 let text = r#"
18138 #[cfg(test)]
18139 mod tests() {
18140 #[test]
18141 fn runnable_1() {
18142 let a = 1;
18143 }
18144
18145 #[test]
18146 fn runnable_2() {
18147 let a = 1;
18148 let b = 2;
18149 }
18150 }
18151 "#
18152 .unindent();
18153
18154 let fs = FakeFs::new(cx.executor());
18155 fs.insert_file("/file.rs", Default::default()).await;
18156
18157 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18158 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18159 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18160 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18161 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18162
18163 let editor = cx.new_window_entity(|window, cx| {
18164 Editor::new(
18165 EditorMode::full(),
18166 multi_buffer,
18167 Some(project.clone()),
18168 window,
18169 cx,
18170 )
18171 });
18172
18173 editor.update_in(cx, |editor, window, cx| {
18174 let snapshot = editor.buffer().read(cx).snapshot(cx);
18175 editor.tasks.insert(
18176 (buffer.read(cx).remote_id(), 3),
18177 RunnableTasks {
18178 templates: vec![],
18179 offset: snapshot.anchor_before(43),
18180 column: 0,
18181 extra_variables: HashMap::default(),
18182 context_range: BufferOffset(43)..BufferOffset(85),
18183 },
18184 );
18185 editor.tasks.insert(
18186 (buffer.read(cx).remote_id(), 8),
18187 RunnableTasks {
18188 templates: vec![],
18189 offset: snapshot.anchor_before(86),
18190 column: 0,
18191 extra_variables: HashMap::default(),
18192 context_range: BufferOffset(86)..BufferOffset(191),
18193 },
18194 );
18195
18196 // Test finding task when cursor is inside function body
18197 editor.change_selections(None, window, cx, |s| {
18198 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18199 });
18200 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18201 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18202
18203 // Test finding task when cursor is on function name
18204 editor.change_selections(None, window, cx, |s| {
18205 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18206 });
18207 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18208 assert_eq!(row, 8, "Should find task when cursor is on function name");
18209 });
18210}
18211
18212#[gpui::test]
18213async fn test_folding_buffers(cx: &mut TestAppContext) {
18214 init_test(cx, |_| {});
18215
18216 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18217 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18218 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18219
18220 let fs = FakeFs::new(cx.executor());
18221 fs.insert_tree(
18222 path!("/a"),
18223 json!({
18224 "first.rs": sample_text_1,
18225 "second.rs": sample_text_2,
18226 "third.rs": sample_text_3,
18227 }),
18228 )
18229 .await;
18230 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18231 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18232 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18233 let worktree = project.update(cx, |project, cx| {
18234 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18235 assert_eq!(worktrees.len(), 1);
18236 worktrees.pop().unwrap()
18237 });
18238 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18239
18240 let buffer_1 = project
18241 .update(cx, |project, cx| {
18242 project.open_buffer((worktree_id, "first.rs"), cx)
18243 })
18244 .await
18245 .unwrap();
18246 let buffer_2 = project
18247 .update(cx, |project, cx| {
18248 project.open_buffer((worktree_id, "second.rs"), cx)
18249 })
18250 .await
18251 .unwrap();
18252 let buffer_3 = project
18253 .update(cx, |project, cx| {
18254 project.open_buffer((worktree_id, "third.rs"), cx)
18255 })
18256 .await
18257 .unwrap();
18258
18259 let multi_buffer = cx.new(|cx| {
18260 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18261 multi_buffer.push_excerpts(
18262 buffer_1.clone(),
18263 [
18264 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18265 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18266 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18267 ],
18268 cx,
18269 );
18270 multi_buffer.push_excerpts(
18271 buffer_2.clone(),
18272 [
18273 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18274 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18275 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18276 ],
18277 cx,
18278 );
18279 multi_buffer.push_excerpts(
18280 buffer_3.clone(),
18281 [
18282 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18283 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18284 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18285 ],
18286 cx,
18287 );
18288 multi_buffer
18289 });
18290 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18291 Editor::new(
18292 EditorMode::full(),
18293 multi_buffer.clone(),
18294 Some(project.clone()),
18295 window,
18296 cx,
18297 )
18298 });
18299
18300 assert_eq!(
18301 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18302 "\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",
18303 );
18304
18305 multi_buffer_editor.update(cx, |editor, cx| {
18306 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18307 });
18308 assert_eq!(
18309 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18310 "\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",
18311 "After folding the first buffer, its text should not be displayed"
18312 );
18313
18314 multi_buffer_editor.update(cx, |editor, cx| {
18315 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18316 });
18317 assert_eq!(
18318 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18319 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18320 "After folding the second buffer, its text should not be displayed"
18321 );
18322
18323 multi_buffer_editor.update(cx, |editor, cx| {
18324 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18325 });
18326 assert_eq!(
18327 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18328 "\n\n\n\n\n",
18329 "After folding the third buffer, its text should not be displayed"
18330 );
18331
18332 // Emulate selection inside the fold logic, that should work
18333 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18334 editor
18335 .snapshot(window, cx)
18336 .next_line_boundary(Point::new(0, 4));
18337 });
18338
18339 multi_buffer_editor.update(cx, |editor, cx| {
18340 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18341 });
18342 assert_eq!(
18343 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18344 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18345 "After unfolding the second buffer, its text should be displayed"
18346 );
18347
18348 // Typing inside of buffer 1 causes that buffer to be unfolded.
18349 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18350 assert_eq!(
18351 multi_buffer
18352 .read(cx)
18353 .snapshot(cx)
18354 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18355 .collect::<String>(),
18356 "bbbb"
18357 );
18358 editor.change_selections(None, window, cx, |selections| {
18359 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18360 });
18361 editor.handle_input("B", window, cx);
18362 });
18363
18364 assert_eq!(
18365 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18366 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18367 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18368 );
18369
18370 multi_buffer_editor.update(cx, |editor, cx| {
18371 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18372 });
18373 assert_eq!(
18374 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18375 "\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",
18376 "After unfolding the all buffers, all original text should be displayed"
18377 );
18378}
18379
18380#[gpui::test]
18381async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18382 init_test(cx, |_| {});
18383
18384 let sample_text_1 = "1111\n2222\n3333".to_string();
18385 let sample_text_2 = "4444\n5555\n6666".to_string();
18386 let sample_text_3 = "7777\n8888\n9999".to_string();
18387
18388 let fs = FakeFs::new(cx.executor());
18389 fs.insert_tree(
18390 path!("/a"),
18391 json!({
18392 "first.rs": sample_text_1,
18393 "second.rs": sample_text_2,
18394 "third.rs": sample_text_3,
18395 }),
18396 )
18397 .await;
18398 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18399 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18400 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18401 let worktree = project.update(cx, |project, cx| {
18402 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18403 assert_eq!(worktrees.len(), 1);
18404 worktrees.pop().unwrap()
18405 });
18406 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18407
18408 let buffer_1 = project
18409 .update(cx, |project, cx| {
18410 project.open_buffer((worktree_id, "first.rs"), cx)
18411 })
18412 .await
18413 .unwrap();
18414 let buffer_2 = project
18415 .update(cx, |project, cx| {
18416 project.open_buffer((worktree_id, "second.rs"), cx)
18417 })
18418 .await
18419 .unwrap();
18420 let buffer_3 = project
18421 .update(cx, |project, cx| {
18422 project.open_buffer((worktree_id, "third.rs"), cx)
18423 })
18424 .await
18425 .unwrap();
18426
18427 let multi_buffer = cx.new(|cx| {
18428 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18429 multi_buffer.push_excerpts(
18430 buffer_1.clone(),
18431 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18432 cx,
18433 );
18434 multi_buffer.push_excerpts(
18435 buffer_2.clone(),
18436 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18437 cx,
18438 );
18439 multi_buffer.push_excerpts(
18440 buffer_3.clone(),
18441 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18442 cx,
18443 );
18444 multi_buffer
18445 });
18446
18447 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18448 Editor::new(
18449 EditorMode::full(),
18450 multi_buffer,
18451 Some(project.clone()),
18452 window,
18453 cx,
18454 )
18455 });
18456
18457 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18458 assert_eq!(
18459 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18460 full_text,
18461 );
18462
18463 multi_buffer_editor.update(cx, |editor, cx| {
18464 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18465 });
18466 assert_eq!(
18467 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18468 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18469 "After folding the first buffer, its text should not be displayed"
18470 );
18471
18472 multi_buffer_editor.update(cx, |editor, cx| {
18473 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18474 });
18475
18476 assert_eq!(
18477 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18478 "\n\n\n\n\n\n7777\n8888\n9999",
18479 "After folding the second buffer, its text should not be displayed"
18480 );
18481
18482 multi_buffer_editor.update(cx, |editor, cx| {
18483 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18484 });
18485 assert_eq!(
18486 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18487 "\n\n\n\n\n",
18488 "After folding the third buffer, its text should not be displayed"
18489 );
18490
18491 multi_buffer_editor.update(cx, |editor, cx| {
18492 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18493 });
18494 assert_eq!(
18495 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18496 "\n\n\n\n4444\n5555\n6666\n\n",
18497 "After unfolding the second buffer, its text should be displayed"
18498 );
18499
18500 multi_buffer_editor.update(cx, |editor, cx| {
18501 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18502 });
18503 assert_eq!(
18504 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18505 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18506 "After unfolding the first buffer, its text should be displayed"
18507 );
18508
18509 multi_buffer_editor.update(cx, |editor, cx| {
18510 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18511 });
18512 assert_eq!(
18513 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18514 full_text,
18515 "After unfolding all buffers, all original text should be displayed"
18516 );
18517}
18518
18519#[gpui::test]
18520async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18521 init_test(cx, |_| {});
18522
18523 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18524
18525 let fs = FakeFs::new(cx.executor());
18526 fs.insert_tree(
18527 path!("/a"),
18528 json!({
18529 "main.rs": sample_text,
18530 }),
18531 )
18532 .await;
18533 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18534 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18535 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18536 let worktree = project.update(cx, |project, cx| {
18537 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18538 assert_eq!(worktrees.len(), 1);
18539 worktrees.pop().unwrap()
18540 });
18541 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18542
18543 let buffer_1 = project
18544 .update(cx, |project, cx| {
18545 project.open_buffer((worktree_id, "main.rs"), cx)
18546 })
18547 .await
18548 .unwrap();
18549
18550 let multi_buffer = cx.new(|cx| {
18551 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18552 multi_buffer.push_excerpts(
18553 buffer_1.clone(),
18554 [ExcerptRange::new(
18555 Point::new(0, 0)
18556 ..Point::new(
18557 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18558 0,
18559 ),
18560 )],
18561 cx,
18562 );
18563 multi_buffer
18564 });
18565 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18566 Editor::new(
18567 EditorMode::full(),
18568 multi_buffer,
18569 Some(project.clone()),
18570 window,
18571 cx,
18572 )
18573 });
18574
18575 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18576 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18577 enum TestHighlight {}
18578 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18579 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18580 editor.highlight_text::<TestHighlight>(
18581 vec![highlight_range.clone()],
18582 HighlightStyle::color(Hsla::green()),
18583 cx,
18584 );
18585 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18586 });
18587
18588 let full_text = format!("\n\n{sample_text}");
18589 assert_eq!(
18590 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18591 full_text,
18592 );
18593}
18594
18595#[gpui::test]
18596async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18597 init_test(cx, |_| {});
18598 cx.update(|cx| {
18599 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18600 "keymaps/default-linux.json",
18601 cx,
18602 )
18603 .unwrap();
18604 cx.bind_keys(default_key_bindings);
18605 });
18606
18607 let (editor, cx) = cx.add_window_view(|window, cx| {
18608 let multi_buffer = MultiBuffer::build_multi(
18609 [
18610 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18611 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18612 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18613 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18614 ],
18615 cx,
18616 );
18617 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18618
18619 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18620 // fold all but the second buffer, so that we test navigating between two
18621 // adjacent folded buffers, as well as folded buffers at the start and
18622 // end the multibuffer
18623 editor.fold_buffer(buffer_ids[0], cx);
18624 editor.fold_buffer(buffer_ids[2], cx);
18625 editor.fold_buffer(buffer_ids[3], cx);
18626
18627 editor
18628 });
18629 cx.simulate_resize(size(px(1000.), px(1000.)));
18630
18631 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18632 cx.assert_excerpts_with_selections(indoc! {"
18633 [EXCERPT]
18634 ˇ[FOLDED]
18635 [EXCERPT]
18636 a1
18637 b1
18638 [EXCERPT]
18639 [FOLDED]
18640 [EXCERPT]
18641 [FOLDED]
18642 "
18643 });
18644 cx.simulate_keystroke("down");
18645 cx.assert_excerpts_with_selections(indoc! {"
18646 [EXCERPT]
18647 [FOLDED]
18648 [EXCERPT]
18649 ˇa1
18650 b1
18651 [EXCERPT]
18652 [FOLDED]
18653 [EXCERPT]
18654 [FOLDED]
18655 "
18656 });
18657 cx.simulate_keystroke("down");
18658 cx.assert_excerpts_with_selections(indoc! {"
18659 [EXCERPT]
18660 [FOLDED]
18661 [EXCERPT]
18662 a1
18663 ˇb1
18664 [EXCERPT]
18665 [FOLDED]
18666 [EXCERPT]
18667 [FOLDED]
18668 "
18669 });
18670 cx.simulate_keystroke("down");
18671 cx.assert_excerpts_with_selections(indoc! {"
18672 [EXCERPT]
18673 [FOLDED]
18674 [EXCERPT]
18675 a1
18676 b1
18677 ˇ[EXCERPT]
18678 [FOLDED]
18679 [EXCERPT]
18680 [FOLDED]
18681 "
18682 });
18683 cx.simulate_keystroke("down");
18684 cx.assert_excerpts_with_selections(indoc! {"
18685 [EXCERPT]
18686 [FOLDED]
18687 [EXCERPT]
18688 a1
18689 b1
18690 [EXCERPT]
18691 ˇ[FOLDED]
18692 [EXCERPT]
18693 [FOLDED]
18694 "
18695 });
18696 for _ in 0..5 {
18697 cx.simulate_keystroke("down");
18698 cx.assert_excerpts_with_selections(indoc! {"
18699 [EXCERPT]
18700 [FOLDED]
18701 [EXCERPT]
18702 a1
18703 b1
18704 [EXCERPT]
18705 [FOLDED]
18706 [EXCERPT]
18707 ˇ[FOLDED]
18708 "
18709 });
18710 }
18711
18712 cx.simulate_keystroke("up");
18713 cx.assert_excerpts_with_selections(indoc! {"
18714 [EXCERPT]
18715 [FOLDED]
18716 [EXCERPT]
18717 a1
18718 b1
18719 [EXCERPT]
18720 ˇ[FOLDED]
18721 [EXCERPT]
18722 [FOLDED]
18723 "
18724 });
18725 cx.simulate_keystroke("up");
18726 cx.assert_excerpts_with_selections(indoc! {"
18727 [EXCERPT]
18728 [FOLDED]
18729 [EXCERPT]
18730 a1
18731 b1
18732 ˇ[EXCERPT]
18733 [FOLDED]
18734 [EXCERPT]
18735 [FOLDED]
18736 "
18737 });
18738 cx.simulate_keystroke("up");
18739 cx.assert_excerpts_with_selections(indoc! {"
18740 [EXCERPT]
18741 [FOLDED]
18742 [EXCERPT]
18743 a1
18744 ˇb1
18745 [EXCERPT]
18746 [FOLDED]
18747 [EXCERPT]
18748 [FOLDED]
18749 "
18750 });
18751 cx.simulate_keystroke("up");
18752 cx.assert_excerpts_with_selections(indoc! {"
18753 [EXCERPT]
18754 [FOLDED]
18755 [EXCERPT]
18756 ˇa1
18757 b1
18758 [EXCERPT]
18759 [FOLDED]
18760 [EXCERPT]
18761 [FOLDED]
18762 "
18763 });
18764 for _ in 0..5 {
18765 cx.simulate_keystroke("up");
18766 cx.assert_excerpts_with_selections(indoc! {"
18767 [EXCERPT]
18768 ˇ[FOLDED]
18769 [EXCERPT]
18770 a1
18771 b1
18772 [EXCERPT]
18773 [FOLDED]
18774 [EXCERPT]
18775 [FOLDED]
18776 "
18777 });
18778 }
18779}
18780
18781#[gpui::test]
18782async fn test_inline_completion_text(cx: &mut TestAppContext) {
18783 init_test(cx, |_| {});
18784
18785 // Simple insertion
18786 assert_highlighted_edits(
18787 "Hello, world!",
18788 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18789 true,
18790 cx,
18791 |highlighted_edits, cx| {
18792 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18793 assert_eq!(highlighted_edits.highlights.len(), 1);
18794 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18795 assert_eq!(
18796 highlighted_edits.highlights[0].1.background_color,
18797 Some(cx.theme().status().created_background)
18798 );
18799 },
18800 )
18801 .await;
18802
18803 // Replacement
18804 assert_highlighted_edits(
18805 "This is a test.",
18806 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18807 false,
18808 cx,
18809 |highlighted_edits, cx| {
18810 assert_eq!(highlighted_edits.text, "That is a test.");
18811 assert_eq!(highlighted_edits.highlights.len(), 1);
18812 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18813 assert_eq!(
18814 highlighted_edits.highlights[0].1.background_color,
18815 Some(cx.theme().status().created_background)
18816 );
18817 },
18818 )
18819 .await;
18820
18821 // Multiple edits
18822 assert_highlighted_edits(
18823 "Hello, world!",
18824 vec![
18825 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18826 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18827 ],
18828 false,
18829 cx,
18830 |highlighted_edits, cx| {
18831 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18832 assert_eq!(highlighted_edits.highlights.len(), 2);
18833 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18834 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18835 assert_eq!(
18836 highlighted_edits.highlights[0].1.background_color,
18837 Some(cx.theme().status().created_background)
18838 );
18839 assert_eq!(
18840 highlighted_edits.highlights[1].1.background_color,
18841 Some(cx.theme().status().created_background)
18842 );
18843 },
18844 )
18845 .await;
18846
18847 // Multiple lines with edits
18848 assert_highlighted_edits(
18849 "First line\nSecond line\nThird line\nFourth line",
18850 vec![
18851 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18852 (
18853 Point::new(2, 0)..Point::new(2, 10),
18854 "New third line".to_string(),
18855 ),
18856 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18857 ],
18858 false,
18859 cx,
18860 |highlighted_edits, cx| {
18861 assert_eq!(
18862 highlighted_edits.text,
18863 "Second modified\nNew third line\nFourth updated line"
18864 );
18865 assert_eq!(highlighted_edits.highlights.len(), 3);
18866 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18867 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18868 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18869 for highlight in &highlighted_edits.highlights {
18870 assert_eq!(
18871 highlight.1.background_color,
18872 Some(cx.theme().status().created_background)
18873 );
18874 }
18875 },
18876 )
18877 .await;
18878}
18879
18880#[gpui::test]
18881async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18882 init_test(cx, |_| {});
18883
18884 // Deletion
18885 assert_highlighted_edits(
18886 "Hello, world!",
18887 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18888 true,
18889 cx,
18890 |highlighted_edits, cx| {
18891 assert_eq!(highlighted_edits.text, "Hello, world!");
18892 assert_eq!(highlighted_edits.highlights.len(), 1);
18893 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18894 assert_eq!(
18895 highlighted_edits.highlights[0].1.background_color,
18896 Some(cx.theme().status().deleted_background)
18897 );
18898 },
18899 )
18900 .await;
18901
18902 // Insertion
18903 assert_highlighted_edits(
18904 "Hello, world!",
18905 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18906 true,
18907 cx,
18908 |highlighted_edits, cx| {
18909 assert_eq!(highlighted_edits.highlights.len(), 1);
18910 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18911 assert_eq!(
18912 highlighted_edits.highlights[0].1.background_color,
18913 Some(cx.theme().status().created_background)
18914 );
18915 },
18916 )
18917 .await;
18918}
18919
18920async fn assert_highlighted_edits(
18921 text: &str,
18922 edits: Vec<(Range<Point>, String)>,
18923 include_deletions: bool,
18924 cx: &mut TestAppContext,
18925 assertion_fn: impl Fn(HighlightedText, &App),
18926) {
18927 let window = cx.add_window(|window, cx| {
18928 let buffer = MultiBuffer::build_simple(text, cx);
18929 Editor::new(EditorMode::full(), buffer, None, window, cx)
18930 });
18931 let cx = &mut VisualTestContext::from_window(*window, cx);
18932
18933 let (buffer, snapshot) = window
18934 .update(cx, |editor, _window, cx| {
18935 (
18936 editor.buffer().clone(),
18937 editor.buffer().read(cx).snapshot(cx),
18938 )
18939 })
18940 .unwrap();
18941
18942 let edits = edits
18943 .into_iter()
18944 .map(|(range, edit)| {
18945 (
18946 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18947 edit,
18948 )
18949 })
18950 .collect::<Vec<_>>();
18951
18952 let text_anchor_edits = edits
18953 .clone()
18954 .into_iter()
18955 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18956 .collect::<Vec<_>>();
18957
18958 let edit_preview = window
18959 .update(cx, |_, _window, cx| {
18960 buffer
18961 .read(cx)
18962 .as_singleton()
18963 .unwrap()
18964 .read(cx)
18965 .preview_edits(text_anchor_edits.into(), cx)
18966 })
18967 .unwrap()
18968 .await;
18969
18970 cx.update(|_window, cx| {
18971 let highlighted_edits = inline_completion_edit_text(
18972 &snapshot.as_singleton().unwrap().2,
18973 &edits,
18974 &edit_preview,
18975 include_deletions,
18976 cx,
18977 );
18978 assertion_fn(highlighted_edits, cx)
18979 });
18980}
18981
18982#[track_caller]
18983fn assert_breakpoint(
18984 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18985 path: &Arc<Path>,
18986 expected: Vec<(u32, Breakpoint)>,
18987) {
18988 if expected.len() == 0usize {
18989 assert!(!breakpoints.contains_key(path), "{}", path.display());
18990 } else {
18991 let mut breakpoint = breakpoints
18992 .get(path)
18993 .unwrap()
18994 .into_iter()
18995 .map(|breakpoint| {
18996 (
18997 breakpoint.row,
18998 Breakpoint {
18999 message: breakpoint.message.clone(),
19000 state: breakpoint.state,
19001 condition: breakpoint.condition.clone(),
19002 hit_condition: breakpoint.hit_condition.clone(),
19003 },
19004 )
19005 })
19006 .collect::<Vec<_>>();
19007
19008 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
19009
19010 assert_eq!(expected, breakpoint);
19011 }
19012}
19013
19014fn add_log_breakpoint_at_cursor(
19015 editor: &mut Editor,
19016 log_message: &str,
19017 window: &mut Window,
19018 cx: &mut Context<Editor>,
19019) {
19020 let (anchor, bp) = editor
19021 .breakpoints_at_cursors(window, cx)
19022 .first()
19023 .and_then(|(anchor, bp)| {
19024 if let Some(bp) = bp {
19025 Some((*anchor, bp.clone()))
19026 } else {
19027 None
19028 }
19029 })
19030 .unwrap_or_else(|| {
19031 let cursor_position: Point = editor.selections.newest(cx).head();
19032
19033 let breakpoint_position = editor
19034 .snapshot(window, cx)
19035 .display_snapshot
19036 .buffer_snapshot
19037 .anchor_before(Point::new(cursor_position.row, 0));
19038
19039 (breakpoint_position, Breakpoint::new_log(&log_message))
19040 });
19041
19042 editor.edit_breakpoint_at_anchor(
19043 anchor,
19044 bp,
19045 BreakpointEditAction::EditLogMessage(log_message.into()),
19046 cx,
19047 );
19048}
19049
19050#[gpui::test]
19051async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
19052 init_test(cx, |_| {});
19053
19054 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19055 let fs = FakeFs::new(cx.executor());
19056 fs.insert_tree(
19057 path!("/a"),
19058 json!({
19059 "main.rs": sample_text,
19060 }),
19061 )
19062 .await;
19063 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19064 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19065 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19066
19067 let fs = FakeFs::new(cx.executor());
19068 fs.insert_tree(
19069 path!("/a"),
19070 json!({
19071 "main.rs": sample_text,
19072 }),
19073 )
19074 .await;
19075 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19076 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19077 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19078 let worktree_id = workspace
19079 .update(cx, |workspace, _window, cx| {
19080 workspace.project().update(cx, |project, cx| {
19081 project.worktrees(cx).next().unwrap().read(cx).id()
19082 })
19083 })
19084 .unwrap();
19085
19086 let buffer = project
19087 .update(cx, |project, cx| {
19088 project.open_buffer((worktree_id, "main.rs"), cx)
19089 })
19090 .await
19091 .unwrap();
19092
19093 let (editor, cx) = cx.add_window_view(|window, cx| {
19094 Editor::new(
19095 EditorMode::full(),
19096 MultiBuffer::build_from_buffer(buffer, cx),
19097 Some(project.clone()),
19098 window,
19099 cx,
19100 )
19101 });
19102
19103 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19104 let abs_path = project.read_with(cx, |project, cx| {
19105 project
19106 .absolute_path(&project_path, cx)
19107 .map(|path_buf| Arc::from(path_buf.to_owned()))
19108 .unwrap()
19109 });
19110
19111 // assert we can add breakpoint on the first line
19112 editor.update_in(cx, |editor, window, cx| {
19113 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19114 editor.move_to_end(&MoveToEnd, window, cx);
19115 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19116 });
19117
19118 let breakpoints = editor.update(cx, |editor, cx| {
19119 editor
19120 .breakpoint_store()
19121 .as_ref()
19122 .unwrap()
19123 .read(cx)
19124 .all_source_breakpoints(cx)
19125 .clone()
19126 });
19127
19128 assert_eq!(1, breakpoints.len());
19129 assert_breakpoint(
19130 &breakpoints,
19131 &abs_path,
19132 vec![
19133 (0, Breakpoint::new_standard()),
19134 (3, Breakpoint::new_standard()),
19135 ],
19136 );
19137
19138 editor.update_in(cx, |editor, window, cx| {
19139 editor.move_to_beginning(&MoveToBeginning, window, cx);
19140 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19141 });
19142
19143 let breakpoints = editor.update(cx, |editor, cx| {
19144 editor
19145 .breakpoint_store()
19146 .as_ref()
19147 .unwrap()
19148 .read(cx)
19149 .all_source_breakpoints(cx)
19150 .clone()
19151 });
19152
19153 assert_eq!(1, breakpoints.len());
19154 assert_breakpoint(
19155 &breakpoints,
19156 &abs_path,
19157 vec![(3, Breakpoint::new_standard())],
19158 );
19159
19160 editor.update_in(cx, |editor, window, cx| {
19161 editor.move_to_end(&MoveToEnd, window, cx);
19162 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19163 });
19164
19165 let breakpoints = editor.update(cx, |editor, cx| {
19166 editor
19167 .breakpoint_store()
19168 .as_ref()
19169 .unwrap()
19170 .read(cx)
19171 .all_source_breakpoints(cx)
19172 .clone()
19173 });
19174
19175 assert_eq!(0, breakpoints.len());
19176 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19177}
19178
19179#[gpui::test]
19180async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19181 init_test(cx, |_| {});
19182
19183 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19184
19185 let fs = FakeFs::new(cx.executor());
19186 fs.insert_tree(
19187 path!("/a"),
19188 json!({
19189 "main.rs": sample_text,
19190 }),
19191 )
19192 .await;
19193 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19194 let (workspace, cx) =
19195 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19196
19197 let worktree_id = workspace.update(cx, |workspace, cx| {
19198 workspace.project().update(cx, |project, cx| {
19199 project.worktrees(cx).next().unwrap().read(cx).id()
19200 })
19201 });
19202
19203 let buffer = project
19204 .update(cx, |project, cx| {
19205 project.open_buffer((worktree_id, "main.rs"), cx)
19206 })
19207 .await
19208 .unwrap();
19209
19210 let (editor, cx) = cx.add_window_view(|window, cx| {
19211 Editor::new(
19212 EditorMode::full(),
19213 MultiBuffer::build_from_buffer(buffer, cx),
19214 Some(project.clone()),
19215 window,
19216 cx,
19217 )
19218 });
19219
19220 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19221 let abs_path = project.read_with(cx, |project, cx| {
19222 project
19223 .absolute_path(&project_path, cx)
19224 .map(|path_buf| Arc::from(path_buf.to_owned()))
19225 .unwrap()
19226 });
19227
19228 editor.update_in(cx, |editor, window, cx| {
19229 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19230 });
19231
19232 let breakpoints = editor.update(cx, |editor, cx| {
19233 editor
19234 .breakpoint_store()
19235 .as_ref()
19236 .unwrap()
19237 .read(cx)
19238 .all_source_breakpoints(cx)
19239 .clone()
19240 });
19241
19242 assert_breakpoint(
19243 &breakpoints,
19244 &abs_path,
19245 vec![(0, Breakpoint::new_log("hello world"))],
19246 );
19247
19248 // Removing a log message from a log breakpoint should remove it
19249 editor.update_in(cx, |editor, window, cx| {
19250 add_log_breakpoint_at_cursor(editor, "", window, cx);
19251 });
19252
19253 let breakpoints = editor.update(cx, |editor, cx| {
19254 editor
19255 .breakpoint_store()
19256 .as_ref()
19257 .unwrap()
19258 .read(cx)
19259 .all_source_breakpoints(cx)
19260 .clone()
19261 });
19262
19263 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19264
19265 editor.update_in(cx, |editor, window, cx| {
19266 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19267 editor.move_to_end(&MoveToEnd, window, cx);
19268 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19269 // Not adding a log message to a standard breakpoint shouldn't remove it
19270 add_log_breakpoint_at_cursor(editor, "", window, cx);
19271 });
19272
19273 let breakpoints = editor.update(cx, |editor, cx| {
19274 editor
19275 .breakpoint_store()
19276 .as_ref()
19277 .unwrap()
19278 .read(cx)
19279 .all_source_breakpoints(cx)
19280 .clone()
19281 });
19282
19283 assert_breakpoint(
19284 &breakpoints,
19285 &abs_path,
19286 vec![
19287 (0, Breakpoint::new_standard()),
19288 (3, Breakpoint::new_standard()),
19289 ],
19290 );
19291
19292 editor.update_in(cx, |editor, window, cx| {
19293 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19294 });
19295
19296 let breakpoints = editor.update(cx, |editor, cx| {
19297 editor
19298 .breakpoint_store()
19299 .as_ref()
19300 .unwrap()
19301 .read(cx)
19302 .all_source_breakpoints(cx)
19303 .clone()
19304 });
19305
19306 assert_breakpoint(
19307 &breakpoints,
19308 &abs_path,
19309 vec![
19310 (0, Breakpoint::new_standard()),
19311 (3, Breakpoint::new_log("hello world")),
19312 ],
19313 );
19314
19315 editor.update_in(cx, |editor, window, cx| {
19316 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19317 });
19318
19319 let breakpoints = editor.update(cx, |editor, cx| {
19320 editor
19321 .breakpoint_store()
19322 .as_ref()
19323 .unwrap()
19324 .read(cx)
19325 .all_source_breakpoints(cx)
19326 .clone()
19327 });
19328
19329 assert_breakpoint(
19330 &breakpoints,
19331 &abs_path,
19332 vec![
19333 (0, Breakpoint::new_standard()),
19334 (3, Breakpoint::new_log("hello Earth!!")),
19335 ],
19336 );
19337}
19338
19339/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19340/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19341/// or when breakpoints were placed out of order. This tests for a regression too
19342#[gpui::test]
19343async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19344 init_test(cx, |_| {});
19345
19346 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19347 let fs = FakeFs::new(cx.executor());
19348 fs.insert_tree(
19349 path!("/a"),
19350 json!({
19351 "main.rs": sample_text,
19352 }),
19353 )
19354 .await;
19355 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19356 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19357 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19358
19359 let fs = FakeFs::new(cx.executor());
19360 fs.insert_tree(
19361 path!("/a"),
19362 json!({
19363 "main.rs": sample_text,
19364 }),
19365 )
19366 .await;
19367 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19368 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19369 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19370 let worktree_id = workspace
19371 .update(cx, |workspace, _window, cx| {
19372 workspace.project().update(cx, |project, cx| {
19373 project.worktrees(cx).next().unwrap().read(cx).id()
19374 })
19375 })
19376 .unwrap();
19377
19378 let buffer = project
19379 .update(cx, |project, cx| {
19380 project.open_buffer((worktree_id, "main.rs"), cx)
19381 })
19382 .await
19383 .unwrap();
19384
19385 let (editor, cx) = cx.add_window_view(|window, cx| {
19386 Editor::new(
19387 EditorMode::full(),
19388 MultiBuffer::build_from_buffer(buffer, cx),
19389 Some(project.clone()),
19390 window,
19391 cx,
19392 )
19393 });
19394
19395 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19396 let abs_path = project.read_with(cx, |project, cx| {
19397 project
19398 .absolute_path(&project_path, cx)
19399 .map(|path_buf| Arc::from(path_buf.to_owned()))
19400 .unwrap()
19401 });
19402
19403 // assert we can add breakpoint on the first line
19404 editor.update_in(cx, |editor, window, cx| {
19405 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19406 editor.move_to_end(&MoveToEnd, window, cx);
19407 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19408 editor.move_up(&MoveUp, window, cx);
19409 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19410 });
19411
19412 let breakpoints = editor.update(cx, |editor, cx| {
19413 editor
19414 .breakpoint_store()
19415 .as_ref()
19416 .unwrap()
19417 .read(cx)
19418 .all_source_breakpoints(cx)
19419 .clone()
19420 });
19421
19422 assert_eq!(1, breakpoints.len());
19423 assert_breakpoint(
19424 &breakpoints,
19425 &abs_path,
19426 vec![
19427 (0, Breakpoint::new_standard()),
19428 (2, Breakpoint::new_standard()),
19429 (3, Breakpoint::new_standard()),
19430 ],
19431 );
19432
19433 editor.update_in(cx, |editor, window, cx| {
19434 editor.move_to_beginning(&MoveToBeginning, window, cx);
19435 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19436 editor.move_to_end(&MoveToEnd, window, cx);
19437 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19438 // Disabling a breakpoint that doesn't exist should do nothing
19439 editor.move_up(&MoveUp, window, cx);
19440 editor.move_up(&MoveUp, window, cx);
19441 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19442 });
19443
19444 let breakpoints = editor.update(cx, |editor, cx| {
19445 editor
19446 .breakpoint_store()
19447 .as_ref()
19448 .unwrap()
19449 .read(cx)
19450 .all_source_breakpoints(cx)
19451 .clone()
19452 });
19453
19454 let disable_breakpoint = {
19455 let mut bp = Breakpoint::new_standard();
19456 bp.state = BreakpointState::Disabled;
19457 bp
19458 };
19459
19460 assert_eq!(1, breakpoints.len());
19461 assert_breakpoint(
19462 &breakpoints,
19463 &abs_path,
19464 vec![
19465 (0, disable_breakpoint.clone()),
19466 (2, Breakpoint::new_standard()),
19467 (3, disable_breakpoint.clone()),
19468 ],
19469 );
19470
19471 editor.update_in(cx, |editor, window, cx| {
19472 editor.move_to_beginning(&MoveToBeginning, window, cx);
19473 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19474 editor.move_to_end(&MoveToEnd, window, cx);
19475 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19476 editor.move_up(&MoveUp, window, cx);
19477 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19478 });
19479
19480 let breakpoints = editor.update(cx, |editor, cx| {
19481 editor
19482 .breakpoint_store()
19483 .as_ref()
19484 .unwrap()
19485 .read(cx)
19486 .all_source_breakpoints(cx)
19487 .clone()
19488 });
19489
19490 assert_eq!(1, breakpoints.len());
19491 assert_breakpoint(
19492 &breakpoints,
19493 &abs_path,
19494 vec![
19495 (0, Breakpoint::new_standard()),
19496 (2, disable_breakpoint),
19497 (3, Breakpoint::new_standard()),
19498 ],
19499 );
19500}
19501
19502#[gpui::test]
19503async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19504 init_test(cx, |_| {});
19505 let capabilities = lsp::ServerCapabilities {
19506 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19507 prepare_provider: Some(true),
19508 work_done_progress_options: Default::default(),
19509 })),
19510 ..Default::default()
19511 };
19512 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19513
19514 cx.set_state(indoc! {"
19515 struct Fˇoo {}
19516 "});
19517
19518 cx.update_editor(|editor, _, cx| {
19519 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19520 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19521 editor.highlight_background::<DocumentHighlightRead>(
19522 &[highlight_range],
19523 |c| c.editor_document_highlight_read_background,
19524 cx,
19525 );
19526 });
19527
19528 let mut prepare_rename_handler = cx
19529 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19530 move |_, _, _| async move {
19531 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19532 start: lsp::Position {
19533 line: 0,
19534 character: 7,
19535 },
19536 end: lsp::Position {
19537 line: 0,
19538 character: 10,
19539 },
19540 })))
19541 },
19542 );
19543 let prepare_rename_task = cx
19544 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19545 .expect("Prepare rename was not started");
19546 prepare_rename_handler.next().await.unwrap();
19547 prepare_rename_task.await.expect("Prepare rename failed");
19548
19549 let mut rename_handler =
19550 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19551 let edit = lsp::TextEdit {
19552 range: lsp::Range {
19553 start: lsp::Position {
19554 line: 0,
19555 character: 7,
19556 },
19557 end: lsp::Position {
19558 line: 0,
19559 character: 10,
19560 },
19561 },
19562 new_text: "FooRenamed".to_string(),
19563 };
19564 Ok(Some(lsp::WorkspaceEdit::new(
19565 // Specify the same edit twice
19566 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19567 )))
19568 });
19569 let rename_task = cx
19570 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19571 .expect("Confirm rename was not started");
19572 rename_handler.next().await.unwrap();
19573 rename_task.await.expect("Confirm rename failed");
19574 cx.run_until_parked();
19575
19576 // Despite two edits, only one is actually applied as those are identical
19577 cx.assert_editor_state(indoc! {"
19578 struct FooRenamedˇ {}
19579 "});
19580}
19581
19582#[gpui::test]
19583async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19584 init_test(cx, |_| {});
19585 // These capabilities indicate that the server does not support prepare rename.
19586 let capabilities = lsp::ServerCapabilities {
19587 rename_provider: Some(lsp::OneOf::Left(true)),
19588 ..Default::default()
19589 };
19590 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19591
19592 cx.set_state(indoc! {"
19593 struct Fˇoo {}
19594 "});
19595
19596 cx.update_editor(|editor, _window, cx| {
19597 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19598 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19599 editor.highlight_background::<DocumentHighlightRead>(
19600 &[highlight_range],
19601 |c| c.editor_document_highlight_read_background,
19602 cx,
19603 );
19604 });
19605
19606 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19607 .expect("Prepare rename was not started")
19608 .await
19609 .expect("Prepare rename failed");
19610
19611 let mut rename_handler =
19612 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19613 let edit = lsp::TextEdit {
19614 range: lsp::Range {
19615 start: lsp::Position {
19616 line: 0,
19617 character: 7,
19618 },
19619 end: lsp::Position {
19620 line: 0,
19621 character: 10,
19622 },
19623 },
19624 new_text: "FooRenamed".to_string(),
19625 };
19626 Ok(Some(lsp::WorkspaceEdit::new(
19627 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19628 )))
19629 });
19630 let rename_task = cx
19631 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19632 .expect("Confirm rename was not started");
19633 rename_handler.next().await.unwrap();
19634 rename_task.await.expect("Confirm rename failed");
19635 cx.run_until_parked();
19636
19637 // Correct range is renamed, as `surrounding_word` is used to find it.
19638 cx.assert_editor_state(indoc! {"
19639 struct FooRenamedˇ {}
19640 "});
19641}
19642
19643#[gpui::test]
19644async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19645 init_test(cx, |_| {});
19646 let mut cx = EditorTestContext::new(cx).await;
19647
19648 let language = Arc::new(
19649 Language::new(
19650 LanguageConfig::default(),
19651 Some(tree_sitter_html::LANGUAGE.into()),
19652 )
19653 .with_brackets_query(
19654 r#"
19655 ("<" @open "/>" @close)
19656 ("</" @open ">" @close)
19657 ("<" @open ">" @close)
19658 ("\"" @open "\"" @close)
19659 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19660 "#,
19661 )
19662 .unwrap(),
19663 );
19664 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19665
19666 cx.set_state(indoc! {"
19667 <span>ˇ</span>
19668 "});
19669 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19670 cx.assert_editor_state(indoc! {"
19671 <span>
19672 ˇ
19673 </span>
19674 "});
19675
19676 cx.set_state(indoc! {"
19677 <span><span></span>ˇ</span>
19678 "});
19679 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19680 cx.assert_editor_state(indoc! {"
19681 <span><span></span>
19682 ˇ</span>
19683 "});
19684
19685 cx.set_state(indoc! {"
19686 <span>ˇ
19687 </span>
19688 "});
19689 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19690 cx.assert_editor_state(indoc! {"
19691 <span>
19692 ˇ
19693 </span>
19694 "});
19695}
19696
19697#[gpui::test(iterations = 10)]
19698async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19699 init_test(cx, |_| {});
19700
19701 let fs = FakeFs::new(cx.executor());
19702 fs.insert_tree(
19703 path!("/dir"),
19704 json!({
19705 "a.ts": "a",
19706 }),
19707 )
19708 .await;
19709
19710 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19711 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19712 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19713
19714 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19715 language_registry.add(Arc::new(Language::new(
19716 LanguageConfig {
19717 name: "TypeScript".into(),
19718 matcher: LanguageMatcher {
19719 path_suffixes: vec!["ts".to_string()],
19720 ..Default::default()
19721 },
19722 ..Default::default()
19723 },
19724 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19725 )));
19726 let mut fake_language_servers = language_registry.register_fake_lsp(
19727 "TypeScript",
19728 FakeLspAdapter {
19729 capabilities: lsp::ServerCapabilities {
19730 code_lens_provider: Some(lsp::CodeLensOptions {
19731 resolve_provider: Some(true),
19732 }),
19733 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19734 commands: vec!["_the/command".to_string()],
19735 ..lsp::ExecuteCommandOptions::default()
19736 }),
19737 ..lsp::ServerCapabilities::default()
19738 },
19739 ..FakeLspAdapter::default()
19740 },
19741 );
19742
19743 let (buffer, _handle) = project
19744 .update(cx, |p, cx| {
19745 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19746 })
19747 .await
19748 .unwrap();
19749 cx.executor().run_until_parked();
19750
19751 let fake_server = fake_language_servers.next().await.unwrap();
19752
19753 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19754 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19755 drop(buffer_snapshot);
19756 let actions = cx
19757 .update_window(*workspace, |_, window, cx| {
19758 project.code_actions(&buffer, anchor..anchor, window, cx)
19759 })
19760 .unwrap();
19761
19762 fake_server
19763 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19764 Ok(Some(vec![
19765 lsp::CodeLens {
19766 range: lsp::Range::default(),
19767 command: Some(lsp::Command {
19768 title: "Code lens command".to_owned(),
19769 command: "_the/command".to_owned(),
19770 arguments: None,
19771 }),
19772 data: None,
19773 },
19774 lsp::CodeLens {
19775 range: lsp::Range::default(),
19776 command: Some(lsp::Command {
19777 title: "Command not in capabilities".to_owned(),
19778 command: "not in capabilities".to_owned(),
19779 arguments: None,
19780 }),
19781 data: None,
19782 },
19783 lsp::CodeLens {
19784 range: lsp::Range {
19785 start: lsp::Position {
19786 line: 1,
19787 character: 1,
19788 },
19789 end: lsp::Position {
19790 line: 1,
19791 character: 1,
19792 },
19793 },
19794 command: Some(lsp::Command {
19795 title: "Command not in range".to_owned(),
19796 command: "_the/command".to_owned(),
19797 arguments: None,
19798 }),
19799 data: None,
19800 },
19801 ]))
19802 })
19803 .next()
19804 .await;
19805
19806 let actions = actions.await.unwrap();
19807 assert_eq!(
19808 actions.len(),
19809 1,
19810 "Should have only one valid action for the 0..0 range"
19811 );
19812 let action = actions[0].clone();
19813 let apply = project.update(cx, |project, cx| {
19814 project.apply_code_action(buffer.clone(), action, true, cx)
19815 });
19816
19817 // Resolving the code action does not populate its edits. In absence of
19818 // edits, we must execute the given command.
19819 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19820 |mut lens, _| async move {
19821 let lens_command = lens.command.as_mut().expect("should have a command");
19822 assert_eq!(lens_command.title, "Code lens command");
19823 lens_command.arguments = Some(vec![json!("the-argument")]);
19824 Ok(lens)
19825 },
19826 );
19827
19828 // While executing the command, the language server sends the editor
19829 // a `workspaceEdit` request.
19830 fake_server
19831 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19832 let fake = fake_server.clone();
19833 move |params, _| {
19834 assert_eq!(params.command, "_the/command");
19835 let fake = fake.clone();
19836 async move {
19837 fake.server
19838 .request::<lsp::request::ApplyWorkspaceEdit>(
19839 lsp::ApplyWorkspaceEditParams {
19840 label: None,
19841 edit: lsp::WorkspaceEdit {
19842 changes: Some(
19843 [(
19844 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19845 vec![lsp::TextEdit {
19846 range: lsp::Range::new(
19847 lsp::Position::new(0, 0),
19848 lsp::Position::new(0, 0),
19849 ),
19850 new_text: "X".into(),
19851 }],
19852 )]
19853 .into_iter()
19854 .collect(),
19855 ),
19856 ..Default::default()
19857 },
19858 },
19859 )
19860 .await
19861 .into_response()
19862 .unwrap();
19863 Ok(Some(json!(null)))
19864 }
19865 }
19866 })
19867 .next()
19868 .await;
19869
19870 // Applying the code lens command returns a project transaction containing the edits
19871 // sent by the language server in its `workspaceEdit` request.
19872 let transaction = apply.await.unwrap();
19873 assert!(transaction.0.contains_key(&buffer));
19874 buffer.update(cx, |buffer, cx| {
19875 assert_eq!(buffer.text(), "Xa");
19876 buffer.undo(cx);
19877 assert_eq!(buffer.text(), "a");
19878 });
19879}
19880
19881#[gpui::test]
19882async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19883 init_test(cx, |_| {});
19884
19885 let fs = FakeFs::new(cx.executor());
19886 let main_text = r#"fn main() {
19887println!("1");
19888println!("2");
19889println!("3");
19890println!("4");
19891println!("5");
19892}"#;
19893 let lib_text = "mod foo {}";
19894 fs.insert_tree(
19895 path!("/a"),
19896 json!({
19897 "lib.rs": lib_text,
19898 "main.rs": main_text,
19899 }),
19900 )
19901 .await;
19902
19903 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19904 let (workspace, cx) =
19905 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19906 let worktree_id = workspace.update(cx, |workspace, cx| {
19907 workspace.project().update(cx, |project, cx| {
19908 project.worktrees(cx).next().unwrap().read(cx).id()
19909 })
19910 });
19911
19912 let expected_ranges = vec![
19913 Point::new(0, 0)..Point::new(0, 0),
19914 Point::new(1, 0)..Point::new(1, 1),
19915 Point::new(2, 0)..Point::new(2, 2),
19916 Point::new(3, 0)..Point::new(3, 3),
19917 ];
19918
19919 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19920 let editor_1 = workspace
19921 .update_in(cx, |workspace, window, cx| {
19922 workspace.open_path(
19923 (worktree_id, "main.rs"),
19924 Some(pane_1.downgrade()),
19925 true,
19926 window,
19927 cx,
19928 )
19929 })
19930 .unwrap()
19931 .await
19932 .downcast::<Editor>()
19933 .unwrap();
19934 pane_1.update(cx, |pane, cx| {
19935 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19936 open_editor.update(cx, |editor, cx| {
19937 assert_eq!(
19938 editor.display_text(cx),
19939 main_text,
19940 "Original main.rs text on initial open",
19941 );
19942 assert_eq!(
19943 editor
19944 .selections
19945 .all::<Point>(cx)
19946 .into_iter()
19947 .map(|s| s.range())
19948 .collect::<Vec<_>>(),
19949 vec![Point::zero()..Point::zero()],
19950 "Default selections on initial open",
19951 );
19952 })
19953 });
19954 editor_1.update_in(cx, |editor, window, cx| {
19955 editor.change_selections(None, window, cx, |s| {
19956 s.select_ranges(expected_ranges.clone());
19957 });
19958 });
19959
19960 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19961 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19962 });
19963 let editor_2 = workspace
19964 .update_in(cx, |workspace, window, cx| {
19965 workspace.open_path(
19966 (worktree_id, "main.rs"),
19967 Some(pane_2.downgrade()),
19968 true,
19969 window,
19970 cx,
19971 )
19972 })
19973 .unwrap()
19974 .await
19975 .downcast::<Editor>()
19976 .unwrap();
19977 pane_2.update(cx, |pane, cx| {
19978 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19979 open_editor.update(cx, |editor, cx| {
19980 assert_eq!(
19981 editor.display_text(cx),
19982 main_text,
19983 "Original main.rs text on initial open in another panel",
19984 );
19985 assert_eq!(
19986 editor
19987 .selections
19988 .all::<Point>(cx)
19989 .into_iter()
19990 .map(|s| s.range())
19991 .collect::<Vec<_>>(),
19992 vec![Point::zero()..Point::zero()],
19993 "Default selections on initial open in another panel",
19994 );
19995 })
19996 });
19997
19998 editor_2.update_in(cx, |editor, window, cx| {
19999 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
20000 });
20001
20002 let _other_editor_1 = workspace
20003 .update_in(cx, |workspace, window, cx| {
20004 workspace.open_path(
20005 (worktree_id, "lib.rs"),
20006 Some(pane_1.downgrade()),
20007 true,
20008 window,
20009 cx,
20010 )
20011 })
20012 .unwrap()
20013 .await
20014 .downcast::<Editor>()
20015 .unwrap();
20016 pane_1
20017 .update_in(cx, |pane, window, cx| {
20018 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20019 .unwrap()
20020 })
20021 .await
20022 .unwrap();
20023 drop(editor_1);
20024 pane_1.update(cx, |pane, cx| {
20025 pane.active_item()
20026 .unwrap()
20027 .downcast::<Editor>()
20028 .unwrap()
20029 .update(cx, |editor, cx| {
20030 assert_eq!(
20031 editor.display_text(cx),
20032 lib_text,
20033 "Other file should be open and active",
20034 );
20035 });
20036 assert_eq!(pane.items().count(), 1, "No other editors should be open");
20037 });
20038
20039 let _other_editor_2 = workspace
20040 .update_in(cx, |workspace, window, cx| {
20041 workspace.open_path(
20042 (worktree_id, "lib.rs"),
20043 Some(pane_2.downgrade()),
20044 true,
20045 window,
20046 cx,
20047 )
20048 })
20049 .unwrap()
20050 .await
20051 .downcast::<Editor>()
20052 .unwrap();
20053 pane_2
20054 .update_in(cx, |pane, window, cx| {
20055 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20056 .unwrap()
20057 })
20058 .await
20059 .unwrap();
20060 drop(editor_2);
20061 pane_2.update(cx, |pane, cx| {
20062 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20063 open_editor.update(cx, |editor, cx| {
20064 assert_eq!(
20065 editor.display_text(cx),
20066 lib_text,
20067 "Other file should be open and active in another panel too",
20068 );
20069 });
20070 assert_eq!(
20071 pane.items().count(),
20072 1,
20073 "No other editors should be open in another pane",
20074 );
20075 });
20076
20077 let _editor_1_reopened = workspace
20078 .update_in(cx, |workspace, window, cx| {
20079 workspace.open_path(
20080 (worktree_id, "main.rs"),
20081 Some(pane_1.downgrade()),
20082 true,
20083 window,
20084 cx,
20085 )
20086 })
20087 .unwrap()
20088 .await
20089 .downcast::<Editor>()
20090 .unwrap();
20091 let _editor_2_reopened = workspace
20092 .update_in(cx, |workspace, window, cx| {
20093 workspace.open_path(
20094 (worktree_id, "main.rs"),
20095 Some(pane_2.downgrade()),
20096 true,
20097 window,
20098 cx,
20099 )
20100 })
20101 .unwrap()
20102 .await
20103 .downcast::<Editor>()
20104 .unwrap();
20105 pane_1.update(cx, |pane, cx| {
20106 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20107 open_editor.update(cx, |editor, cx| {
20108 assert_eq!(
20109 editor.display_text(cx),
20110 main_text,
20111 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20112 );
20113 assert_eq!(
20114 editor
20115 .selections
20116 .all::<Point>(cx)
20117 .into_iter()
20118 .map(|s| s.range())
20119 .collect::<Vec<_>>(),
20120 expected_ranges,
20121 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20122 );
20123 })
20124 });
20125 pane_2.update(cx, |pane, cx| {
20126 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20127 open_editor.update(cx, |editor, cx| {
20128 assert_eq!(
20129 editor.display_text(cx),
20130 r#"fn main() {
20131⋯rintln!("1");
20132⋯intln!("2");
20133⋯ntln!("3");
20134println!("4");
20135println!("5");
20136}"#,
20137 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20138 );
20139 assert_eq!(
20140 editor
20141 .selections
20142 .all::<Point>(cx)
20143 .into_iter()
20144 .map(|s| s.range())
20145 .collect::<Vec<_>>(),
20146 vec![Point::zero()..Point::zero()],
20147 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20148 );
20149 })
20150 });
20151}
20152
20153#[gpui::test]
20154async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20155 init_test(cx, |_| {});
20156
20157 let fs = FakeFs::new(cx.executor());
20158 let main_text = r#"fn main() {
20159println!("1");
20160println!("2");
20161println!("3");
20162println!("4");
20163println!("5");
20164}"#;
20165 let lib_text = "mod foo {}";
20166 fs.insert_tree(
20167 path!("/a"),
20168 json!({
20169 "lib.rs": lib_text,
20170 "main.rs": main_text,
20171 }),
20172 )
20173 .await;
20174
20175 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20176 let (workspace, cx) =
20177 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20178 let worktree_id = workspace.update(cx, |workspace, cx| {
20179 workspace.project().update(cx, |project, cx| {
20180 project.worktrees(cx).next().unwrap().read(cx).id()
20181 })
20182 });
20183
20184 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20185 let editor = workspace
20186 .update_in(cx, |workspace, window, cx| {
20187 workspace.open_path(
20188 (worktree_id, "main.rs"),
20189 Some(pane.downgrade()),
20190 true,
20191 window,
20192 cx,
20193 )
20194 })
20195 .unwrap()
20196 .await
20197 .downcast::<Editor>()
20198 .unwrap();
20199 pane.update(cx, |pane, cx| {
20200 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20201 open_editor.update(cx, |editor, cx| {
20202 assert_eq!(
20203 editor.display_text(cx),
20204 main_text,
20205 "Original main.rs text on initial open",
20206 );
20207 })
20208 });
20209 editor.update_in(cx, |editor, window, cx| {
20210 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20211 });
20212
20213 cx.update_global(|store: &mut SettingsStore, cx| {
20214 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20215 s.restore_on_file_reopen = Some(false);
20216 });
20217 });
20218 editor.update_in(cx, |editor, window, cx| {
20219 editor.fold_ranges(
20220 vec![
20221 Point::new(1, 0)..Point::new(1, 1),
20222 Point::new(2, 0)..Point::new(2, 2),
20223 Point::new(3, 0)..Point::new(3, 3),
20224 ],
20225 false,
20226 window,
20227 cx,
20228 );
20229 });
20230 pane.update_in(cx, |pane, window, cx| {
20231 pane.close_all_items(&CloseAllItems::default(), window, cx)
20232 .unwrap()
20233 })
20234 .await
20235 .unwrap();
20236 pane.update(cx, |pane, _| {
20237 assert!(pane.active_item().is_none());
20238 });
20239 cx.update_global(|store: &mut SettingsStore, cx| {
20240 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20241 s.restore_on_file_reopen = Some(true);
20242 });
20243 });
20244
20245 let _editor_reopened = workspace
20246 .update_in(cx, |workspace, window, cx| {
20247 workspace.open_path(
20248 (worktree_id, "main.rs"),
20249 Some(pane.downgrade()),
20250 true,
20251 window,
20252 cx,
20253 )
20254 })
20255 .unwrap()
20256 .await
20257 .downcast::<Editor>()
20258 .unwrap();
20259 pane.update(cx, |pane, cx| {
20260 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20261 open_editor.update(cx, |editor, cx| {
20262 assert_eq!(
20263 editor.display_text(cx),
20264 main_text,
20265 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20266 );
20267 })
20268 });
20269}
20270
20271#[gpui::test]
20272async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20273 struct EmptyModalView {
20274 focus_handle: gpui::FocusHandle,
20275 }
20276 impl EventEmitter<DismissEvent> for EmptyModalView {}
20277 impl Render for EmptyModalView {
20278 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20279 div()
20280 }
20281 }
20282 impl Focusable for EmptyModalView {
20283 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20284 self.focus_handle.clone()
20285 }
20286 }
20287 impl workspace::ModalView for EmptyModalView {}
20288 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20289 EmptyModalView {
20290 focus_handle: cx.focus_handle(),
20291 }
20292 }
20293
20294 init_test(cx, |_| {});
20295
20296 let fs = FakeFs::new(cx.executor());
20297 let project = Project::test(fs, [], cx).await;
20298 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20299 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20300 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20301 let editor = cx.new_window_entity(|window, cx| {
20302 Editor::new(
20303 EditorMode::full(),
20304 buffer,
20305 Some(project.clone()),
20306 window,
20307 cx,
20308 )
20309 });
20310 workspace
20311 .update(cx, |workspace, window, cx| {
20312 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20313 })
20314 .unwrap();
20315 editor.update_in(cx, |editor, window, cx| {
20316 editor.open_context_menu(&OpenContextMenu, window, cx);
20317 assert!(editor.mouse_context_menu.is_some());
20318 });
20319 workspace
20320 .update(cx, |workspace, window, cx| {
20321 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20322 })
20323 .unwrap();
20324 cx.read(|cx| {
20325 assert!(editor.read(cx).mouse_context_menu.is_none());
20326 });
20327}
20328
20329#[gpui::test]
20330async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20331 init_test(cx, |_| {});
20332
20333 let fs = FakeFs::new(cx.executor());
20334 fs.insert_file(path!("/file.html"), Default::default())
20335 .await;
20336
20337 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20338
20339 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20340 let html_language = Arc::new(Language::new(
20341 LanguageConfig {
20342 name: "HTML".into(),
20343 matcher: LanguageMatcher {
20344 path_suffixes: vec!["html".to_string()],
20345 ..LanguageMatcher::default()
20346 },
20347 brackets: BracketPairConfig {
20348 pairs: vec![BracketPair {
20349 start: "<".into(),
20350 end: ">".into(),
20351 close: true,
20352 ..Default::default()
20353 }],
20354 ..Default::default()
20355 },
20356 ..Default::default()
20357 },
20358 Some(tree_sitter_html::LANGUAGE.into()),
20359 ));
20360 language_registry.add(html_language);
20361 let mut fake_servers = language_registry.register_fake_lsp(
20362 "HTML",
20363 FakeLspAdapter {
20364 capabilities: lsp::ServerCapabilities {
20365 completion_provider: Some(lsp::CompletionOptions {
20366 resolve_provider: Some(true),
20367 ..Default::default()
20368 }),
20369 ..Default::default()
20370 },
20371 ..Default::default()
20372 },
20373 );
20374
20375 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20376 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20377
20378 let worktree_id = workspace
20379 .update(cx, |workspace, _window, cx| {
20380 workspace.project().update(cx, |project, cx| {
20381 project.worktrees(cx).next().unwrap().read(cx).id()
20382 })
20383 })
20384 .unwrap();
20385 project
20386 .update(cx, |project, cx| {
20387 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20388 })
20389 .await
20390 .unwrap();
20391 let editor = workspace
20392 .update(cx, |workspace, window, cx| {
20393 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20394 })
20395 .unwrap()
20396 .await
20397 .unwrap()
20398 .downcast::<Editor>()
20399 .unwrap();
20400
20401 let fake_server = fake_servers.next().await.unwrap();
20402 editor.update_in(cx, |editor, window, cx| {
20403 editor.set_text("<ad></ad>", window, cx);
20404 editor.change_selections(None, window, cx, |selections| {
20405 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20406 });
20407 let Some((buffer, _)) = editor
20408 .buffer
20409 .read(cx)
20410 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20411 else {
20412 panic!("Failed to get buffer for selection position");
20413 };
20414 let buffer = buffer.read(cx);
20415 let buffer_id = buffer.remote_id();
20416 let opening_range =
20417 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20418 let closing_range =
20419 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20420 let mut linked_ranges = HashMap::default();
20421 linked_ranges.insert(
20422 buffer_id,
20423 vec![(opening_range.clone(), vec![closing_range.clone()])],
20424 );
20425 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20426 });
20427 let mut completion_handle =
20428 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20429 Ok(Some(lsp::CompletionResponse::Array(vec![
20430 lsp::CompletionItem {
20431 label: "head".to_string(),
20432 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20433 lsp::InsertReplaceEdit {
20434 new_text: "head".to_string(),
20435 insert: lsp::Range::new(
20436 lsp::Position::new(0, 1),
20437 lsp::Position::new(0, 3),
20438 ),
20439 replace: lsp::Range::new(
20440 lsp::Position::new(0, 1),
20441 lsp::Position::new(0, 3),
20442 ),
20443 },
20444 )),
20445 ..Default::default()
20446 },
20447 ])))
20448 });
20449 editor.update_in(cx, |editor, window, cx| {
20450 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20451 });
20452 cx.run_until_parked();
20453 completion_handle.next().await.unwrap();
20454 editor.update(cx, |editor, _| {
20455 assert!(
20456 editor.context_menu_visible(),
20457 "Completion menu should be visible"
20458 );
20459 });
20460 editor.update_in(cx, |editor, window, cx| {
20461 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20462 });
20463 cx.executor().run_until_parked();
20464 editor.update(cx, |editor, cx| {
20465 assert_eq!(editor.text(cx), "<head></head>");
20466 });
20467}
20468
20469#[gpui::test]
20470async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20471 init_test(cx, |_| {});
20472
20473 let fs = FakeFs::new(cx.executor());
20474 fs.insert_tree(
20475 path!("/root"),
20476 json!({
20477 "a": {
20478 "main.rs": "fn main() {}",
20479 },
20480 "foo": {
20481 "bar": {
20482 "external_file.rs": "pub mod external {}",
20483 }
20484 }
20485 }),
20486 )
20487 .await;
20488
20489 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20490 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20491 language_registry.add(rust_lang());
20492 let _fake_servers = language_registry.register_fake_lsp(
20493 "Rust",
20494 FakeLspAdapter {
20495 ..FakeLspAdapter::default()
20496 },
20497 );
20498 let (workspace, cx) =
20499 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20500 let worktree_id = workspace.update(cx, |workspace, cx| {
20501 workspace.project().update(cx, |project, cx| {
20502 project.worktrees(cx).next().unwrap().read(cx).id()
20503 })
20504 });
20505
20506 let assert_language_servers_count =
20507 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20508 project.update(cx, |project, cx| {
20509 let current = project
20510 .lsp_store()
20511 .read(cx)
20512 .as_local()
20513 .unwrap()
20514 .language_servers
20515 .len();
20516 assert_eq!(expected, current, "{context}");
20517 });
20518 };
20519
20520 assert_language_servers_count(
20521 0,
20522 "No servers should be running before any file is open",
20523 cx,
20524 );
20525 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20526 let main_editor = workspace
20527 .update_in(cx, |workspace, window, cx| {
20528 workspace.open_path(
20529 (worktree_id, "main.rs"),
20530 Some(pane.downgrade()),
20531 true,
20532 window,
20533 cx,
20534 )
20535 })
20536 .unwrap()
20537 .await
20538 .downcast::<Editor>()
20539 .unwrap();
20540 pane.update(cx, |pane, cx| {
20541 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20542 open_editor.update(cx, |editor, cx| {
20543 assert_eq!(
20544 editor.display_text(cx),
20545 "fn main() {}",
20546 "Original main.rs text on initial open",
20547 );
20548 });
20549 assert_eq!(open_editor, main_editor);
20550 });
20551 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20552
20553 let external_editor = workspace
20554 .update_in(cx, |workspace, window, cx| {
20555 workspace.open_abs_path(
20556 PathBuf::from("/root/foo/bar/external_file.rs"),
20557 OpenOptions::default(),
20558 window,
20559 cx,
20560 )
20561 })
20562 .await
20563 .expect("opening external file")
20564 .downcast::<Editor>()
20565 .expect("downcasted external file's open element to editor");
20566 pane.update(cx, |pane, cx| {
20567 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20568 open_editor.update(cx, |editor, cx| {
20569 assert_eq!(
20570 editor.display_text(cx),
20571 "pub mod external {}",
20572 "External file is open now",
20573 );
20574 });
20575 assert_eq!(open_editor, external_editor);
20576 });
20577 assert_language_servers_count(
20578 1,
20579 "Second, external, *.rs file should join the existing server",
20580 cx,
20581 );
20582
20583 pane.update_in(cx, |pane, window, cx| {
20584 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20585 })
20586 .unwrap()
20587 .await
20588 .unwrap();
20589 pane.update_in(cx, |pane, window, cx| {
20590 pane.navigate_backward(window, cx);
20591 });
20592 cx.run_until_parked();
20593 pane.update(cx, |pane, cx| {
20594 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20595 open_editor.update(cx, |editor, cx| {
20596 assert_eq!(
20597 editor.display_text(cx),
20598 "pub mod external {}",
20599 "External file is open now",
20600 );
20601 });
20602 });
20603 assert_language_servers_count(
20604 1,
20605 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20606 cx,
20607 );
20608
20609 cx.update(|_, cx| {
20610 workspace::reload(&workspace::Reload::default(), cx);
20611 });
20612 assert_language_servers_count(
20613 1,
20614 "After reloading the worktree with local and external files opened, only one project should be started",
20615 cx,
20616 );
20617}
20618
20619#[gpui::test]
20620async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20621 init_test(cx, |_| {});
20622
20623 let mut cx = EditorTestContext::new(cx).await;
20624 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20625 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20626
20627 // test cursor move to start of each line on tab
20628 // for `if`, `elif`, `else`, `while`, `with` and `for`
20629 cx.set_state(indoc! {"
20630 def main():
20631 ˇ for item in items:
20632 ˇ while item.active:
20633 ˇ if item.value > 10:
20634 ˇ continue
20635 ˇ elif item.value < 0:
20636 ˇ break
20637 ˇ else:
20638 ˇ with item.context() as ctx:
20639 ˇ yield count
20640 ˇ else:
20641 ˇ log('while else')
20642 ˇ else:
20643 ˇ log('for else')
20644 "});
20645 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20646 cx.assert_editor_state(indoc! {"
20647 def main():
20648 ˇfor item in items:
20649 ˇwhile item.active:
20650 ˇif item.value > 10:
20651 ˇcontinue
20652 ˇelif item.value < 0:
20653 ˇbreak
20654 ˇelse:
20655 ˇwith item.context() as ctx:
20656 ˇyield count
20657 ˇelse:
20658 ˇlog('while else')
20659 ˇelse:
20660 ˇlog('for else')
20661 "});
20662 // test relative indent is preserved when tab
20663 // for `if`, `elif`, `else`, `while`, `with` and `for`
20664 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20665 cx.assert_editor_state(indoc! {"
20666 def main():
20667 ˇfor item in items:
20668 ˇwhile item.active:
20669 ˇif item.value > 10:
20670 ˇcontinue
20671 ˇelif item.value < 0:
20672 ˇbreak
20673 ˇelse:
20674 ˇwith item.context() as ctx:
20675 ˇyield count
20676 ˇelse:
20677 ˇlog('while else')
20678 ˇelse:
20679 ˇlog('for else')
20680 "});
20681
20682 // test cursor move to start of each line on tab
20683 // for `try`, `except`, `else`, `finally`, `match` and `def`
20684 cx.set_state(indoc! {"
20685 def main():
20686 ˇ try:
20687 ˇ fetch()
20688 ˇ except ValueError:
20689 ˇ handle_error()
20690 ˇ else:
20691 ˇ match value:
20692 ˇ case _:
20693 ˇ finally:
20694 ˇ def status():
20695 ˇ return 0
20696 "});
20697 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20698 cx.assert_editor_state(indoc! {"
20699 def main():
20700 ˇtry:
20701 ˇfetch()
20702 ˇexcept ValueError:
20703 ˇhandle_error()
20704 ˇelse:
20705 ˇmatch value:
20706 ˇcase _:
20707 ˇfinally:
20708 ˇdef status():
20709 ˇreturn 0
20710 "});
20711 // test relative indent is preserved when tab
20712 // for `try`, `except`, `else`, `finally`, `match` and `def`
20713 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20714 cx.assert_editor_state(indoc! {"
20715 def main():
20716 ˇtry:
20717 ˇfetch()
20718 ˇexcept ValueError:
20719 ˇhandle_error()
20720 ˇelse:
20721 ˇmatch value:
20722 ˇcase _:
20723 ˇfinally:
20724 ˇdef status():
20725 ˇreturn 0
20726 "});
20727}
20728
20729#[gpui::test]
20730async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
20731 init_test(cx, |_| {});
20732
20733 let mut cx = EditorTestContext::new(cx).await;
20734 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20735 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20736
20737 // test `else` auto outdents when typed inside `if` block
20738 cx.set_state(indoc! {"
20739 def main():
20740 if i == 2:
20741 return
20742 ˇ
20743 "});
20744 cx.update_editor(|editor, window, cx| {
20745 editor.handle_input("else:", window, cx);
20746 });
20747 cx.assert_editor_state(indoc! {"
20748 def main():
20749 if i == 2:
20750 return
20751 else:ˇ
20752 "});
20753
20754 // test `except` auto outdents when typed inside `try` block
20755 cx.set_state(indoc! {"
20756 def main():
20757 try:
20758 i = 2
20759 ˇ
20760 "});
20761 cx.update_editor(|editor, window, cx| {
20762 editor.handle_input("except:", window, cx);
20763 });
20764 cx.assert_editor_state(indoc! {"
20765 def main():
20766 try:
20767 i = 2
20768 except:ˇ
20769 "});
20770
20771 // test `else` auto outdents when typed inside `except` block
20772 cx.set_state(indoc! {"
20773 def main():
20774 try:
20775 i = 2
20776 except:
20777 j = 2
20778 ˇ
20779 "});
20780 cx.update_editor(|editor, window, cx| {
20781 editor.handle_input("else:", window, cx);
20782 });
20783 cx.assert_editor_state(indoc! {"
20784 def main():
20785 try:
20786 i = 2
20787 except:
20788 j = 2
20789 else:ˇ
20790 "});
20791
20792 // test `finally` auto outdents when typed inside `else` block
20793 cx.set_state(indoc! {"
20794 def main():
20795 try:
20796 i = 2
20797 except:
20798 j = 2
20799 else:
20800 k = 2
20801 ˇ
20802 "});
20803 cx.update_editor(|editor, window, cx| {
20804 editor.handle_input("finally:", window, cx);
20805 });
20806 cx.assert_editor_state(indoc! {"
20807 def main():
20808 try:
20809 i = 2
20810 except:
20811 j = 2
20812 else:
20813 k = 2
20814 finally:ˇ
20815 "});
20816
20817 // TODO: test `except` auto outdents when typed inside `try` block right after for block
20818 // cx.set_state(indoc! {"
20819 // def main():
20820 // try:
20821 // for i in range(n):
20822 // pass
20823 // ˇ
20824 // "});
20825 // cx.update_editor(|editor, window, cx| {
20826 // editor.handle_input("except:", window, cx);
20827 // });
20828 // cx.assert_editor_state(indoc! {"
20829 // def main():
20830 // try:
20831 // for i in range(n):
20832 // pass
20833 // except:ˇ
20834 // "});
20835
20836 // TODO: test `else` auto outdents when typed inside `except` block right after for block
20837 // cx.set_state(indoc! {"
20838 // def main():
20839 // try:
20840 // i = 2
20841 // except:
20842 // for i in range(n):
20843 // pass
20844 // ˇ
20845 // "});
20846 // cx.update_editor(|editor, window, cx| {
20847 // editor.handle_input("else:", window, cx);
20848 // });
20849 // cx.assert_editor_state(indoc! {"
20850 // def main():
20851 // try:
20852 // i = 2
20853 // except:
20854 // for i in range(n):
20855 // pass
20856 // else:ˇ
20857 // "});
20858
20859 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
20860 // cx.set_state(indoc! {"
20861 // def main():
20862 // try:
20863 // i = 2
20864 // except:
20865 // j = 2
20866 // else:
20867 // for i in range(n):
20868 // pass
20869 // ˇ
20870 // "});
20871 // cx.update_editor(|editor, window, cx| {
20872 // editor.handle_input("finally:", window, cx);
20873 // });
20874 // cx.assert_editor_state(indoc! {"
20875 // def main():
20876 // try:
20877 // i = 2
20878 // except:
20879 // j = 2
20880 // else:
20881 // for i in range(n):
20882 // pass
20883 // finally:ˇ
20884 // "});
20885
20886 // test `else` stays at correct indent when typed after `for` block
20887 cx.set_state(indoc! {"
20888 def main():
20889 for i in range(10):
20890 if i == 3:
20891 break
20892 ˇ
20893 "});
20894 cx.update_editor(|editor, window, cx| {
20895 editor.handle_input("else:", window, cx);
20896 });
20897 cx.assert_editor_state(indoc! {"
20898 def main():
20899 for i in range(10):
20900 if i == 3:
20901 break
20902 else:ˇ
20903 "});
20904
20905 // test does not outdent on typing after line with square brackets
20906 cx.set_state(indoc! {"
20907 def f() -> list[str]:
20908 ˇ
20909 "});
20910 cx.update_editor(|editor, window, cx| {
20911 editor.handle_input("a", window, cx);
20912 });
20913 cx.assert_editor_state(indoc! {"
20914 def f() -> list[str]:
20915 aˇ
20916 "});
20917}
20918
20919#[gpui::test]
20920async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
20921 init_test(cx, |_| {});
20922 update_test_language_settings(cx, |settings| {
20923 settings.defaults.extend_comment_on_newline = Some(false);
20924 });
20925 let mut cx = EditorTestContext::new(cx).await;
20926 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20927 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20928
20929 // test correct indent after newline on comment
20930 cx.set_state(indoc! {"
20931 # COMMENT:ˇ
20932 "});
20933 cx.update_editor(|editor, window, cx| {
20934 editor.newline(&Newline, window, cx);
20935 });
20936 cx.assert_editor_state(indoc! {"
20937 # COMMENT:
20938 ˇ
20939 "});
20940
20941 // test correct indent after newline in brackets
20942 cx.set_state(indoc! {"
20943 {ˇ}
20944 "});
20945 cx.update_editor(|editor, window, cx| {
20946 editor.newline(&Newline, window, cx);
20947 });
20948 cx.run_until_parked();
20949 cx.assert_editor_state(indoc! {"
20950 {
20951 ˇ
20952 }
20953 "});
20954
20955 cx.set_state(indoc! {"
20956 (ˇ)
20957 "});
20958 cx.update_editor(|editor, window, cx| {
20959 editor.newline(&Newline, window, cx);
20960 });
20961 cx.run_until_parked();
20962 cx.assert_editor_state(indoc! {"
20963 (
20964 ˇ
20965 )
20966 "});
20967
20968 // do not indent after empty lists or dictionaries
20969 cx.set_state(indoc! {"
20970 a = []ˇ
20971 "});
20972 cx.update_editor(|editor, window, cx| {
20973 editor.newline(&Newline, window, cx);
20974 });
20975 cx.run_until_parked();
20976 cx.assert_editor_state(indoc! {"
20977 a = []
20978 ˇ
20979 "});
20980}
20981
20982fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20983 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20984 point..point
20985}
20986
20987fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20988 let (text, ranges) = marked_text_ranges(marked_text, true);
20989 assert_eq!(editor.text(cx), text);
20990 assert_eq!(
20991 editor.selections.ranges(cx),
20992 ranges,
20993 "Assert selections are {}",
20994 marked_text
20995 );
20996}
20997
20998pub fn handle_signature_help_request(
20999 cx: &mut EditorLspTestContext,
21000 mocked_response: lsp::SignatureHelp,
21001) -> impl Future<Output = ()> + use<> {
21002 let mut request =
21003 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
21004 let mocked_response = mocked_response.clone();
21005 async move { Ok(Some(mocked_response)) }
21006 });
21007
21008 async move {
21009 request.next().await;
21010 }
21011}
21012
21013/// Handle completion request passing a marked string specifying where the completion
21014/// should be triggered from using '|' character, what range should be replaced, and what completions
21015/// should be returned using '<' and '>' to delimit the range.
21016///
21017/// Also see `handle_completion_request_with_insert_and_replace`.
21018#[track_caller]
21019pub fn handle_completion_request(
21020 cx: &mut EditorLspTestContext,
21021 marked_string: &str,
21022 completions: Vec<&'static str>,
21023 counter: Arc<AtomicUsize>,
21024) -> impl Future<Output = ()> {
21025 let complete_from_marker: TextRangeMarker = '|'.into();
21026 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21027 let (_, mut marked_ranges) = marked_text_ranges_by(
21028 marked_string,
21029 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21030 );
21031
21032 let complete_from_position =
21033 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21034 let replace_range =
21035 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21036
21037 let mut request =
21038 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21039 let completions = completions.clone();
21040 counter.fetch_add(1, atomic::Ordering::Release);
21041 async move {
21042 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21043 assert_eq!(
21044 params.text_document_position.position,
21045 complete_from_position
21046 );
21047 Ok(Some(lsp::CompletionResponse::Array(
21048 completions
21049 .iter()
21050 .map(|completion_text| lsp::CompletionItem {
21051 label: completion_text.to_string(),
21052 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21053 range: replace_range,
21054 new_text: completion_text.to_string(),
21055 })),
21056 ..Default::default()
21057 })
21058 .collect(),
21059 )))
21060 }
21061 });
21062
21063 async move {
21064 request.next().await;
21065 }
21066}
21067
21068/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
21069/// given instead, which also contains an `insert` range.
21070///
21071/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
21072/// that is, `replace_range.start..cursor_pos`.
21073pub fn handle_completion_request_with_insert_and_replace(
21074 cx: &mut EditorLspTestContext,
21075 marked_string: &str,
21076 completions: Vec<&'static str>,
21077 counter: Arc<AtomicUsize>,
21078) -> impl Future<Output = ()> {
21079 let complete_from_marker: TextRangeMarker = '|'.into();
21080 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21081 let (_, mut marked_ranges) = marked_text_ranges_by(
21082 marked_string,
21083 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21084 );
21085
21086 let complete_from_position =
21087 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21088 let replace_range =
21089 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21090
21091 let mut request =
21092 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21093 let completions = completions.clone();
21094 counter.fetch_add(1, atomic::Ordering::Release);
21095 async move {
21096 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21097 assert_eq!(
21098 params.text_document_position.position, complete_from_position,
21099 "marker `|` position doesn't match",
21100 );
21101 Ok(Some(lsp::CompletionResponse::Array(
21102 completions
21103 .iter()
21104 .map(|completion_text| lsp::CompletionItem {
21105 label: completion_text.to_string(),
21106 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21107 lsp::InsertReplaceEdit {
21108 insert: lsp::Range {
21109 start: replace_range.start,
21110 end: complete_from_position,
21111 },
21112 replace: replace_range,
21113 new_text: completion_text.to_string(),
21114 },
21115 )),
21116 ..Default::default()
21117 })
21118 .collect(),
21119 )))
21120 }
21121 });
21122
21123 async move {
21124 request.next().await;
21125 }
21126}
21127
21128fn handle_resolve_completion_request(
21129 cx: &mut EditorLspTestContext,
21130 edits: Option<Vec<(&'static str, &'static str)>>,
21131) -> impl Future<Output = ()> {
21132 let edits = edits.map(|edits| {
21133 edits
21134 .iter()
21135 .map(|(marked_string, new_text)| {
21136 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21137 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21138 lsp::TextEdit::new(replace_range, new_text.to_string())
21139 })
21140 .collect::<Vec<_>>()
21141 });
21142
21143 let mut request =
21144 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21145 let edits = edits.clone();
21146 async move {
21147 Ok(lsp::CompletionItem {
21148 additional_text_edits: edits,
21149 ..Default::default()
21150 })
21151 }
21152 });
21153
21154 async move {
21155 request.next().await;
21156 }
21157}
21158
21159pub(crate) fn update_test_language_settings(
21160 cx: &mut TestAppContext,
21161 f: impl Fn(&mut AllLanguageSettingsContent),
21162) {
21163 cx.update(|cx| {
21164 SettingsStore::update_global(cx, |store, cx| {
21165 store.update_user_settings::<AllLanguageSettings>(cx, f);
21166 });
21167 });
21168}
21169
21170pub(crate) fn update_test_project_settings(
21171 cx: &mut TestAppContext,
21172 f: impl Fn(&mut ProjectSettings),
21173) {
21174 cx.update(|cx| {
21175 SettingsStore::update_global(cx, |store, cx| {
21176 store.update_user_settings::<ProjectSettings>(cx, f);
21177 });
21178 });
21179}
21180
21181pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
21182 cx.update(|cx| {
21183 assets::Assets.load_test_fonts(cx);
21184 let store = SettingsStore::test(cx);
21185 cx.set_global(store);
21186 theme::init(theme::LoadThemes::JustBase, cx);
21187 release_channel::init(SemanticVersion::default(), cx);
21188 client::init_settings(cx);
21189 language::init(cx);
21190 Project::init_settings(cx);
21191 workspace::init_settings(cx);
21192 crate::init(cx);
21193 });
21194
21195 update_test_language_settings(cx, f);
21196}
21197
21198#[track_caller]
21199fn assert_hunk_revert(
21200 not_reverted_text_with_selections: &str,
21201 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21202 expected_reverted_text_with_selections: &str,
21203 base_text: &str,
21204 cx: &mut EditorLspTestContext,
21205) {
21206 cx.set_state(not_reverted_text_with_selections);
21207 cx.set_head_text(base_text);
21208 cx.executor().run_until_parked();
21209
21210 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21211 let snapshot = editor.snapshot(window, cx);
21212 let reverted_hunk_statuses = snapshot
21213 .buffer_snapshot
21214 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21215 .map(|hunk| hunk.status().kind)
21216 .collect::<Vec<_>>();
21217
21218 editor.git_restore(&Default::default(), window, cx);
21219 reverted_hunk_statuses
21220 });
21221 cx.executor().run_until_parked();
21222 cx.assert_editor_state(expected_reverted_text_with_selections);
21223 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21224}