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![]))
9115 })
9116 .next()
9117 .await;
9118 cx.executor().start_waiting();
9119 save.await;
9120}
9121
9122#[gpui::test]
9123async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9124 init_test(cx, |settings| {
9125 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9126 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9127 ))
9128 });
9129
9130 let fs = FakeFs::new(cx.executor());
9131 fs.insert_file(path!("/file.rs"), Default::default()).await;
9132
9133 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9134
9135 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9136 language_registry.add(Arc::new(Language::new(
9137 LanguageConfig {
9138 name: "Rust".into(),
9139 matcher: LanguageMatcher {
9140 path_suffixes: vec!["rs".to_string()],
9141 ..Default::default()
9142 },
9143 ..LanguageConfig::default()
9144 },
9145 Some(tree_sitter_rust::LANGUAGE.into()),
9146 )));
9147 update_test_language_settings(cx, |settings| {
9148 // Enable Prettier formatting for the same buffer, and ensure
9149 // LSP is called instead of Prettier.
9150 settings.defaults.prettier = Some(PrettierSettings {
9151 allowed: true,
9152 ..PrettierSettings::default()
9153 });
9154 });
9155 let mut fake_servers = language_registry.register_fake_lsp(
9156 "Rust",
9157 FakeLspAdapter {
9158 capabilities: lsp::ServerCapabilities {
9159 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9160 ..Default::default()
9161 },
9162 ..Default::default()
9163 },
9164 );
9165
9166 let buffer = project
9167 .update(cx, |project, cx| {
9168 project.open_local_buffer(path!("/file.rs"), cx)
9169 })
9170 .await
9171 .unwrap();
9172
9173 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9174 let (editor, cx) = cx.add_window_view(|window, cx| {
9175 build_editor_with_project(project.clone(), buffer, window, cx)
9176 });
9177 editor.update_in(cx, |editor, window, cx| {
9178 editor.set_text("one\ntwo\nthree\n", window, cx)
9179 });
9180
9181 cx.executor().start_waiting();
9182 let fake_server = fake_servers.next().await.unwrap();
9183
9184 let format = editor
9185 .update_in(cx, |editor, window, cx| {
9186 editor.perform_format(
9187 project.clone(),
9188 FormatTrigger::Manual,
9189 FormatTarget::Buffers,
9190 window,
9191 cx,
9192 )
9193 })
9194 .unwrap();
9195 fake_server
9196 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9197 assert_eq!(
9198 params.text_document.uri,
9199 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9200 );
9201 assert_eq!(params.options.tab_size, 4);
9202 Ok(Some(vec![lsp::TextEdit::new(
9203 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9204 ", ".to_string(),
9205 )]))
9206 })
9207 .next()
9208 .await;
9209 cx.executor().start_waiting();
9210 format.await;
9211 assert_eq!(
9212 editor.update(cx, |editor, cx| editor.text(cx)),
9213 "one, two\nthree\n"
9214 );
9215
9216 editor.update_in(cx, |editor, window, cx| {
9217 editor.set_text("one\ntwo\nthree\n", window, cx)
9218 });
9219 // Ensure we don't lock if formatting hangs.
9220 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9221 move |params, _| async move {
9222 assert_eq!(
9223 params.text_document.uri,
9224 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9225 );
9226 futures::future::pending::<()>().await;
9227 unreachable!()
9228 },
9229 );
9230 let format = editor
9231 .update_in(cx, |editor, window, cx| {
9232 editor.perform_format(
9233 project,
9234 FormatTrigger::Manual,
9235 FormatTarget::Buffers,
9236 window,
9237 cx,
9238 )
9239 })
9240 .unwrap();
9241 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9242 cx.executor().start_waiting();
9243 format.await;
9244 assert_eq!(
9245 editor.update(cx, |editor, cx| editor.text(cx)),
9246 "one\ntwo\nthree\n"
9247 );
9248}
9249
9250#[gpui::test]
9251async fn test_multiple_formatters(cx: &mut TestAppContext) {
9252 init_test(cx, |settings| {
9253 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9254 settings.defaults.formatter =
9255 Some(language_settings::SelectedFormatter::List(FormatterList(
9256 vec![
9257 Formatter::LanguageServer { name: None },
9258 Formatter::CodeActions(
9259 [
9260 ("code-action-1".into(), true),
9261 ("code-action-2".into(), true),
9262 ]
9263 .into_iter()
9264 .collect(),
9265 ),
9266 ]
9267 .into(),
9268 )))
9269 });
9270
9271 let fs = FakeFs::new(cx.executor());
9272 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9273 .await;
9274
9275 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9276 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9277 language_registry.add(rust_lang());
9278
9279 let mut fake_servers = language_registry.register_fake_lsp(
9280 "Rust",
9281 FakeLspAdapter {
9282 capabilities: lsp::ServerCapabilities {
9283 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9284 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9285 commands: vec!["the-command-for-code-action-1".into()],
9286 ..Default::default()
9287 }),
9288 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9289 ..Default::default()
9290 },
9291 ..Default::default()
9292 },
9293 );
9294
9295 let buffer = project
9296 .update(cx, |project, cx| {
9297 project.open_local_buffer(path!("/file.rs"), cx)
9298 })
9299 .await
9300 .unwrap();
9301
9302 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9303 let (editor, cx) = cx.add_window_view(|window, cx| {
9304 build_editor_with_project(project.clone(), buffer, window, cx)
9305 });
9306
9307 cx.executor().start_waiting();
9308
9309 let fake_server = fake_servers.next().await.unwrap();
9310 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9311 move |_params, _| async move {
9312 Ok(Some(vec![lsp::TextEdit::new(
9313 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9314 "applied-formatting\n".to_string(),
9315 )]))
9316 },
9317 );
9318 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9319 move |params, _| async move {
9320 assert_eq!(
9321 params.context.only,
9322 Some(vec!["code-action-1".into(), "code-action-2".into()])
9323 );
9324 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9325 Ok(Some(vec![
9326 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9327 kind: Some("code-action-1".into()),
9328 edit: Some(lsp::WorkspaceEdit::new(
9329 [(
9330 uri.clone(),
9331 vec![lsp::TextEdit::new(
9332 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9333 "applied-code-action-1-edit\n".to_string(),
9334 )],
9335 )]
9336 .into_iter()
9337 .collect(),
9338 )),
9339 command: Some(lsp::Command {
9340 command: "the-command-for-code-action-1".into(),
9341 ..Default::default()
9342 }),
9343 ..Default::default()
9344 }),
9345 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9346 kind: Some("code-action-2".into()),
9347 edit: Some(lsp::WorkspaceEdit::new(
9348 [(
9349 uri.clone(),
9350 vec![lsp::TextEdit::new(
9351 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9352 "applied-code-action-2-edit\n".to_string(),
9353 )],
9354 )]
9355 .into_iter()
9356 .collect(),
9357 )),
9358 ..Default::default()
9359 }),
9360 ]))
9361 },
9362 );
9363
9364 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9365 move |params, _| async move { Ok(params) }
9366 });
9367
9368 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9369 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9370 let fake = fake_server.clone();
9371 let lock = command_lock.clone();
9372 move |params, _| {
9373 assert_eq!(params.command, "the-command-for-code-action-1");
9374 let fake = fake.clone();
9375 let lock = lock.clone();
9376 async move {
9377 lock.lock().await;
9378 fake.server
9379 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9380 label: None,
9381 edit: lsp::WorkspaceEdit {
9382 changes: Some(
9383 [(
9384 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9385 vec![lsp::TextEdit {
9386 range: lsp::Range::new(
9387 lsp::Position::new(0, 0),
9388 lsp::Position::new(0, 0),
9389 ),
9390 new_text: "applied-code-action-1-command\n".into(),
9391 }],
9392 )]
9393 .into_iter()
9394 .collect(),
9395 ),
9396 ..Default::default()
9397 },
9398 })
9399 .await
9400 .into_response()
9401 .unwrap();
9402 Ok(Some(json!(null)))
9403 }
9404 }
9405 });
9406
9407 cx.executor().start_waiting();
9408 editor
9409 .update_in(cx, |editor, window, cx| {
9410 editor.perform_format(
9411 project.clone(),
9412 FormatTrigger::Manual,
9413 FormatTarget::Buffers,
9414 window,
9415 cx,
9416 )
9417 })
9418 .unwrap()
9419 .await;
9420 editor.update(cx, |editor, cx| {
9421 assert_eq!(
9422 editor.text(cx),
9423 r#"
9424 applied-code-action-2-edit
9425 applied-code-action-1-command
9426 applied-code-action-1-edit
9427 applied-formatting
9428 one
9429 two
9430 three
9431 "#
9432 .unindent()
9433 );
9434 });
9435
9436 editor.update_in(cx, |editor, window, cx| {
9437 editor.undo(&Default::default(), window, cx);
9438 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9439 });
9440
9441 // Perform a manual edit while waiting for an LSP command
9442 // that's being run as part of a formatting code action.
9443 let lock_guard = command_lock.lock().await;
9444 let format = editor
9445 .update_in(cx, |editor, window, cx| {
9446 editor.perform_format(
9447 project.clone(),
9448 FormatTrigger::Manual,
9449 FormatTarget::Buffers,
9450 window,
9451 cx,
9452 )
9453 })
9454 .unwrap();
9455 cx.run_until_parked();
9456 editor.update(cx, |editor, cx| {
9457 assert_eq!(
9458 editor.text(cx),
9459 r#"
9460 applied-code-action-1-edit
9461 applied-formatting
9462 one
9463 two
9464 three
9465 "#
9466 .unindent()
9467 );
9468
9469 editor.buffer.update(cx, |buffer, cx| {
9470 let ix = buffer.len(cx);
9471 buffer.edit([(ix..ix, "edited\n")], None, cx);
9472 });
9473 });
9474
9475 // Allow the LSP command to proceed. Because the buffer was edited,
9476 // the second code action will not be run.
9477 drop(lock_guard);
9478 format.await;
9479 editor.update_in(cx, |editor, window, cx| {
9480 assert_eq!(
9481 editor.text(cx),
9482 r#"
9483 applied-code-action-1-command
9484 applied-code-action-1-edit
9485 applied-formatting
9486 one
9487 two
9488 three
9489 edited
9490 "#
9491 .unindent()
9492 );
9493
9494 // The manual edit is undone first, because it is the last thing the user did
9495 // (even though the command completed afterwards).
9496 editor.undo(&Default::default(), window, cx);
9497 assert_eq!(
9498 editor.text(cx),
9499 r#"
9500 applied-code-action-1-command
9501 applied-code-action-1-edit
9502 applied-formatting
9503 one
9504 two
9505 three
9506 "#
9507 .unindent()
9508 );
9509
9510 // All the formatting (including the command, which completed after the manual edit)
9511 // is undone together.
9512 editor.undo(&Default::default(), window, cx);
9513 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9514 });
9515}
9516
9517#[gpui::test]
9518async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9519 init_test(cx, |settings| {
9520 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9521 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9522 ))
9523 });
9524
9525 let fs = FakeFs::new(cx.executor());
9526 fs.insert_file(path!("/file.ts"), Default::default()).await;
9527
9528 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9529
9530 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9531 language_registry.add(Arc::new(Language::new(
9532 LanguageConfig {
9533 name: "TypeScript".into(),
9534 matcher: LanguageMatcher {
9535 path_suffixes: vec!["ts".to_string()],
9536 ..Default::default()
9537 },
9538 ..LanguageConfig::default()
9539 },
9540 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9541 )));
9542 update_test_language_settings(cx, |settings| {
9543 settings.defaults.prettier = Some(PrettierSettings {
9544 allowed: true,
9545 ..PrettierSettings::default()
9546 });
9547 });
9548 let mut fake_servers = language_registry.register_fake_lsp(
9549 "TypeScript",
9550 FakeLspAdapter {
9551 capabilities: lsp::ServerCapabilities {
9552 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9553 ..Default::default()
9554 },
9555 ..Default::default()
9556 },
9557 );
9558
9559 let buffer = project
9560 .update(cx, |project, cx| {
9561 project.open_local_buffer(path!("/file.ts"), cx)
9562 })
9563 .await
9564 .unwrap();
9565
9566 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9567 let (editor, cx) = cx.add_window_view(|window, cx| {
9568 build_editor_with_project(project.clone(), buffer, window, cx)
9569 });
9570 editor.update_in(cx, |editor, window, cx| {
9571 editor.set_text(
9572 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9573 window,
9574 cx,
9575 )
9576 });
9577
9578 cx.executor().start_waiting();
9579 let fake_server = fake_servers.next().await.unwrap();
9580
9581 let format = editor
9582 .update_in(cx, |editor, window, cx| {
9583 editor.perform_code_action_kind(
9584 project.clone(),
9585 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9586 window,
9587 cx,
9588 )
9589 })
9590 .unwrap();
9591 fake_server
9592 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9593 assert_eq!(
9594 params.text_document.uri,
9595 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9596 );
9597 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9598 lsp::CodeAction {
9599 title: "Organize Imports".to_string(),
9600 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9601 edit: Some(lsp::WorkspaceEdit {
9602 changes: Some(
9603 [(
9604 params.text_document.uri.clone(),
9605 vec![lsp::TextEdit::new(
9606 lsp::Range::new(
9607 lsp::Position::new(1, 0),
9608 lsp::Position::new(2, 0),
9609 ),
9610 "".to_string(),
9611 )],
9612 )]
9613 .into_iter()
9614 .collect(),
9615 ),
9616 ..Default::default()
9617 }),
9618 ..Default::default()
9619 },
9620 )]))
9621 })
9622 .next()
9623 .await;
9624 cx.executor().start_waiting();
9625 format.await;
9626 assert_eq!(
9627 editor.update(cx, |editor, cx| editor.text(cx)),
9628 "import { a } from 'module';\n\nconst x = a;\n"
9629 );
9630
9631 editor.update_in(cx, |editor, window, cx| {
9632 editor.set_text(
9633 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9634 window,
9635 cx,
9636 )
9637 });
9638 // Ensure we don't lock if code action hangs.
9639 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9640 move |params, _| async move {
9641 assert_eq!(
9642 params.text_document.uri,
9643 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9644 );
9645 futures::future::pending::<()>().await;
9646 unreachable!()
9647 },
9648 );
9649 let format = editor
9650 .update_in(cx, |editor, window, cx| {
9651 editor.perform_code_action_kind(
9652 project,
9653 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9654 window,
9655 cx,
9656 )
9657 })
9658 .unwrap();
9659 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9660 cx.executor().start_waiting();
9661 format.await;
9662 assert_eq!(
9663 editor.update(cx, |editor, cx| editor.text(cx)),
9664 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9665 );
9666}
9667
9668#[gpui::test]
9669async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9670 init_test(cx, |_| {});
9671
9672 let mut cx = EditorLspTestContext::new_rust(
9673 lsp::ServerCapabilities {
9674 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9675 ..Default::default()
9676 },
9677 cx,
9678 )
9679 .await;
9680
9681 cx.set_state(indoc! {"
9682 one.twoˇ
9683 "});
9684
9685 // The format request takes a long time. When it completes, it inserts
9686 // a newline and an indent before the `.`
9687 cx.lsp
9688 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9689 let executor = cx.background_executor().clone();
9690 async move {
9691 executor.timer(Duration::from_millis(100)).await;
9692 Ok(Some(vec![lsp::TextEdit {
9693 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9694 new_text: "\n ".into(),
9695 }]))
9696 }
9697 });
9698
9699 // Submit a format request.
9700 let format_1 = cx
9701 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9702 .unwrap();
9703 cx.executor().run_until_parked();
9704
9705 // Submit a second format request.
9706 let format_2 = cx
9707 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9708 .unwrap();
9709 cx.executor().run_until_parked();
9710
9711 // Wait for both format requests to complete
9712 cx.executor().advance_clock(Duration::from_millis(200));
9713 cx.executor().start_waiting();
9714 format_1.await.unwrap();
9715 cx.executor().start_waiting();
9716 format_2.await.unwrap();
9717
9718 // The formatting edits only happens once.
9719 cx.assert_editor_state(indoc! {"
9720 one
9721 .twoˇ
9722 "});
9723}
9724
9725#[gpui::test]
9726async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9727 init_test(cx, |settings| {
9728 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9729 });
9730
9731 let mut cx = EditorLspTestContext::new_rust(
9732 lsp::ServerCapabilities {
9733 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9734 ..Default::default()
9735 },
9736 cx,
9737 )
9738 .await;
9739
9740 // Set up a buffer white some trailing whitespace and no trailing newline.
9741 cx.set_state(
9742 &[
9743 "one ", //
9744 "twoˇ", //
9745 "three ", //
9746 "four", //
9747 ]
9748 .join("\n"),
9749 );
9750
9751 // Submit a format request.
9752 let format = cx
9753 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9754 .unwrap();
9755
9756 // Record which buffer changes have been sent to the language server
9757 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9758 cx.lsp
9759 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9760 let buffer_changes = buffer_changes.clone();
9761 move |params, _| {
9762 buffer_changes.lock().extend(
9763 params
9764 .content_changes
9765 .into_iter()
9766 .map(|e| (e.range.unwrap(), e.text)),
9767 );
9768 }
9769 });
9770
9771 // Handle formatting requests to the language server.
9772 cx.lsp
9773 .set_request_handler::<lsp::request::Formatting, _, _>({
9774 let buffer_changes = buffer_changes.clone();
9775 move |_, _| {
9776 // When formatting is requested, trailing whitespace has already been stripped,
9777 // and the trailing newline has already been added.
9778 assert_eq!(
9779 &buffer_changes.lock()[1..],
9780 &[
9781 (
9782 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9783 "".into()
9784 ),
9785 (
9786 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9787 "".into()
9788 ),
9789 (
9790 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9791 "\n".into()
9792 ),
9793 ]
9794 );
9795
9796 // Insert blank lines between each line of the buffer.
9797 async move {
9798 Ok(Some(vec![
9799 lsp::TextEdit {
9800 range: lsp::Range::new(
9801 lsp::Position::new(1, 0),
9802 lsp::Position::new(1, 0),
9803 ),
9804 new_text: "\n".into(),
9805 },
9806 lsp::TextEdit {
9807 range: lsp::Range::new(
9808 lsp::Position::new(2, 0),
9809 lsp::Position::new(2, 0),
9810 ),
9811 new_text: "\n".into(),
9812 },
9813 ]))
9814 }
9815 }
9816 });
9817
9818 // After formatting the buffer, the trailing whitespace is stripped,
9819 // a newline is appended, and the edits provided by the language server
9820 // have been applied.
9821 format.await.unwrap();
9822 cx.assert_editor_state(
9823 &[
9824 "one", //
9825 "", //
9826 "twoˇ", //
9827 "", //
9828 "three", //
9829 "four", //
9830 "", //
9831 ]
9832 .join("\n"),
9833 );
9834
9835 // Undoing the formatting undoes the trailing whitespace removal, the
9836 // trailing newline, and the LSP edits.
9837 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9838 cx.assert_editor_state(
9839 &[
9840 "one ", //
9841 "twoˇ", //
9842 "three ", //
9843 "four", //
9844 ]
9845 .join("\n"),
9846 );
9847}
9848
9849#[gpui::test]
9850async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9851 cx: &mut TestAppContext,
9852) {
9853 init_test(cx, |_| {});
9854
9855 cx.update(|cx| {
9856 cx.update_global::<SettingsStore, _>(|settings, cx| {
9857 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9858 settings.auto_signature_help = Some(true);
9859 });
9860 });
9861 });
9862
9863 let mut cx = EditorLspTestContext::new_rust(
9864 lsp::ServerCapabilities {
9865 signature_help_provider: Some(lsp::SignatureHelpOptions {
9866 ..Default::default()
9867 }),
9868 ..Default::default()
9869 },
9870 cx,
9871 )
9872 .await;
9873
9874 let language = Language::new(
9875 LanguageConfig {
9876 name: "Rust".into(),
9877 brackets: BracketPairConfig {
9878 pairs: vec![
9879 BracketPair {
9880 start: "{".to_string(),
9881 end: "}".to_string(),
9882 close: true,
9883 surround: true,
9884 newline: true,
9885 },
9886 BracketPair {
9887 start: "(".to_string(),
9888 end: ")".to_string(),
9889 close: true,
9890 surround: true,
9891 newline: true,
9892 },
9893 BracketPair {
9894 start: "/*".to_string(),
9895 end: " */".to_string(),
9896 close: true,
9897 surround: true,
9898 newline: true,
9899 },
9900 BracketPair {
9901 start: "[".to_string(),
9902 end: "]".to_string(),
9903 close: false,
9904 surround: false,
9905 newline: true,
9906 },
9907 BracketPair {
9908 start: "\"".to_string(),
9909 end: "\"".to_string(),
9910 close: true,
9911 surround: true,
9912 newline: false,
9913 },
9914 BracketPair {
9915 start: "<".to_string(),
9916 end: ">".to_string(),
9917 close: false,
9918 surround: true,
9919 newline: true,
9920 },
9921 ],
9922 ..Default::default()
9923 },
9924 autoclose_before: "})]".to_string(),
9925 ..Default::default()
9926 },
9927 Some(tree_sitter_rust::LANGUAGE.into()),
9928 );
9929 let language = Arc::new(language);
9930
9931 cx.language_registry().add(language.clone());
9932 cx.update_buffer(|buffer, cx| {
9933 buffer.set_language(Some(language), cx);
9934 });
9935
9936 cx.set_state(
9937 &r#"
9938 fn main() {
9939 sampleˇ
9940 }
9941 "#
9942 .unindent(),
9943 );
9944
9945 cx.update_editor(|editor, window, cx| {
9946 editor.handle_input("(", window, cx);
9947 });
9948 cx.assert_editor_state(
9949 &"
9950 fn main() {
9951 sample(ˇ)
9952 }
9953 "
9954 .unindent(),
9955 );
9956
9957 let mocked_response = lsp::SignatureHelp {
9958 signatures: vec![lsp::SignatureInformation {
9959 label: "fn sample(param1: u8, param2: u8)".to_string(),
9960 documentation: None,
9961 parameters: Some(vec![
9962 lsp::ParameterInformation {
9963 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9964 documentation: None,
9965 },
9966 lsp::ParameterInformation {
9967 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9968 documentation: None,
9969 },
9970 ]),
9971 active_parameter: None,
9972 }],
9973 active_signature: Some(0),
9974 active_parameter: Some(0),
9975 };
9976 handle_signature_help_request(&mut cx, mocked_response).await;
9977
9978 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9979 .await;
9980
9981 cx.editor(|editor, _, _| {
9982 let signature_help_state = editor.signature_help_state.popover().cloned();
9983 assert_eq!(
9984 signature_help_state.unwrap().label,
9985 "param1: u8, param2: u8"
9986 );
9987 });
9988}
9989
9990#[gpui::test]
9991async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9992 init_test(cx, |_| {});
9993
9994 cx.update(|cx| {
9995 cx.update_global::<SettingsStore, _>(|settings, cx| {
9996 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9997 settings.auto_signature_help = Some(false);
9998 settings.show_signature_help_after_edits = Some(false);
9999 });
10000 });
10001 });
10002
10003 let mut cx = EditorLspTestContext::new_rust(
10004 lsp::ServerCapabilities {
10005 signature_help_provider: Some(lsp::SignatureHelpOptions {
10006 ..Default::default()
10007 }),
10008 ..Default::default()
10009 },
10010 cx,
10011 )
10012 .await;
10013
10014 let language = Language::new(
10015 LanguageConfig {
10016 name: "Rust".into(),
10017 brackets: BracketPairConfig {
10018 pairs: vec![
10019 BracketPair {
10020 start: "{".to_string(),
10021 end: "}".to_string(),
10022 close: true,
10023 surround: true,
10024 newline: true,
10025 },
10026 BracketPair {
10027 start: "(".to_string(),
10028 end: ")".to_string(),
10029 close: true,
10030 surround: true,
10031 newline: true,
10032 },
10033 BracketPair {
10034 start: "/*".to_string(),
10035 end: " */".to_string(),
10036 close: true,
10037 surround: true,
10038 newline: true,
10039 },
10040 BracketPair {
10041 start: "[".to_string(),
10042 end: "]".to_string(),
10043 close: false,
10044 surround: false,
10045 newline: true,
10046 },
10047 BracketPair {
10048 start: "\"".to_string(),
10049 end: "\"".to_string(),
10050 close: true,
10051 surround: true,
10052 newline: false,
10053 },
10054 BracketPair {
10055 start: "<".to_string(),
10056 end: ">".to_string(),
10057 close: false,
10058 surround: true,
10059 newline: true,
10060 },
10061 ],
10062 ..Default::default()
10063 },
10064 autoclose_before: "})]".to_string(),
10065 ..Default::default()
10066 },
10067 Some(tree_sitter_rust::LANGUAGE.into()),
10068 );
10069 let language = Arc::new(language);
10070
10071 cx.language_registry().add(language.clone());
10072 cx.update_buffer(|buffer, cx| {
10073 buffer.set_language(Some(language), cx);
10074 });
10075
10076 // Ensure that signature_help is not called when no signature help is enabled.
10077 cx.set_state(
10078 &r#"
10079 fn main() {
10080 sampleˇ
10081 }
10082 "#
10083 .unindent(),
10084 );
10085 cx.update_editor(|editor, window, cx| {
10086 editor.handle_input("(", window, cx);
10087 });
10088 cx.assert_editor_state(
10089 &"
10090 fn main() {
10091 sample(ˇ)
10092 }
10093 "
10094 .unindent(),
10095 );
10096 cx.editor(|editor, _, _| {
10097 assert!(editor.signature_help_state.task().is_none());
10098 });
10099
10100 let mocked_response = lsp::SignatureHelp {
10101 signatures: vec![lsp::SignatureInformation {
10102 label: "fn sample(param1: u8, param2: u8)".to_string(),
10103 documentation: None,
10104 parameters: Some(vec![
10105 lsp::ParameterInformation {
10106 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10107 documentation: None,
10108 },
10109 lsp::ParameterInformation {
10110 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10111 documentation: None,
10112 },
10113 ]),
10114 active_parameter: None,
10115 }],
10116 active_signature: Some(0),
10117 active_parameter: Some(0),
10118 };
10119
10120 // Ensure that signature_help is called when enabled afte edits
10121 cx.update(|_, cx| {
10122 cx.update_global::<SettingsStore, _>(|settings, cx| {
10123 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10124 settings.auto_signature_help = Some(false);
10125 settings.show_signature_help_after_edits = Some(true);
10126 });
10127 });
10128 });
10129 cx.set_state(
10130 &r#"
10131 fn main() {
10132 sampleˇ
10133 }
10134 "#
10135 .unindent(),
10136 );
10137 cx.update_editor(|editor, window, cx| {
10138 editor.handle_input("(", window, cx);
10139 });
10140 cx.assert_editor_state(
10141 &"
10142 fn main() {
10143 sample(ˇ)
10144 }
10145 "
10146 .unindent(),
10147 );
10148 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10149 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10150 .await;
10151 cx.update_editor(|editor, _, _| {
10152 let signature_help_state = editor.signature_help_state.popover().cloned();
10153 assert!(signature_help_state.is_some());
10154 assert_eq!(
10155 signature_help_state.unwrap().label,
10156 "param1: u8, param2: u8"
10157 );
10158 editor.signature_help_state = SignatureHelpState::default();
10159 });
10160
10161 // Ensure that signature_help is called when auto signature help override is enabled
10162 cx.update(|_, cx| {
10163 cx.update_global::<SettingsStore, _>(|settings, cx| {
10164 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10165 settings.auto_signature_help = Some(true);
10166 settings.show_signature_help_after_edits = Some(false);
10167 });
10168 });
10169 });
10170 cx.set_state(
10171 &r#"
10172 fn main() {
10173 sampleˇ
10174 }
10175 "#
10176 .unindent(),
10177 );
10178 cx.update_editor(|editor, window, cx| {
10179 editor.handle_input("(", window, cx);
10180 });
10181 cx.assert_editor_state(
10182 &"
10183 fn main() {
10184 sample(ˇ)
10185 }
10186 "
10187 .unindent(),
10188 );
10189 handle_signature_help_request(&mut cx, mocked_response).await;
10190 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10191 .await;
10192 cx.editor(|editor, _, _| {
10193 let signature_help_state = editor.signature_help_state.popover().cloned();
10194 assert!(signature_help_state.is_some());
10195 assert_eq!(
10196 signature_help_state.unwrap().label,
10197 "param1: u8, param2: u8"
10198 );
10199 });
10200}
10201
10202#[gpui::test]
10203async fn test_signature_help(cx: &mut TestAppContext) {
10204 init_test(cx, |_| {});
10205 cx.update(|cx| {
10206 cx.update_global::<SettingsStore, _>(|settings, cx| {
10207 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10208 settings.auto_signature_help = Some(true);
10209 });
10210 });
10211 });
10212
10213 let mut cx = EditorLspTestContext::new_rust(
10214 lsp::ServerCapabilities {
10215 signature_help_provider: Some(lsp::SignatureHelpOptions {
10216 ..Default::default()
10217 }),
10218 ..Default::default()
10219 },
10220 cx,
10221 )
10222 .await;
10223
10224 // A test that directly calls `show_signature_help`
10225 cx.update_editor(|editor, window, cx| {
10226 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10227 });
10228
10229 let mocked_response = lsp::SignatureHelp {
10230 signatures: vec![lsp::SignatureInformation {
10231 label: "fn sample(param1: u8, param2: u8)".to_string(),
10232 documentation: None,
10233 parameters: Some(vec![
10234 lsp::ParameterInformation {
10235 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10236 documentation: None,
10237 },
10238 lsp::ParameterInformation {
10239 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10240 documentation: None,
10241 },
10242 ]),
10243 active_parameter: None,
10244 }],
10245 active_signature: Some(0),
10246 active_parameter: Some(0),
10247 };
10248 handle_signature_help_request(&mut cx, mocked_response).await;
10249
10250 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10251 .await;
10252
10253 cx.editor(|editor, _, _| {
10254 let signature_help_state = editor.signature_help_state.popover().cloned();
10255 assert!(signature_help_state.is_some());
10256 assert_eq!(
10257 signature_help_state.unwrap().label,
10258 "param1: u8, param2: u8"
10259 );
10260 });
10261
10262 // When exiting outside from inside the brackets, `signature_help` is closed.
10263 cx.set_state(indoc! {"
10264 fn main() {
10265 sample(ˇ);
10266 }
10267
10268 fn sample(param1: u8, param2: u8) {}
10269 "});
10270
10271 cx.update_editor(|editor, window, cx| {
10272 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10273 });
10274
10275 let mocked_response = lsp::SignatureHelp {
10276 signatures: Vec::new(),
10277 active_signature: None,
10278 active_parameter: None,
10279 };
10280 handle_signature_help_request(&mut cx, mocked_response).await;
10281
10282 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10283 .await;
10284
10285 cx.editor(|editor, _, _| {
10286 assert!(!editor.signature_help_state.is_shown());
10287 });
10288
10289 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10290 cx.set_state(indoc! {"
10291 fn main() {
10292 sample(ˇ);
10293 }
10294
10295 fn sample(param1: u8, param2: u8) {}
10296 "});
10297
10298 let mocked_response = lsp::SignatureHelp {
10299 signatures: vec![lsp::SignatureInformation {
10300 label: "fn sample(param1: u8, param2: u8)".to_string(),
10301 documentation: None,
10302 parameters: Some(vec![
10303 lsp::ParameterInformation {
10304 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10305 documentation: None,
10306 },
10307 lsp::ParameterInformation {
10308 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10309 documentation: None,
10310 },
10311 ]),
10312 active_parameter: None,
10313 }],
10314 active_signature: Some(0),
10315 active_parameter: Some(0),
10316 };
10317 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10318 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10319 .await;
10320 cx.editor(|editor, _, _| {
10321 assert!(editor.signature_help_state.is_shown());
10322 });
10323
10324 // Restore the popover with more parameter input
10325 cx.set_state(indoc! {"
10326 fn main() {
10327 sample(param1, param2ˇ);
10328 }
10329
10330 fn sample(param1: u8, param2: u8) {}
10331 "});
10332
10333 let mocked_response = lsp::SignatureHelp {
10334 signatures: vec![lsp::SignatureInformation {
10335 label: "fn sample(param1: u8, param2: u8)".to_string(),
10336 documentation: None,
10337 parameters: Some(vec![
10338 lsp::ParameterInformation {
10339 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10340 documentation: None,
10341 },
10342 lsp::ParameterInformation {
10343 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10344 documentation: None,
10345 },
10346 ]),
10347 active_parameter: None,
10348 }],
10349 active_signature: Some(0),
10350 active_parameter: Some(1),
10351 };
10352 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10353 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10354 .await;
10355
10356 // When selecting a range, the popover is gone.
10357 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10358 cx.update_editor(|editor, window, cx| {
10359 editor.change_selections(None, window, cx, |s| {
10360 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10361 })
10362 });
10363 cx.assert_editor_state(indoc! {"
10364 fn main() {
10365 sample(param1, «ˇparam2»);
10366 }
10367
10368 fn sample(param1: u8, param2: u8) {}
10369 "});
10370 cx.editor(|editor, _, _| {
10371 assert!(!editor.signature_help_state.is_shown());
10372 });
10373
10374 // When unselecting again, the popover is back if within the brackets.
10375 cx.update_editor(|editor, window, cx| {
10376 editor.change_selections(None, window, cx, |s| {
10377 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10378 })
10379 });
10380 cx.assert_editor_state(indoc! {"
10381 fn main() {
10382 sample(param1, ˇparam2);
10383 }
10384
10385 fn sample(param1: u8, param2: u8) {}
10386 "});
10387 handle_signature_help_request(&mut cx, mocked_response).await;
10388 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10389 .await;
10390 cx.editor(|editor, _, _| {
10391 assert!(editor.signature_help_state.is_shown());
10392 });
10393
10394 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10395 cx.update_editor(|editor, window, cx| {
10396 editor.change_selections(None, window, cx, |s| {
10397 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10398 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10399 })
10400 });
10401 cx.assert_editor_state(indoc! {"
10402 fn main() {
10403 sample(param1, ˇparam2);
10404 }
10405
10406 fn sample(param1: u8, param2: u8) {}
10407 "});
10408
10409 let mocked_response = lsp::SignatureHelp {
10410 signatures: vec![lsp::SignatureInformation {
10411 label: "fn sample(param1: u8, param2: u8)".to_string(),
10412 documentation: None,
10413 parameters: Some(vec![
10414 lsp::ParameterInformation {
10415 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10416 documentation: None,
10417 },
10418 lsp::ParameterInformation {
10419 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10420 documentation: None,
10421 },
10422 ]),
10423 active_parameter: None,
10424 }],
10425 active_signature: Some(0),
10426 active_parameter: Some(1),
10427 };
10428 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10429 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10430 .await;
10431 cx.update_editor(|editor, _, cx| {
10432 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10433 });
10434 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10435 .await;
10436 cx.update_editor(|editor, window, cx| {
10437 editor.change_selections(None, window, cx, |s| {
10438 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10439 })
10440 });
10441 cx.assert_editor_state(indoc! {"
10442 fn main() {
10443 sample(param1, «ˇparam2»);
10444 }
10445
10446 fn sample(param1: u8, param2: u8) {}
10447 "});
10448 cx.update_editor(|editor, window, cx| {
10449 editor.change_selections(None, window, cx, |s| {
10450 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10451 })
10452 });
10453 cx.assert_editor_state(indoc! {"
10454 fn main() {
10455 sample(param1, ˇparam2);
10456 }
10457
10458 fn sample(param1: u8, param2: u8) {}
10459 "});
10460 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10461 .await;
10462}
10463
10464#[gpui::test]
10465async fn test_completion_mode(cx: &mut TestAppContext) {
10466 init_test(cx, |_| {});
10467 let mut cx = EditorLspTestContext::new_rust(
10468 lsp::ServerCapabilities {
10469 completion_provider: Some(lsp::CompletionOptions {
10470 resolve_provider: Some(true),
10471 ..Default::default()
10472 }),
10473 ..Default::default()
10474 },
10475 cx,
10476 )
10477 .await;
10478
10479 struct Run {
10480 run_description: &'static str,
10481 initial_state: String,
10482 buffer_marked_text: String,
10483 completion_text: &'static str,
10484 expected_with_insert_mode: String,
10485 expected_with_replace_mode: String,
10486 expected_with_replace_subsequence_mode: String,
10487 expected_with_replace_suffix_mode: String,
10488 }
10489
10490 let runs = [
10491 Run {
10492 run_description: "Start of word matches completion text",
10493 initial_state: "before ediˇ after".into(),
10494 buffer_marked_text: "before <edi|> after".into(),
10495 completion_text: "editor",
10496 expected_with_insert_mode: "before editorˇ after".into(),
10497 expected_with_replace_mode: "before editorˇ after".into(),
10498 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10499 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10500 },
10501 Run {
10502 run_description: "Accept same text at the middle of the word",
10503 initial_state: "before ediˇtor after".into(),
10504 buffer_marked_text: "before <edi|tor> after".into(),
10505 completion_text: "editor",
10506 expected_with_insert_mode: "before editorˇtor after".into(),
10507 expected_with_replace_mode: "before editorˇ after".into(),
10508 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10509 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10510 },
10511 Run {
10512 run_description: "End of word matches completion text -- cursor at end",
10513 initial_state: "before torˇ after".into(),
10514 buffer_marked_text: "before <tor|> after".into(),
10515 completion_text: "editor",
10516 expected_with_insert_mode: "before editorˇ after".into(),
10517 expected_with_replace_mode: "before editorˇ after".into(),
10518 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10519 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10520 },
10521 Run {
10522 run_description: "End of word matches completion text -- cursor at start",
10523 initial_state: "before ˇtor after".into(),
10524 buffer_marked_text: "before <|tor> after".into(),
10525 completion_text: "editor",
10526 expected_with_insert_mode: "before editorˇtor after".into(),
10527 expected_with_replace_mode: "before editorˇ after".into(),
10528 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10529 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10530 },
10531 Run {
10532 run_description: "Prepend text containing whitespace",
10533 initial_state: "pˇfield: bool".into(),
10534 buffer_marked_text: "<p|field>: bool".into(),
10535 completion_text: "pub ",
10536 expected_with_insert_mode: "pub ˇfield: bool".into(),
10537 expected_with_replace_mode: "pub ˇ: bool".into(),
10538 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10539 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10540 },
10541 Run {
10542 run_description: "Add element to start of list",
10543 initial_state: "[element_ˇelement_2]".into(),
10544 buffer_marked_text: "[<element_|element_2>]".into(),
10545 completion_text: "element_1",
10546 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10547 expected_with_replace_mode: "[element_1ˇ]".into(),
10548 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10549 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10550 },
10551 Run {
10552 run_description: "Add element to start of list -- first and second elements are equal",
10553 initial_state: "[elˇelement]".into(),
10554 buffer_marked_text: "[<el|element>]".into(),
10555 completion_text: "element",
10556 expected_with_insert_mode: "[elementˇelement]".into(),
10557 expected_with_replace_mode: "[elementˇ]".into(),
10558 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10559 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10560 },
10561 Run {
10562 run_description: "Ends with matching suffix",
10563 initial_state: "SubˇError".into(),
10564 buffer_marked_text: "<Sub|Error>".into(),
10565 completion_text: "SubscriptionError",
10566 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10567 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10568 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10569 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10570 },
10571 Run {
10572 run_description: "Suffix is a subsequence -- contiguous",
10573 initial_state: "SubˇErr".into(),
10574 buffer_marked_text: "<Sub|Err>".into(),
10575 completion_text: "SubscriptionError",
10576 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10577 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10578 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10579 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10580 },
10581 Run {
10582 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10583 initial_state: "Suˇscrirr".into(),
10584 buffer_marked_text: "<Su|scrirr>".into(),
10585 completion_text: "SubscriptionError",
10586 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10587 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10588 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10589 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10590 },
10591 Run {
10592 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10593 initial_state: "foo(indˇix)".into(),
10594 buffer_marked_text: "foo(<ind|ix>)".into(),
10595 completion_text: "node_index",
10596 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10597 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10598 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10599 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10600 },
10601 ];
10602
10603 for run in runs {
10604 let run_variations = [
10605 (LspInsertMode::Insert, run.expected_with_insert_mode),
10606 (LspInsertMode::Replace, run.expected_with_replace_mode),
10607 (
10608 LspInsertMode::ReplaceSubsequence,
10609 run.expected_with_replace_subsequence_mode,
10610 ),
10611 (
10612 LspInsertMode::ReplaceSuffix,
10613 run.expected_with_replace_suffix_mode,
10614 ),
10615 ];
10616
10617 for (lsp_insert_mode, expected_text) in run_variations {
10618 eprintln!(
10619 "run = {:?}, mode = {lsp_insert_mode:.?}",
10620 run.run_description,
10621 );
10622
10623 update_test_language_settings(&mut cx, |settings| {
10624 settings.defaults.completions = Some(CompletionSettings {
10625 lsp_insert_mode,
10626 words: WordsCompletionMode::Disabled,
10627 lsp: true,
10628 lsp_fetch_timeout_ms: 0,
10629 });
10630 });
10631
10632 cx.set_state(&run.initial_state);
10633 cx.update_editor(|editor, window, cx| {
10634 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10635 });
10636
10637 let counter = Arc::new(AtomicUsize::new(0));
10638 handle_completion_request_with_insert_and_replace(
10639 &mut cx,
10640 &run.buffer_marked_text,
10641 vec![run.completion_text],
10642 counter.clone(),
10643 )
10644 .await;
10645 cx.condition(|editor, _| editor.context_menu_visible())
10646 .await;
10647 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10648
10649 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10650 editor
10651 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10652 .unwrap()
10653 });
10654 cx.assert_editor_state(&expected_text);
10655 handle_resolve_completion_request(&mut cx, None).await;
10656 apply_additional_edits.await.unwrap();
10657 }
10658 }
10659}
10660
10661#[gpui::test]
10662async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10663 init_test(cx, |_| {});
10664 let mut cx = EditorLspTestContext::new_rust(
10665 lsp::ServerCapabilities {
10666 completion_provider: Some(lsp::CompletionOptions {
10667 resolve_provider: Some(true),
10668 ..Default::default()
10669 }),
10670 ..Default::default()
10671 },
10672 cx,
10673 )
10674 .await;
10675
10676 let initial_state = "SubˇError";
10677 let buffer_marked_text = "<Sub|Error>";
10678 let completion_text = "SubscriptionError";
10679 let expected_with_insert_mode = "SubscriptionErrorˇError";
10680 let expected_with_replace_mode = "SubscriptionErrorˇ";
10681
10682 update_test_language_settings(&mut cx, |settings| {
10683 settings.defaults.completions = Some(CompletionSettings {
10684 words: WordsCompletionMode::Disabled,
10685 // set the opposite here to ensure that the action is overriding the default behavior
10686 lsp_insert_mode: LspInsertMode::Insert,
10687 lsp: true,
10688 lsp_fetch_timeout_ms: 0,
10689 });
10690 });
10691
10692 cx.set_state(initial_state);
10693 cx.update_editor(|editor, window, cx| {
10694 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10695 });
10696
10697 let counter = Arc::new(AtomicUsize::new(0));
10698 handle_completion_request_with_insert_and_replace(
10699 &mut cx,
10700 &buffer_marked_text,
10701 vec![completion_text],
10702 counter.clone(),
10703 )
10704 .await;
10705 cx.condition(|editor, _| editor.context_menu_visible())
10706 .await;
10707 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10708
10709 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10710 editor
10711 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10712 .unwrap()
10713 });
10714 cx.assert_editor_state(&expected_with_replace_mode);
10715 handle_resolve_completion_request(&mut cx, None).await;
10716 apply_additional_edits.await.unwrap();
10717
10718 update_test_language_settings(&mut cx, |settings| {
10719 settings.defaults.completions = Some(CompletionSettings {
10720 words: WordsCompletionMode::Disabled,
10721 // set the opposite here to ensure that the action is overriding the default behavior
10722 lsp_insert_mode: LspInsertMode::Replace,
10723 lsp: true,
10724 lsp_fetch_timeout_ms: 0,
10725 });
10726 });
10727
10728 cx.set_state(initial_state);
10729 cx.update_editor(|editor, window, cx| {
10730 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10731 });
10732 handle_completion_request_with_insert_and_replace(
10733 &mut cx,
10734 &buffer_marked_text,
10735 vec![completion_text],
10736 counter.clone(),
10737 )
10738 .await;
10739 cx.condition(|editor, _| editor.context_menu_visible())
10740 .await;
10741 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10742
10743 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10744 editor
10745 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10746 .unwrap()
10747 });
10748 cx.assert_editor_state(&expected_with_insert_mode);
10749 handle_resolve_completion_request(&mut cx, None).await;
10750 apply_additional_edits.await.unwrap();
10751}
10752
10753#[gpui::test]
10754async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10755 init_test(cx, |_| {});
10756 let mut cx = EditorLspTestContext::new_rust(
10757 lsp::ServerCapabilities {
10758 completion_provider: Some(lsp::CompletionOptions {
10759 resolve_provider: Some(true),
10760 ..Default::default()
10761 }),
10762 ..Default::default()
10763 },
10764 cx,
10765 )
10766 .await;
10767
10768 // scenario: surrounding text matches completion text
10769 let completion_text = "to_offset";
10770 let initial_state = indoc! {"
10771 1. buf.to_offˇsuffix
10772 2. buf.to_offˇsuf
10773 3. buf.to_offˇfix
10774 4. buf.to_offˇ
10775 5. into_offˇensive
10776 6. ˇsuffix
10777 7. let ˇ //
10778 8. aaˇzz
10779 9. buf.to_off«zzzzzˇ»suffix
10780 10. buf.«ˇzzzzz»suffix
10781 11. to_off«ˇzzzzz»
10782
10783 buf.to_offˇsuffix // newest cursor
10784 "};
10785 let completion_marked_buffer = indoc! {"
10786 1. buf.to_offsuffix
10787 2. buf.to_offsuf
10788 3. buf.to_offfix
10789 4. buf.to_off
10790 5. into_offensive
10791 6. suffix
10792 7. let //
10793 8. aazz
10794 9. buf.to_offzzzzzsuffix
10795 10. buf.zzzzzsuffix
10796 11. to_offzzzzz
10797
10798 buf.<to_off|suffix> // newest cursor
10799 "};
10800 let expected = indoc! {"
10801 1. buf.to_offsetˇ
10802 2. buf.to_offsetˇsuf
10803 3. buf.to_offsetˇfix
10804 4. buf.to_offsetˇ
10805 5. into_offsetˇensive
10806 6. to_offsetˇsuffix
10807 7. let to_offsetˇ //
10808 8. aato_offsetˇzz
10809 9. buf.to_offsetˇ
10810 10. buf.to_offsetˇsuffix
10811 11. to_offsetˇ
10812
10813 buf.to_offsetˇ // newest cursor
10814 "};
10815 cx.set_state(initial_state);
10816 cx.update_editor(|editor, window, cx| {
10817 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10818 });
10819 handle_completion_request_with_insert_and_replace(
10820 &mut cx,
10821 completion_marked_buffer,
10822 vec![completion_text],
10823 Arc::new(AtomicUsize::new(0)),
10824 )
10825 .await;
10826 cx.condition(|editor, _| editor.context_menu_visible())
10827 .await;
10828 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10829 editor
10830 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10831 .unwrap()
10832 });
10833 cx.assert_editor_state(expected);
10834 handle_resolve_completion_request(&mut cx, None).await;
10835 apply_additional_edits.await.unwrap();
10836
10837 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10838 let completion_text = "foo_and_bar";
10839 let initial_state = indoc! {"
10840 1. ooanbˇ
10841 2. zooanbˇ
10842 3. ooanbˇz
10843 4. zooanbˇz
10844 5. ooanˇ
10845 6. oanbˇ
10846
10847 ooanbˇ
10848 "};
10849 let completion_marked_buffer = indoc! {"
10850 1. ooanb
10851 2. zooanb
10852 3. ooanbz
10853 4. zooanbz
10854 5. ooan
10855 6. oanb
10856
10857 <ooanb|>
10858 "};
10859 let expected = indoc! {"
10860 1. foo_and_barˇ
10861 2. zfoo_and_barˇ
10862 3. foo_and_barˇz
10863 4. zfoo_and_barˇz
10864 5. ooanfoo_and_barˇ
10865 6. oanbfoo_and_barˇ
10866
10867 foo_and_barˇ
10868 "};
10869 cx.set_state(initial_state);
10870 cx.update_editor(|editor, window, cx| {
10871 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10872 });
10873 handle_completion_request_with_insert_and_replace(
10874 &mut cx,
10875 completion_marked_buffer,
10876 vec![completion_text],
10877 Arc::new(AtomicUsize::new(0)),
10878 )
10879 .await;
10880 cx.condition(|editor, _| editor.context_menu_visible())
10881 .await;
10882 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10883 editor
10884 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10885 .unwrap()
10886 });
10887 cx.assert_editor_state(expected);
10888 handle_resolve_completion_request(&mut cx, None).await;
10889 apply_additional_edits.await.unwrap();
10890
10891 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10892 // (expects the same as if it was inserted at the end)
10893 let completion_text = "foo_and_bar";
10894 let initial_state = indoc! {"
10895 1. ooˇanb
10896 2. zooˇanb
10897 3. ooˇanbz
10898 4. zooˇanbz
10899
10900 ooˇanb
10901 "};
10902 let completion_marked_buffer = indoc! {"
10903 1. ooanb
10904 2. zooanb
10905 3. ooanbz
10906 4. zooanbz
10907
10908 <oo|anb>
10909 "};
10910 let expected = indoc! {"
10911 1. foo_and_barˇ
10912 2. zfoo_and_barˇ
10913 3. foo_and_barˇz
10914 4. zfoo_and_barˇz
10915
10916 foo_and_barˇ
10917 "};
10918 cx.set_state(initial_state);
10919 cx.update_editor(|editor, window, cx| {
10920 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10921 });
10922 handle_completion_request_with_insert_and_replace(
10923 &mut cx,
10924 completion_marked_buffer,
10925 vec![completion_text],
10926 Arc::new(AtomicUsize::new(0)),
10927 )
10928 .await;
10929 cx.condition(|editor, _| editor.context_menu_visible())
10930 .await;
10931 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10932 editor
10933 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10934 .unwrap()
10935 });
10936 cx.assert_editor_state(expected);
10937 handle_resolve_completion_request(&mut cx, None).await;
10938 apply_additional_edits.await.unwrap();
10939}
10940
10941// This used to crash
10942#[gpui::test]
10943async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10944 init_test(cx, |_| {});
10945
10946 let buffer_text = indoc! {"
10947 fn main() {
10948 10.satu;
10949
10950 //
10951 // separate cursors so they open in different excerpts (manually reproducible)
10952 //
10953
10954 10.satu20;
10955 }
10956 "};
10957 let multibuffer_text_with_selections = indoc! {"
10958 fn main() {
10959 10.satuˇ;
10960
10961 //
10962
10963 //
10964
10965 10.satuˇ20;
10966 }
10967 "};
10968 let expected_multibuffer = indoc! {"
10969 fn main() {
10970 10.saturating_sub()ˇ;
10971
10972 //
10973
10974 //
10975
10976 10.saturating_sub()ˇ;
10977 }
10978 "};
10979
10980 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10981 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10982
10983 let fs = FakeFs::new(cx.executor());
10984 fs.insert_tree(
10985 path!("/a"),
10986 json!({
10987 "main.rs": buffer_text,
10988 }),
10989 )
10990 .await;
10991
10992 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10993 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10994 language_registry.add(rust_lang());
10995 let mut fake_servers = language_registry.register_fake_lsp(
10996 "Rust",
10997 FakeLspAdapter {
10998 capabilities: lsp::ServerCapabilities {
10999 completion_provider: Some(lsp::CompletionOptions {
11000 resolve_provider: None,
11001 ..lsp::CompletionOptions::default()
11002 }),
11003 ..lsp::ServerCapabilities::default()
11004 },
11005 ..FakeLspAdapter::default()
11006 },
11007 );
11008 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11009 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11010 let buffer = project
11011 .update(cx, |project, cx| {
11012 project.open_local_buffer(path!("/a/main.rs"), cx)
11013 })
11014 .await
11015 .unwrap();
11016
11017 let multi_buffer = cx.new(|cx| {
11018 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11019 multi_buffer.push_excerpts(
11020 buffer.clone(),
11021 [ExcerptRange::new(0..first_excerpt_end)],
11022 cx,
11023 );
11024 multi_buffer.push_excerpts(
11025 buffer.clone(),
11026 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11027 cx,
11028 );
11029 multi_buffer
11030 });
11031
11032 let editor = workspace
11033 .update(cx, |_, window, cx| {
11034 cx.new(|cx| {
11035 Editor::new(
11036 EditorMode::Full {
11037 scale_ui_elements_with_buffer_font_size: false,
11038 show_active_line_background: false,
11039 sized_by_content: false,
11040 },
11041 multi_buffer.clone(),
11042 Some(project.clone()),
11043 window,
11044 cx,
11045 )
11046 })
11047 })
11048 .unwrap();
11049
11050 let pane = workspace
11051 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11052 .unwrap();
11053 pane.update_in(cx, |pane, window, cx| {
11054 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11055 });
11056
11057 let fake_server = fake_servers.next().await.unwrap();
11058
11059 editor.update_in(cx, |editor, window, cx| {
11060 editor.change_selections(None, window, cx, |s| {
11061 s.select_ranges([
11062 Point::new(1, 11)..Point::new(1, 11),
11063 Point::new(7, 11)..Point::new(7, 11),
11064 ])
11065 });
11066
11067 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11068 });
11069
11070 editor.update_in(cx, |editor, window, cx| {
11071 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11072 });
11073
11074 fake_server
11075 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11076 let completion_item = lsp::CompletionItem {
11077 label: "saturating_sub()".into(),
11078 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11079 lsp::InsertReplaceEdit {
11080 new_text: "saturating_sub()".to_owned(),
11081 insert: lsp::Range::new(
11082 lsp::Position::new(7, 7),
11083 lsp::Position::new(7, 11),
11084 ),
11085 replace: lsp::Range::new(
11086 lsp::Position::new(7, 7),
11087 lsp::Position::new(7, 13),
11088 ),
11089 },
11090 )),
11091 ..lsp::CompletionItem::default()
11092 };
11093
11094 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11095 })
11096 .next()
11097 .await
11098 .unwrap();
11099
11100 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11101 .await;
11102
11103 editor
11104 .update_in(cx, |editor, window, cx| {
11105 editor
11106 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11107 .unwrap()
11108 })
11109 .await
11110 .unwrap();
11111
11112 editor.update(cx, |editor, cx| {
11113 assert_text_with_selections(editor, expected_multibuffer, cx);
11114 })
11115}
11116
11117#[gpui::test]
11118async fn test_completion(cx: &mut TestAppContext) {
11119 init_test(cx, |_| {});
11120
11121 let mut cx = EditorLspTestContext::new_rust(
11122 lsp::ServerCapabilities {
11123 completion_provider: Some(lsp::CompletionOptions {
11124 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11125 resolve_provider: Some(true),
11126 ..Default::default()
11127 }),
11128 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11129 ..Default::default()
11130 },
11131 cx,
11132 )
11133 .await;
11134 let counter = Arc::new(AtomicUsize::new(0));
11135
11136 cx.set_state(indoc! {"
11137 oneˇ
11138 two
11139 three
11140 "});
11141 cx.simulate_keystroke(".");
11142 handle_completion_request(
11143 &mut cx,
11144 indoc! {"
11145 one.|<>
11146 two
11147 three
11148 "},
11149 vec!["first_completion", "second_completion"],
11150 counter.clone(),
11151 )
11152 .await;
11153 cx.condition(|editor, _| editor.context_menu_visible())
11154 .await;
11155 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11156
11157 let _handler = handle_signature_help_request(
11158 &mut cx,
11159 lsp::SignatureHelp {
11160 signatures: vec![lsp::SignatureInformation {
11161 label: "test signature".to_string(),
11162 documentation: None,
11163 parameters: Some(vec![lsp::ParameterInformation {
11164 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11165 documentation: None,
11166 }]),
11167 active_parameter: None,
11168 }],
11169 active_signature: None,
11170 active_parameter: None,
11171 },
11172 );
11173 cx.update_editor(|editor, window, cx| {
11174 assert!(
11175 !editor.signature_help_state.is_shown(),
11176 "No signature help was called for"
11177 );
11178 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11179 });
11180 cx.run_until_parked();
11181 cx.update_editor(|editor, _, _| {
11182 assert!(
11183 !editor.signature_help_state.is_shown(),
11184 "No signature help should be shown when completions menu is open"
11185 );
11186 });
11187
11188 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11189 editor.context_menu_next(&Default::default(), window, cx);
11190 editor
11191 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11192 .unwrap()
11193 });
11194 cx.assert_editor_state(indoc! {"
11195 one.second_completionˇ
11196 two
11197 three
11198 "});
11199
11200 handle_resolve_completion_request(
11201 &mut cx,
11202 Some(vec![
11203 (
11204 //This overlaps with the primary completion edit which is
11205 //misbehavior from the LSP spec, test that we filter it out
11206 indoc! {"
11207 one.second_ˇcompletion
11208 two
11209 threeˇ
11210 "},
11211 "overlapping additional edit",
11212 ),
11213 (
11214 indoc! {"
11215 one.second_completion
11216 two
11217 threeˇ
11218 "},
11219 "\nadditional edit",
11220 ),
11221 ]),
11222 )
11223 .await;
11224 apply_additional_edits.await.unwrap();
11225 cx.assert_editor_state(indoc! {"
11226 one.second_completionˇ
11227 two
11228 three
11229 additional edit
11230 "});
11231
11232 cx.set_state(indoc! {"
11233 one.second_completion
11234 twoˇ
11235 threeˇ
11236 additional edit
11237 "});
11238 cx.simulate_keystroke(" ");
11239 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11240 cx.simulate_keystroke("s");
11241 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11242
11243 cx.assert_editor_state(indoc! {"
11244 one.second_completion
11245 two sˇ
11246 three sˇ
11247 additional edit
11248 "});
11249 handle_completion_request(
11250 &mut cx,
11251 indoc! {"
11252 one.second_completion
11253 two s
11254 three <s|>
11255 additional edit
11256 "},
11257 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11258 counter.clone(),
11259 )
11260 .await;
11261 cx.condition(|editor, _| editor.context_menu_visible())
11262 .await;
11263 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11264
11265 cx.simulate_keystroke("i");
11266
11267 handle_completion_request(
11268 &mut cx,
11269 indoc! {"
11270 one.second_completion
11271 two si
11272 three <si|>
11273 additional edit
11274 "},
11275 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11276 counter.clone(),
11277 )
11278 .await;
11279 cx.condition(|editor, _| editor.context_menu_visible())
11280 .await;
11281 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11282
11283 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11284 editor
11285 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11286 .unwrap()
11287 });
11288 cx.assert_editor_state(indoc! {"
11289 one.second_completion
11290 two sixth_completionˇ
11291 three sixth_completionˇ
11292 additional edit
11293 "});
11294
11295 apply_additional_edits.await.unwrap();
11296
11297 update_test_language_settings(&mut cx, |settings| {
11298 settings.defaults.show_completions_on_input = Some(false);
11299 });
11300 cx.set_state("editorˇ");
11301 cx.simulate_keystroke(".");
11302 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11303 cx.simulate_keystrokes("c l o");
11304 cx.assert_editor_state("editor.cloˇ");
11305 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11306 cx.update_editor(|editor, window, cx| {
11307 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11308 });
11309 handle_completion_request(
11310 &mut cx,
11311 "editor.<clo|>",
11312 vec!["close", "clobber"],
11313 counter.clone(),
11314 )
11315 .await;
11316 cx.condition(|editor, _| editor.context_menu_visible())
11317 .await;
11318 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11319
11320 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11321 editor
11322 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11323 .unwrap()
11324 });
11325 cx.assert_editor_state("editor.closeˇ");
11326 handle_resolve_completion_request(&mut cx, None).await;
11327 apply_additional_edits.await.unwrap();
11328}
11329
11330#[gpui::test]
11331async fn test_word_completion(cx: &mut TestAppContext) {
11332 let lsp_fetch_timeout_ms = 10;
11333 init_test(cx, |language_settings| {
11334 language_settings.defaults.completions = Some(CompletionSettings {
11335 words: WordsCompletionMode::Fallback,
11336 lsp: true,
11337 lsp_fetch_timeout_ms: 10,
11338 lsp_insert_mode: LspInsertMode::Insert,
11339 });
11340 });
11341
11342 let mut cx = EditorLspTestContext::new_rust(
11343 lsp::ServerCapabilities {
11344 completion_provider: Some(lsp::CompletionOptions {
11345 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11346 ..lsp::CompletionOptions::default()
11347 }),
11348 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11349 ..lsp::ServerCapabilities::default()
11350 },
11351 cx,
11352 )
11353 .await;
11354
11355 let throttle_completions = Arc::new(AtomicBool::new(false));
11356
11357 let lsp_throttle_completions = throttle_completions.clone();
11358 let _completion_requests_handler =
11359 cx.lsp
11360 .server
11361 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11362 let lsp_throttle_completions = lsp_throttle_completions.clone();
11363 let cx = cx.clone();
11364 async move {
11365 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11366 cx.background_executor()
11367 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11368 .await;
11369 }
11370 Ok(Some(lsp::CompletionResponse::Array(vec![
11371 lsp::CompletionItem {
11372 label: "first".into(),
11373 ..lsp::CompletionItem::default()
11374 },
11375 lsp::CompletionItem {
11376 label: "last".into(),
11377 ..lsp::CompletionItem::default()
11378 },
11379 ])))
11380 }
11381 });
11382
11383 cx.set_state(indoc! {"
11384 oneˇ
11385 two
11386 three
11387 "});
11388 cx.simulate_keystroke(".");
11389 cx.executor().run_until_parked();
11390 cx.condition(|editor, _| editor.context_menu_visible())
11391 .await;
11392 cx.update_editor(|editor, window, cx| {
11393 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11394 {
11395 assert_eq!(
11396 completion_menu_entries(&menu),
11397 &["first", "last"],
11398 "When LSP server is fast to reply, no fallback word completions are used"
11399 );
11400 } else {
11401 panic!("expected completion menu to be open");
11402 }
11403 editor.cancel(&Cancel, window, cx);
11404 });
11405 cx.executor().run_until_parked();
11406 cx.condition(|editor, _| !editor.context_menu_visible())
11407 .await;
11408
11409 throttle_completions.store(true, atomic::Ordering::Release);
11410 cx.simulate_keystroke(".");
11411 cx.executor()
11412 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11413 cx.executor().run_until_parked();
11414 cx.condition(|editor, _| editor.context_menu_visible())
11415 .await;
11416 cx.update_editor(|editor, _, _| {
11417 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11418 {
11419 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11420 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11421 } else {
11422 panic!("expected completion menu to be open");
11423 }
11424 });
11425}
11426
11427#[gpui::test]
11428async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11429 init_test(cx, |language_settings| {
11430 language_settings.defaults.completions = Some(CompletionSettings {
11431 words: WordsCompletionMode::Enabled,
11432 lsp: true,
11433 lsp_fetch_timeout_ms: 0,
11434 lsp_insert_mode: LspInsertMode::Insert,
11435 });
11436 });
11437
11438 let mut cx = EditorLspTestContext::new_rust(
11439 lsp::ServerCapabilities {
11440 completion_provider: Some(lsp::CompletionOptions {
11441 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11442 ..lsp::CompletionOptions::default()
11443 }),
11444 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11445 ..lsp::ServerCapabilities::default()
11446 },
11447 cx,
11448 )
11449 .await;
11450
11451 let _completion_requests_handler =
11452 cx.lsp
11453 .server
11454 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11455 Ok(Some(lsp::CompletionResponse::Array(vec![
11456 lsp::CompletionItem {
11457 label: "first".into(),
11458 ..lsp::CompletionItem::default()
11459 },
11460 lsp::CompletionItem {
11461 label: "last".into(),
11462 ..lsp::CompletionItem::default()
11463 },
11464 ])))
11465 });
11466
11467 cx.set_state(indoc! {"ˇ
11468 first
11469 last
11470 second
11471 "});
11472 cx.simulate_keystroke(".");
11473 cx.executor().run_until_parked();
11474 cx.condition(|editor, _| editor.context_menu_visible())
11475 .await;
11476 cx.update_editor(|editor, _, _| {
11477 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11478 {
11479 assert_eq!(
11480 completion_menu_entries(&menu),
11481 &["first", "last", "second"],
11482 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11483 );
11484 } else {
11485 panic!("expected completion menu to be open");
11486 }
11487 });
11488}
11489
11490#[gpui::test]
11491async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11492 init_test(cx, |language_settings| {
11493 language_settings.defaults.completions = Some(CompletionSettings {
11494 words: WordsCompletionMode::Disabled,
11495 lsp: true,
11496 lsp_fetch_timeout_ms: 0,
11497 lsp_insert_mode: LspInsertMode::Insert,
11498 });
11499 });
11500
11501 let mut cx = EditorLspTestContext::new_rust(
11502 lsp::ServerCapabilities {
11503 completion_provider: Some(lsp::CompletionOptions {
11504 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11505 ..lsp::CompletionOptions::default()
11506 }),
11507 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11508 ..lsp::ServerCapabilities::default()
11509 },
11510 cx,
11511 )
11512 .await;
11513
11514 let _completion_requests_handler =
11515 cx.lsp
11516 .server
11517 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11518 panic!("LSP completions should not be queried when dealing with word completions")
11519 });
11520
11521 cx.set_state(indoc! {"ˇ
11522 first
11523 last
11524 second
11525 "});
11526 cx.update_editor(|editor, window, cx| {
11527 editor.show_word_completions(&ShowWordCompletions, window, cx);
11528 });
11529 cx.executor().run_until_parked();
11530 cx.condition(|editor, _| editor.context_menu_visible())
11531 .await;
11532 cx.update_editor(|editor, _, _| {
11533 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11534 {
11535 assert_eq!(
11536 completion_menu_entries(&menu),
11537 &["first", "last", "second"],
11538 "`ShowWordCompletions` action should show word completions"
11539 );
11540 } else {
11541 panic!("expected completion menu to be open");
11542 }
11543 });
11544
11545 cx.simulate_keystroke("l");
11546 cx.executor().run_until_parked();
11547 cx.condition(|editor, _| editor.context_menu_visible())
11548 .await;
11549 cx.update_editor(|editor, _, _| {
11550 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11551 {
11552 assert_eq!(
11553 completion_menu_entries(&menu),
11554 &["last"],
11555 "After showing word completions, further editing should filter them and not query the LSP"
11556 );
11557 } else {
11558 panic!("expected completion menu to be open");
11559 }
11560 });
11561}
11562
11563#[gpui::test]
11564async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11565 init_test(cx, |language_settings| {
11566 language_settings.defaults.completions = Some(CompletionSettings {
11567 words: WordsCompletionMode::Fallback,
11568 lsp: false,
11569 lsp_fetch_timeout_ms: 0,
11570 lsp_insert_mode: LspInsertMode::Insert,
11571 });
11572 });
11573
11574 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11575
11576 cx.set_state(indoc! {"ˇ
11577 0_usize
11578 let
11579 33
11580 4.5f32
11581 "});
11582 cx.update_editor(|editor, window, cx| {
11583 editor.show_completions(&ShowCompletions::default(), window, cx);
11584 });
11585 cx.executor().run_until_parked();
11586 cx.condition(|editor, _| editor.context_menu_visible())
11587 .await;
11588 cx.update_editor(|editor, window, cx| {
11589 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11590 {
11591 assert_eq!(
11592 completion_menu_entries(&menu),
11593 &["let"],
11594 "With no digits in the completion query, no digits should be in the word completions"
11595 );
11596 } else {
11597 panic!("expected completion menu to be open");
11598 }
11599 editor.cancel(&Cancel, window, cx);
11600 });
11601
11602 cx.set_state(indoc! {"3ˇ
11603 0_usize
11604 let
11605 3
11606 33.35f32
11607 "});
11608 cx.update_editor(|editor, window, cx| {
11609 editor.show_completions(&ShowCompletions::default(), window, cx);
11610 });
11611 cx.executor().run_until_parked();
11612 cx.condition(|editor, _| editor.context_menu_visible())
11613 .await;
11614 cx.update_editor(|editor, _, _| {
11615 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11616 {
11617 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11618 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11619 } else {
11620 panic!("expected completion menu to be open");
11621 }
11622 });
11623}
11624
11625fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11626 let position = || lsp::Position {
11627 line: params.text_document_position.position.line,
11628 character: params.text_document_position.position.character,
11629 };
11630 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11631 range: lsp::Range {
11632 start: position(),
11633 end: position(),
11634 },
11635 new_text: text.to_string(),
11636 }))
11637}
11638
11639#[gpui::test]
11640async fn test_multiline_completion(cx: &mut TestAppContext) {
11641 init_test(cx, |_| {});
11642
11643 let fs = FakeFs::new(cx.executor());
11644 fs.insert_tree(
11645 path!("/a"),
11646 json!({
11647 "main.ts": "a",
11648 }),
11649 )
11650 .await;
11651
11652 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11653 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11654 let typescript_language = Arc::new(Language::new(
11655 LanguageConfig {
11656 name: "TypeScript".into(),
11657 matcher: LanguageMatcher {
11658 path_suffixes: vec!["ts".to_string()],
11659 ..LanguageMatcher::default()
11660 },
11661 line_comments: vec!["// ".into()],
11662 ..LanguageConfig::default()
11663 },
11664 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11665 ));
11666 language_registry.add(typescript_language.clone());
11667 let mut fake_servers = language_registry.register_fake_lsp(
11668 "TypeScript",
11669 FakeLspAdapter {
11670 capabilities: lsp::ServerCapabilities {
11671 completion_provider: Some(lsp::CompletionOptions {
11672 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11673 ..lsp::CompletionOptions::default()
11674 }),
11675 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11676 ..lsp::ServerCapabilities::default()
11677 },
11678 // Emulate vtsls label generation
11679 label_for_completion: Some(Box::new(|item, _| {
11680 let text = if let Some(description) = item
11681 .label_details
11682 .as_ref()
11683 .and_then(|label_details| label_details.description.as_ref())
11684 {
11685 format!("{} {}", item.label, description)
11686 } else if let Some(detail) = &item.detail {
11687 format!("{} {}", item.label, detail)
11688 } else {
11689 item.label.clone()
11690 };
11691 let len = text.len();
11692 Some(language::CodeLabel {
11693 text,
11694 runs: Vec::new(),
11695 filter_range: 0..len,
11696 })
11697 })),
11698 ..FakeLspAdapter::default()
11699 },
11700 );
11701 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11702 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11703 let worktree_id = workspace
11704 .update(cx, |workspace, _window, cx| {
11705 workspace.project().update(cx, |project, cx| {
11706 project.worktrees(cx).next().unwrap().read(cx).id()
11707 })
11708 })
11709 .unwrap();
11710 let _buffer = project
11711 .update(cx, |project, cx| {
11712 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11713 })
11714 .await
11715 .unwrap();
11716 let editor = workspace
11717 .update(cx, |workspace, window, cx| {
11718 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11719 })
11720 .unwrap()
11721 .await
11722 .unwrap()
11723 .downcast::<Editor>()
11724 .unwrap();
11725 let fake_server = fake_servers.next().await.unwrap();
11726
11727 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11728 let multiline_label_2 = "a\nb\nc\n";
11729 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11730 let multiline_description = "d\ne\nf\n";
11731 let multiline_detail_2 = "g\nh\ni\n";
11732
11733 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11734 move |params, _| async move {
11735 Ok(Some(lsp::CompletionResponse::Array(vec![
11736 lsp::CompletionItem {
11737 label: multiline_label.to_string(),
11738 text_edit: gen_text_edit(¶ms, "new_text_1"),
11739 ..lsp::CompletionItem::default()
11740 },
11741 lsp::CompletionItem {
11742 label: "single line label 1".to_string(),
11743 detail: Some(multiline_detail.to_string()),
11744 text_edit: gen_text_edit(¶ms, "new_text_2"),
11745 ..lsp::CompletionItem::default()
11746 },
11747 lsp::CompletionItem {
11748 label: "single line label 2".to_string(),
11749 label_details: Some(lsp::CompletionItemLabelDetails {
11750 description: Some(multiline_description.to_string()),
11751 detail: None,
11752 }),
11753 text_edit: gen_text_edit(¶ms, "new_text_2"),
11754 ..lsp::CompletionItem::default()
11755 },
11756 lsp::CompletionItem {
11757 label: multiline_label_2.to_string(),
11758 detail: Some(multiline_detail_2.to_string()),
11759 text_edit: gen_text_edit(¶ms, "new_text_3"),
11760 ..lsp::CompletionItem::default()
11761 },
11762 lsp::CompletionItem {
11763 label: "Label with many spaces and \t but without newlines".to_string(),
11764 detail: Some(
11765 "Details with many spaces and \t but without newlines".to_string(),
11766 ),
11767 text_edit: gen_text_edit(¶ms, "new_text_4"),
11768 ..lsp::CompletionItem::default()
11769 },
11770 ])))
11771 },
11772 );
11773
11774 editor.update_in(cx, |editor, window, cx| {
11775 cx.focus_self(window);
11776 editor.move_to_end(&MoveToEnd, window, cx);
11777 editor.handle_input(".", window, cx);
11778 });
11779 cx.run_until_parked();
11780 completion_handle.next().await.unwrap();
11781
11782 editor.update(cx, |editor, _| {
11783 assert!(editor.context_menu_visible());
11784 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11785 {
11786 let completion_labels = menu
11787 .completions
11788 .borrow()
11789 .iter()
11790 .map(|c| c.label.text.clone())
11791 .collect::<Vec<_>>();
11792 assert_eq!(
11793 completion_labels,
11794 &[
11795 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11796 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11797 "single line label 2 d e f ",
11798 "a b c g h i ",
11799 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11800 ],
11801 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11802 );
11803
11804 for completion in menu
11805 .completions
11806 .borrow()
11807 .iter() {
11808 assert_eq!(
11809 completion.label.filter_range,
11810 0..completion.label.text.len(),
11811 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11812 );
11813 }
11814 } else {
11815 panic!("expected completion menu to be open");
11816 }
11817 });
11818}
11819
11820#[gpui::test]
11821async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11822 init_test(cx, |_| {});
11823 let mut cx = EditorLspTestContext::new_rust(
11824 lsp::ServerCapabilities {
11825 completion_provider: Some(lsp::CompletionOptions {
11826 trigger_characters: Some(vec![".".to_string()]),
11827 ..Default::default()
11828 }),
11829 ..Default::default()
11830 },
11831 cx,
11832 )
11833 .await;
11834 cx.lsp
11835 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11836 Ok(Some(lsp::CompletionResponse::Array(vec![
11837 lsp::CompletionItem {
11838 label: "first".into(),
11839 ..Default::default()
11840 },
11841 lsp::CompletionItem {
11842 label: "last".into(),
11843 ..Default::default()
11844 },
11845 ])))
11846 });
11847 cx.set_state("variableˇ");
11848 cx.simulate_keystroke(".");
11849 cx.executor().run_until_parked();
11850
11851 cx.update_editor(|editor, _, _| {
11852 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11853 {
11854 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11855 } else {
11856 panic!("expected completion menu to be open");
11857 }
11858 });
11859
11860 cx.update_editor(|editor, window, cx| {
11861 editor.move_page_down(&MovePageDown::default(), window, cx);
11862 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11863 {
11864 assert!(
11865 menu.selected_item == 1,
11866 "expected PageDown to select the last item from the context menu"
11867 );
11868 } else {
11869 panic!("expected completion menu to stay open after PageDown");
11870 }
11871 });
11872
11873 cx.update_editor(|editor, window, cx| {
11874 editor.move_page_up(&MovePageUp::default(), window, cx);
11875 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11876 {
11877 assert!(
11878 menu.selected_item == 0,
11879 "expected PageUp to select the first item from the context menu"
11880 );
11881 } else {
11882 panic!("expected completion menu to stay open after PageUp");
11883 }
11884 });
11885}
11886
11887#[gpui::test]
11888async fn test_as_is_completions(cx: &mut TestAppContext) {
11889 init_test(cx, |_| {});
11890 let mut cx = EditorLspTestContext::new_rust(
11891 lsp::ServerCapabilities {
11892 completion_provider: Some(lsp::CompletionOptions {
11893 ..Default::default()
11894 }),
11895 ..Default::default()
11896 },
11897 cx,
11898 )
11899 .await;
11900 cx.lsp
11901 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11902 Ok(Some(lsp::CompletionResponse::Array(vec![
11903 lsp::CompletionItem {
11904 label: "unsafe".into(),
11905 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11906 range: lsp::Range {
11907 start: lsp::Position {
11908 line: 1,
11909 character: 2,
11910 },
11911 end: lsp::Position {
11912 line: 1,
11913 character: 3,
11914 },
11915 },
11916 new_text: "unsafe".to_string(),
11917 })),
11918 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11919 ..Default::default()
11920 },
11921 ])))
11922 });
11923 cx.set_state("fn a() {}\n nˇ");
11924 cx.executor().run_until_parked();
11925 cx.update_editor(|editor, window, cx| {
11926 editor.show_completions(
11927 &ShowCompletions {
11928 trigger: Some("\n".into()),
11929 },
11930 window,
11931 cx,
11932 );
11933 });
11934 cx.executor().run_until_parked();
11935
11936 cx.update_editor(|editor, window, cx| {
11937 editor.confirm_completion(&Default::default(), window, cx)
11938 });
11939 cx.executor().run_until_parked();
11940 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11941}
11942
11943#[gpui::test]
11944async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11945 init_test(cx, |_| {});
11946
11947 let mut cx = EditorLspTestContext::new_rust(
11948 lsp::ServerCapabilities {
11949 completion_provider: Some(lsp::CompletionOptions {
11950 trigger_characters: Some(vec![".".to_string()]),
11951 resolve_provider: Some(true),
11952 ..Default::default()
11953 }),
11954 ..Default::default()
11955 },
11956 cx,
11957 )
11958 .await;
11959
11960 cx.set_state("fn main() { let a = 2ˇ; }");
11961 cx.simulate_keystroke(".");
11962 let completion_item = lsp::CompletionItem {
11963 label: "Some".into(),
11964 kind: Some(lsp::CompletionItemKind::SNIPPET),
11965 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11966 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11967 kind: lsp::MarkupKind::Markdown,
11968 value: "```rust\nSome(2)\n```".to_string(),
11969 })),
11970 deprecated: Some(false),
11971 sort_text: Some("Some".to_string()),
11972 filter_text: Some("Some".to_string()),
11973 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11974 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11975 range: lsp::Range {
11976 start: lsp::Position {
11977 line: 0,
11978 character: 22,
11979 },
11980 end: lsp::Position {
11981 line: 0,
11982 character: 22,
11983 },
11984 },
11985 new_text: "Some(2)".to_string(),
11986 })),
11987 additional_text_edits: Some(vec![lsp::TextEdit {
11988 range: lsp::Range {
11989 start: lsp::Position {
11990 line: 0,
11991 character: 20,
11992 },
11993 end: lsp::Position {
11994 line: 0,
11995 character: 22,
11996 },
11997 },
11998 new_text: "".to_string(),
11999 }]),
12000 ..Default::default()
12001 };
12002
12003 let closure_completion_item = completion_item.clone();
12004 let counter = Arc::new(AtomicUsize::new(0));
12005 let counter_clone = counter.clone();
12006 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12007 let task_completion_item = closure_completion_item.clone();
12008 counter_clone.fetch_add(1, atomic::Ordering::Release);
12009 async move {
12010 Ok(Some(lsp::CompletionResponse::Array(vec![
12011 task_completion_item,
12012 ])))
12013 }
12014 });
12015
12016 cx.condition(|editor, _| editor.context_menu_visible())
12017 .await;
12018 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12019 assert!(request.next().await.is_some());
12020 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12021
12022 cx.simulate_keystrokes("S o m");
12023 cx.condition(|editor, _| editor.context_menu_visible())
12024 .await;
12025 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12026 assert!(request.next().await.is_some());
12027 assert!(request.next().await.is_some());
12028 assert!(request.next().await.is_some());
12029 request.close();
12030 assert!(request.next().await.is_none());
12031 assert_eq!(
12032 counter.load(atomic::Ordering::Acquire),
12033 4,
12034 "With the completions menu open, only one LSP request should happen per input"
12035 );
12036}
12037
12038#[gpui::test]
12039async fn test_toggle_comment(cx: &mut TestAppContext) {
12040 init_test(cx, |_| {});
12041 let mut cx = EditorTestContext::new(cx).await;
12042 let language = Arc::new(Language::new(
12043 LanguageConfig {
12044 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12045 ..Default::default()
12046 },
12047 Some(tree_sitter_rust::LANGUAGE.into()),
12048 ));
12049 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12050
12051 // If multiple selections intersect a line, the line is only toggled once.
12052 cx.set_state(indoc! {"
12053 fn a() {
12054 «//b();
12055 ˇ»// «c();
12056 //ˇ» d();
12057 }
12058 "});
12059
12060 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12061
12062 cx.assert_editor_state(indoc! {"
12063 fn a() {
12064 «b();
12065 c();
12066 ˇ» d();
12067 }
12068 "});
12069
12070 // The comment prefix is inserted at the same column for every line in a
12071 // selection.
12072 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12073
12074 cx.assert_editor_state(indoc! {"
12075 fn a() {
12076 // «b();
12077 // c();
12078 ˇ»// d();
12079 }
12080 "});
12081
12082 // If a selection ends at the beginning of a line, that line is not toggled.
12083 cx.set_selections_state(indoc! {"
12084 fn a() {
12085 // b();
12086 «// c();
12087 ˇ» // d();
12088 }
12089 "});
12090
12091 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12092
12093 cx.assert_editor_state(indoc! {"
12094 fn a() {
12095 // b();
12096 «c();
12097 ˇ» // d();
12098 }
12099 "});
12100
12101 // If a selection span a single line and is empty, the line is toggled.
12102 cx.set_state(indoc! {"
12103 fn a() {
12104 a();
12105 b();
12106 ˇ
12107 }
12108 "});
12109
12110 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12111
12112 cx.assert_editor_state(indoc! {"
12113 fn a() {
12114 a();
12115 b();
12116 //•ˇ
12117 }
12118 "});
12119
12120 // If a selection span multiple lines, empty lines are not toggled.
12121 cx.set_state(indoc! {"
12122 fn a() {
12123 «a();
12124
12125 c();ˇ»
12126 }
12127 "});
12128
12129 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12130
12131 cx.assert_editor_state(indoc! {"
12132 fn a() {
12133 // «a();
12134
12135 // c();ˇ»
12136 }
12137 "});
12138
12139 // If a selection includes multiple comment prefixes, all lines are uncommented.
12140 cx.set_state(indoc! {"
12141 fn a() {
12142 «// a();
12143 /// b();
12144 //! c();ˇ»
12145 }
12146 "});
12147
12148 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12149
12150 cx.assert_editor_state(indoc! {"
12151 fn a() {
12152 «a();
12153 b();
12154 c();ˇ»
12155 }
12156 "});
12157}
12158
12159#[gpui::test]
12160async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12161 init_test(cx, |_| {});
12162 let mut cx = EditorTestContext::new(cx).await;
12163 let language = Arc::new(Language::new(
12164 LanguageConfig {
12165 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12166 ..Default::default()
12167 },
12168 Some(tree_sitter_rust::LANGUAGE.into()),
12169 ));
12170 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12171
12172 let toggle_comments = &ToggleComments {
12173 advance_downwards: false,
12174 ignore_indent: true,
12175 };
12176
12177 // If multiple selections intersect a line, the line is only toggled once.
12178 cx.set_state(indoc! {"
12179 fn a() {
12180 // «b();
12181 // c();
12182 // ˇ» d();
12183 }
12184 "});
12185
12186 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12187
12188 cx.assert_editor_state(indoc! {"
12189 fn a() {
12190 «b();
12191 c();
12192 ˇ» d();
12193 }
12194 "});
12195
12196 // The comment prefix is inserted at the beginning of each line
12197 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12198
12199 cx.assert_editor_state(indoc! {"
12200 fn a() {
12201 // «b();
12202 // c();
12203 // ˇ» d();
12204 }
12205 "});
12206
12207 // If a selection ends at the beginning of a line, that line is not toggled.
12208 cx.set_selections_state(indoc! {"
12209 fn a() {
12210 // b();
12211 // «c();
12212 ˇ»// d();
12213 }
12214 "});
12215
12216 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12217
12218 cx.assert_editor_state(indoc! {"
12219 fn a() {
12220 // b();
12221 «c();
12222 ˇ»// d();
12223 }
12224 "});
12225
12226 // If a selection span a single line and is empty, the line is toggled.
12227 cx.set_state(indoc! {"
12228 fn a() {
12229 a();
12230 b();
12231 ˇ
12232 }
12233 "});
12234
12235 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12236
12237 cx.assert_editor_state(indoc! {"
12238 fn a() {
12239 a();
12240 b();
12241 //ˇ
12242 }
12243 "});
12244
12245 // If a selection span multiple lines, empty lines are not toggled.
12246 cx.set_state(indoc! {"
12247 fn a() {
12248 «a();
12249
12250 c();ˇ»
12251 }
12252 "});
12253
12254 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12255
12256 cx.assert_editor_state(indoc! {"
12257 fn a() {
12258 // «a();
12259
12260 // c();ˇ»
12261 }
12262 "});
12263
12264 // If a selection includes multiple comment prefixes, all lines are uncommented.
12265 cx.set_state(indoc! {"
12266 fn a() {
12267 // «a();
12268 /// b();
12269 //! c();ˇ»
12270 }
12271 "});
12272
12273 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12274
12275 cx.assert_editor_state(indoc! {"
12276 fn a() {
12277 «a();
12278 b();
12279 c();ˇ»
12280 }
12281 "});
12282}
12283
12284#[gpui::test]
12285async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12286 init_test(cx, |_| {});
12287
12288 let language = Arc::new(Language::new(
12289 LanguageConfig {
12290 line_comments: vec!["// ".into()],
12291 ..Default::default()
12292 },
12293 Some(tree_sitter_rust::LANGUAGE.into()),
12294 ));
12295
12296 let mut cx = EditorTestContext::new(cx).await;
12297
12298 cx.language_registry().add(language.clone());
12299 cx.update_buffer(|buffer, cx| {
12300 buffer.set_language(Some(language), cx);
12301 });
12302
12303 let toggle_comments = &ToggleComments {
12304 advance_downwards: true,
12305 ignore_indent: false,
12306 };
12307
12308 // Single cursor on one line -> advance
12309 // Cursor moves horizontally 3 characters as well on non-blank line
12310 cx.set_state(indoc!(
12311 "fn a() {
12312 ˇdog();
12313 cat();
12314 }"
12315 ));
12316 cx.update_editor(|editor, window, cx| {
12317 editor.toggle_comments(toggle_comments, window, cx);
12318 });
12319 cx.assert_editor_state(indoc!(
12320 "fn a() {
12321 // dog();
12322 catˇ();
12323 }"
12324 ));
12325
12326 // Single selection on one line -> don't advance
12327 cx.set_state(indoc!(
12328 "fn a() {
12329 «dog()ˇ»;
12330 cat();
12331 }"
12332 ));
12333 cx.update_editor(|editor, window, cx| {
12334 editor.toggle_comments(toggle_comments, window, cx);
12335 });
12336 cx.assert_editor_state(indoc!(
12337 "fn a() {
12338 // «dog()ˇ»;
12339 cat();
12340 }"
12341 ));
12342
12343 // Multiple cursors on one line -> advance
12344 cx.set_state(indoc!(
12345 "fn a() {
12346 ˇdˇog();
12347 cat();
12348 }"
12349 ));
12350 cx.update_editor(|editor, window, cx| {
12351 editor.toggle_comments(toggle_comments, window, cx);
12352 });
12353 cx.assert_editor_state(indoc!(
12354 "fn a() {
12355 // dog();
12356 catˇ(ˇ);
12357 }"
12358 ));
12359
12360 // Multiple cursors on one line, with selection -> don't advance
12361 cx.set_state(indoc!(
12362 "fn a() {
12363 ˇdˇog«()ˇ»;
12364 cat();
12365 }"
12366 ));
12367 cx.update_editor(|editor, window, cx| {
12368 editor.toggle_comments(toggle_comments, window, cx);
12369 });
12370 cx.assert_editor_state(indoc!(
12371 "fn a() {
12372 // ˇdˇog«()ˇ»;
12373 cat();
12374 }"
12375 ));
12376
12377 // Single cursor on one line -> advance
12378 // Cursor moves to column 0 on blank line
12379 cx.set_state(indoc!(
12380 "fn a() {
12381 ˇdog();
12382
12383 cat();
12384 }"
12385 ));
12386 cx.update_editor(|editor, window, cx| {
12387 editor.toggle_comments(toggle_comments, window, cx);
12388 });
12389 cx.assert_editor_state(indoc!(
12390 "fn a() {
12391 // dog();
12392 ˇ
12393 cat();
12394 }"
12395 ));
12396
12397 // Single cursor on one line -> advance
12398 // Cursor starts and ends at column 0
12399 cx.set_state(indoc!(
12400 "fn a() {
12401 ˇ dog();
12402 cat();
12403 }"
12404 ));
12405 cx.update_editor(|editor, window, cx| {
12406 editor.toggle_comments(toggle_comments, window, cx);
12407 });
12408 cx.assert_editor_state(indoc!(
12409 "fn a() {
12410 // dog();
12411 ˇ cat();
12412 }"
12413 ));
12414}
12415
12416#[gpui::test]
12417async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12418 init_test(cx, |_| {});
12419
12420 let mut cx = EditorTestContext::new(cx).await;
12421
12422 let html_language = Arc::new(
12423 Language::new(
12424 LanguageConfig {
12425 name: "HTML".into(),
12426 block_comment: Some(("<!-- ".into(), " -->".into())),
12427 ..Default::default()
12428 },
12429 Some(tree_sitter_html::LANGUAGE.into()),
12430 )
12431 .with_injection_query(
12432 r#"
12433 (script_element
12434 (raw_text) @injection.content
12435 (#set! injection.language "javascript"))
12436 "#,
12437 )
12438 .unwrap(),
12439 );
12440
12441 let javascript_language = Arc::new(Language::new(
12442 LanguageConfig {
12443 name: "JavaScript".into(),
12444 line_comments: vec!["// ".into()],
12445 ..Default::default()
12446 },
12447 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12448 ));
12449
12450 cx.language_registry().add(html_language.clone());
12451 cx.language_registry().add(javascript_language.clone());
12452 cx.update_buffer(|buffer, cx| {
12453 buffer.set_language(Some(html_language), cx);
12454 });
12455
12456 // Toggle comments for empty selections
12457 cx.set_state(
12458 &r#"
12459 <p>A</p>ˇ
12460 <p>B</p>ˇ
12461 <p>C</p>ˇ
12462 "#
12463 .unindent(),
12464 );
12465 cx.update_editor(|editor, window, cx| {
12466 editor.toggle_comments(&ToggleComments::default(), window, cx)
12467 });
12468 cx.assert_editor_state(
12469 &r#"
12470 <!-- <p>A</p>ˇ -->
12471 <!-- <p>B</p>ˇ -->
12472 <!-- <p>C</p>ˇ -->
12473 "#
12474 .unindent(),
12475 );
12476 cx.update_editor(|editor, window, cx| {
12477 editor.toggle_comments(&ToggleComments::default(), window, cx)
12478 });
12479 cx.assert_editor_state(
12480 &r#"
12481 <p>A</p>ˇ
12482 <p>B</p>ˇ
12483 <p>C</p>ˇ
12484 "#
12485 .unindent(),
12486 );
12487
12488 // Toggle comments for mixture of empty and non-empty selections, where
12489 // multiple selections occupy a given line.
12490 cx.set_state(
12491 &r#"
12492 <p>A«</p>
12493 <p>ˇ»B</p>ˇ
12494 <p>C«</p>
12495 <p>ˇ»D</p>ˇ
12496 "#
12497 .unindent(),
12498 );
12499
12500 cx.update_editor(|editor, window, cx| {
12501 editor.toggle_comments(&ToggleComments::default(), window, cx)
12502 });
12503 cx.assert_editor_state(
12504 &r#"
12505 <!-- <p>A«</p>
12506 <p>ˇ»B</p>ˇ -->
12507 <!-- <p>C«</p>
12508 <p>ˇ»D</p>ˇ -->
12509 "#
12510 .unindent(),
12511 );
12512 cx.update_editor(|editor, window, cx| {
12513 editor.toggle_comments(&ToggleComments::default(), window, cx)
12514 });
12515 cx.assert_editor_state(
12516 &r#"
12517 <p>A«</p>
12518 <p>ˇ»B</p>ˇ
12519 <p>C«</p>
12520 <p>ˇ»D</p>ˇ
12521 "#
12522 .unindent(),
12523 );
12524
12525 // Toggle comments when different languages are active for different
12526 // selections.
12527 cx.set_state(
12528 &r#"
12529 ˇ<script>
12530 ˇvar x = new Y();
12531 ˇ</script>
12532 "#
12533 .unindent(),
12534 );
12535 cx.executor().run_until_parked();
12536 cx.update_editor(|editor, window, cx| {
12537 editor.toggle_comments(&ToggleComments::default(), window, cx)
12538 });
12539 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12540 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12541 cx.assert_editor_state(
12542 &r#"
12543 <!-- ˇ<script> -->
12544 // ˇvar x = new Y();
12545 <!-- ˇ</script> -->
12546 "#
12547 .unindent(),
12548 );
12549}
12550
12551#[gpui::test]
12552fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12553 init_test(cx, |_| {});
12554
12555 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12556 let multibuffer = cx.new(|cx| {
12557 let mut multibuffer = MultiBuffer::new(ReadWrite);
12558 multibuffer.push_excerpts(
12559 buffer.clone(),
12560 [
12561 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12562 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12563 ],
12564 cx,
12565 );
12566 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12567 multibuffer
12568 });
12569
12570 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12571 editor.update_in(cx, |editor, window, cx| {
12572 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12573 editor.change_selections(None, window, cx, |s| {
12574 s.select_ranges([
12575 Point::new(0, 0)..Point::new(0, 0),
12576 Point::new(1, 0)..Point::new(1, 0),
12577 ])
12578 });
12579
12580 editor.handle_input("X", window, cx);
12581 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12582 assert_eq!(
12583 editor.selections.ranges(cx),
12584 [
12585 Point::new(0, 1)..Point::new(0, 1),
12586 Point::new(1, 1)..Point::new(1, 1),
12587 ]
12588 );
12589
12590 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12591 editor.change_selections(None, window, cx, |s| {
12592 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12593 });
12594 editor.backspace(&Default::default(), window, cx);
12595 assert_eq!(editor.text(cx), "Xa\nbbb");
12596 assert_eq!(
12597 editor.selections.ranges(cx),
12598 [Point::new(1, 0)..Point::new(1, 0)]
12599 );
12600
12601 editor.change_selections(None, window, cx, |s| {
12602 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12603 });
12604 editor.backspace(&Default::default(), window, cx);
12605 assert_eq!(editor.text(cx), "X\nbb");
12606 assert_eq!(
12607 editor.selections.ranges(cx),
12608 [Point::new(0, 1)..Point::new(0, 1)]
12609 );
12610 });
12611}
12612
12613#[gpui::test]
12614fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12615 init_test(cx, |_| {});
12616
12617 let markers = vec![('[', ']').into(), ('(', ')').into()];
12618 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12619 indoc! {"
12620 [aaaa
12621 (bbbb]
12622 cccc)",
12623 },
12624 markers.clone(),
12625 );
12626 let excerpt_ranges = markers.into_iter().map(|marker| {
12627 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12628 ExcerptRange::new(context.clone())
12629 });
12630 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12631 let multibuffer = cx.new(|cx| {
12632 let mut multibuffer = MultiBuffer::new(ReadWrite);
12633 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12634 multibuffer
12635 });
12636
12637 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12638 editor.update_in(cx, |editor, window, cx| {
12639 let (expected_text, selection_ranges) = marked_text_ranges(
12640 indoc! {"
12641 aaaa
12642 bˇbbb
12643 bˇbbˇb
12644 cccc"
12645 },
12646 true,
12647 );
12648 assert_eq!(editor.text(cx), expected_text);
12649 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12650
12651 editor.handle_input("X", window, cx);
12652
12653 let (expected_text, expected_selections) = marked_text_ranges(
12654 indoc! {"
12655 aaaa
12656 bXˇbbXb
12657 bXˇbbXˇb
12658 cccc"
12659 },
12660 false,
12661 );
12662 assert_eq!(editor.text(cx), expected_text);
12663 assert_eq!(editor.selections.ranges(cx), expected_selections);
12664
12665 editor.newline(&Newline, window, cx);
12666 let (expected_text, expected_selections) = marked_text_ranges(
12667 indoc! {"
12668 aaaa
12669 bX
12670 ˇbbX
12671 b
12672 bX
12673 ˇbbX
12674 ˇb
12675 cccc"
12676 },
12677 false,
12678 );
12679 assert_eq!(editor.text(cx), expected_text);
12680 assert_eq!(editor.selections.ranges(cx), expected_selections);
12681 });
12682}
12683
12684#[gpui::test]
12685fn test_refresh_selections(cx: &mut TestAppContext) {
12686 init_test(cx, |_| {});
12687
12688 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12689 let mut excerpt1_id = None;
12690 let multibuffer = cx.new(|cx| {
12691 let mut multibuffer = MultiBuffer::new(ReadWrite);
12692 excerpt1_id = multibuffer
12693 .push_excerpts(
12694 buffer.clone(),
12695 [
12696 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12697 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12698 ],
12699 cx,
12700 )
12701 .into_iter()
12702 .next();
12703 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12704 multibuffer
12705 });
12706
12707 let editor = cx.add_window(|window, cx| {
12708 let mut editor = build_editor(multibuffer.clone(), window, cx);
12709 let snapshot = editor.snapshot(window, cx);
12710 editor.change_selections(None, window, cx, |s| {
12711 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12712 });
12713 editor.begin_selection(
12714 Point::new(2, 1).to_display_point(&snapshot),
12715 true,
12716 1,
12717 window,
12718 cx,
12719 );
12720 assert_eq!(
12721 editor.selections.ranges(cx),
12722 [
12723 Point::new(1, 3)..Point::new(1, 3),
12724 Point::new(2, 1)..Point::new(2, 1),
12725 ]
12726 );
12727 editor
12728 });
12729
12730 // Refreshing selections is a no-op when excerpts haven't changed.
12731 _ = editor.update(cx, |editor, window, cx| {
12732 editor.change_selections(None, window, cx, |s| s.refresh());
12733 assert_eq!(
12734 editor.selections.ranges(cx),
12735 [
12736 Point::new(1, 3)..Point::new(1, 3),
12737 Point::new(2, 1)..Point::new(2, 1),
12738 ]
12739 );
12740 });
12741
12742 multibuffer.update(cx, |multibuffer, cx| {
12743 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12744 });
12745 _ = editor.update(cx, |editor, window, cx| {
12746 // Removing an excerpt causes the first selection to become degenerate.
12747 assert_eq!(
12748 editor.selections.ranges(cx),
12749 [
12750 Point::new(0, 0)..Point::new(0, 0),
12751 Point::new(0, 1)..Point::new(0, 1)
12752 ]
12753 );
12754
12755 // Refreshing selections will relocate the first selection to the original buffer
12756 // location.
12757 editor.change_selections(None, window, cx, |s| s.refresh());
12758 assert_eq!(
12759 editor.selections.ranges(cx),
12760 [
12761 Point::new(0, 1)..Point::new(0, 1),
12762 Point::new(0, 3)..Point::new(0, 3)
12763 ]
12764 );
12765 assert!(editor.selections.pending_anchor().is_some());
12766 });
12767}
12768
12769#[gpui::test]
12770fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12771 init_test(cx, |_| {});
12772
12773 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12774 let mut excerpt1_id = None;
12775 let multibuffer = cx.new(|cx| {
12776 let mut multibuffer = MultiBuffer::new(ReadWrite);
12777 excerpt1_id = multibuffer
12778 .push_excerpts(
12779 buffer.clone(),
12780 [
12781 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12782 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12783 ],
12784 cx,
12785 )
12786 .into_iter()
12787 .next();
12788 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12789 multibuffer
12790 });
12791
12792 let editor = cx.add_window(|window, cx| {
12793 let mut editor = build_editor(multibuffer.clone(), window, cx);
12794 let snapshot = editor.snapshot(window, cx);
12795 editor.begin_selection(
12796 Point::new(1, 3).to_display_point(&snapshot),
12797 false,
12798 1,
12799 window,
12800 cx,
12801 );
12802 assert_eq!(
12803 editor.selections.ranges(cx),
12804 [Point::new(1, 3)..Point::new(1, 3)]
12805 );
12806 editor
12807 });
12808
12809 multibuffer.update(cx, |multibuffer, cx| {
12810 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12811 });
12812 _ = editor.update(cx, |editor, window, cx| {
12813 assert_eq!(
12814 editor.selections.ranges(cx),
12815 [Point::new(0, 0)..Point::new(0, 0)]
12816 );
12817
12818 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12819 editor.change_selections(None, window, cx, |s| s.refresh());
12820 assert_eq!(
12821 editor.selections.ranges(cx),
12822 [Point::new(0, 3)..Point::new(0, 3)]
12823 );
12824 assert!(editor.selections.pending_anchor().is_some());
12825 });
12826}
12827
12828#[gpui::test]
12829async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12830 init_test(cx, |_| {});
12831
12832 let language = Arc::new(
12833 Language::new(
12834 LanguageConfig {
12835 brackets: BracketPairConfig {
12836 pairs: vec![
12837 BracketPair {
12838 start: "{".to_string(),
12839 end: "}".to_string(),
12840 close: true,
12841 surround: true,
12842 newline: true,
12843 },
12844 BracketPair {
12845 start: "/* ".to_string(),
12846 end: " */".to_string(),
12847 close: true,
12848 surround: true,
12849 newline: true,
12850 },
12851 ],
12852 ..Default::default()
12853 },
12854 ..Default::default()
12855 },
12856 Some(tree_sitter_rust::LANGUAGE.into()),
12857 )
12858 .with_indents_query("")
12859 .unwrap(),
12860 );
12861
12862 let text = concat!(
12863 "{ }\n", //
12864 " x\n", //
12865 " /* */\n", //
12866 "x\n", //
12867 "{{} }\n", //
12868 );
12869
12870 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12871 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12872 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12873 editor
12874 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12875 .await;
12876
12877 editor.update_in(cx, |editor, window, cx| {
12878 editor.change_selections(None, window, cx, |s| {
12879 s.select_display_ranges([
12880 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12881 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12882 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12883 ])
12884 });
12885 editor.newline(&Newline, window, cx);
12886
12887 assert_eq!(
12888 editor.buffer().read(cx).read(cx).text(),
12889 concat!(
12890 "{ \n", // Suppress rustfmt
12891 "\n", //
12892 "}\n", //
12893 " x\n", //
12894 " /* \n", //
12895 " \n", //
12896 " */\n", //
12897 "x\n", //
12898 "{{} \n", //
12899 "}\n", //
12900 )
12901 );
12902 });
12903}
12904
12905#[gpui::test]
12906fn test_highlighted_ranges(cx: &mut TestAppContext) {
12907 init_test(cx, |_| {});
12908
12909 let editor = cx.add_window(|window, cx| {
12910 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12911 build_editor(buffer.clone(), window, cx)
12912 });
12913
12914 _ = editor.update(cx, |editor, window, cx| {
12915 struct Type1;
12916 struct Type2;
12917
12918 let buffer = editor.buffer.read(cx).snapshot(cx);
12919
12920 let anchor_range =
12921 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12922
12923 editor.highlight_background::<Type1>(
12924 &[
12925 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12926 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12927 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12928 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12929 ],
12930 |_| Hsla::red(),
12931 cx,
12932 );
12933 editor.highlight_background::<Type2>(
12934 &[
12935 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12936 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12937 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12938 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12939 ],
12940 |_| Hsla::green(),
12941 cx,
12942 );
12943
12944 let snapshot = editor.snapshot(window, cx);
12945 let mut highlighted_ranges = editor.background_highlights_in_range(
12946 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12947 &snapshot,
12948 cx.theme().colors(),
12949 );
12950 // Enforce a consistent ordering based on color without relying on the ordering of the
12951 // highlight's `TypeId` which is non-executor.
12952 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12953 assert_eq!(
12954 highlighted_ranges,
12955 &[
12956 (
12957 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12958 Hsla::red(),
12959 ),
12960 (
12961 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12962 Hsla::red(),
12963 ),
12964 (
12965 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12966 Hsla::green(),
12967 ),
12968 (
12969 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12970 Hsla::green(),
12971 ),
12972 ]
12973 );
12974 assert_eq!(
12975 editor.background_highlights_in_range(
12976 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12977 &snapshot,
12978 cx.theme().colors(),
12979 ),
12980 &[(
12981 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12982 Hsla::red(),
12983 )]
12984 );
12985 });
12986}
12987
12988#[gpui::test]
12989async fn test_following(cx: &mut TestAppContext) {
12990 init_test(cx, |_| {});
12991
12992 let fs = FakeFs::new(cx.executor());
12993 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12994
12995 let buffer = project.update(cx, |project, cx| {
12996 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12997 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12998 });
12999 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13000 let follower = cx.update(|cx| {
13001 cx.open_window(
13002 WindowOptions {
13003 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13004 gpui::Point::new(px(0.), px(0.)),
13005 gpui::Point::new(px(10.), px(80.)),
13006 ))),
13007 ..Default::default()
13008 },
13009 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13010 )
13011 .unwrap()
13012 });
13013
13014 let is_still_following = Rc::new(RefCell::new(true));
13015 let follower_edit_event_count = Rc::new(RefCell::new(0));
13016 let pending_update = Rc::new(RefCell::new(None));
13017 let leader_entity = leader.root(cx).unwrap();
13018 let follower_entity = follower.root(cx).unwrap();
13019 _ = follower.update(cx, {
13020 let update = pending_update.clone();
13021 let is_still_following = is_still_following.clone();
13022 let follower_edit_event_count = follower_edit_event_count.clone();
13023 |_, window, cx| {
13024 cx.subscribe_in(
13025 &leader_entity,
13026 window,
13027 move |_, leader, event, window, cx| {
13028 leader.read(cx).add_event_to_update_proto(
13029 event,
13030 &mut update.borrow_mut(),
13031 window,
13032 cx,
13033 );
13034 },
13035 )
13036 .detach();
13037
13038 cx.subscribe_in(
13039 &follower_entity,
13040 window,
13041 move |_, _, event: &EditorEvent, _window, _cx| {
13042 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13043 *is_still_following.borrow_mut() = false;
13044 }
13045
13046 if let EditorEvent::BufferEdited = event {
13047 *follower_edit_event_count.borrow_mut() += 1;
13048 }
13049 },
13050 )
13051 .detach();
13052 }
13053 });
13054
13055 // Update the selections only
13056 _ = leader.update(cx, |leader, window, cx| {
13057 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13058 });
13059 follower
13060 .update(cx, |follower, window, cx| {
13061 follower.apply_update_proto(
13062 &project,
13063 pending_update.borrow_mut().take().unwrap(),
13064 window,
13065 cx,
13066 )
13067 })
13068 .unwrap()
13069 .await
13070 .unwrap();
13071 _ = follower.update(cx, |follower, _, cx| {
13072 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13073 });
13074 assert!(*is_still_following.borrow());
13075 assert_eq!(*follower_edit_event_count.borrow(), 0);
13076
13077 // Update the scroll position only
13078 _ = leader.update(cx, |leader, window, cx| {
13079 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13080 });
13081 follower
13082 .update(cx, |follower, window, cx| {
13083 follower.apply_update_proto(
13084 &project,
13085 pending_update.borrow_mut().take().unwrap(),
13086 window,
13087 cx,
13088 )
13089 })
13090 .unwrap()
13091 .await
13092 .unwrap();
13093 assert_eq!(
13094 follower
13095 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13096 .unwrap(),
13097 gpui::Point::new(1.5, 3.5)
13098 );
13099 assert!(*is_still_following.borrow());
13100 assert_eq!(*follower_edit_event_count.borrow(), 0);
13101
13102 // Update the selections and scroll position. The follower's scroll position is updated
13103 // via autoscroll, not via the leader's exact scroll position.
13104 _ = leader.update(cx, |leader, window, cx| {
13105 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13106 leader.request_autoscroll(Autoscroll::newest(), cx);
13107 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13108 });
13109 follower
13110 .update(cx, |follower, window, cx| {
13111 follower.apply_update_proto(
13112 &project,
13113 pending_update.borrow_mut().take().unwrap(),
13114 window,
13115 cx,
13116 )
13117 })
13118 .unwrap()
13119 .await
13120 .unwrap();
13121 _ = follower.update(cx, |follower, _, cx| {
13122 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13123 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13124 });
13125 assert!(*is_still_following.borrow());
13126
13127 // Creating a pending selection that precedes another selection
13128 _ = leader.update(cx, |leader, window, cx| {
13129 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13130 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13131 });
13132 follower
13133 .update(cx, |follower, window, cx| {
13134 follower.apply_update_proto(
13135 &project,
13136 pending_update.borrow_mut().take().unwrap(),
13137 window,
13138 cx,
13139 )
13140 })
13141 .unwrap()
13142 .await
13143 .unwrap();
13144 _ = follower.update(cx, |follower, _, cx| {
13145 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13146 });
13147 assert!(*is_still_following.borrow());
13148
13149 // Extend the pending selection so that it surrounds another selection
13150 _ = leader.update(cx, |leader, window, cx| {
13151 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13152 });
13153 follower
13154 .update(cx, |follower, window, cx| {
13155 follower.apply_update_proto(
13156 &project,
13157 pending_update.borrow_mut().take().unwrap(),
13158 window,
13159 cx,
13160 )
13161 })
13162 .unwrap()
13163 .await
13164 .unwrap();
13165 _ = follower.update(cx, |follower, _, cx| {
13166 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13167 });
13168
13169 // Scrolling locally breaks the follow
13170 _ = follower.update(cx, |follower, window, cx| {
13171 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13172 follower.set_scroll_anchor(
13173 ScrollAnchor {
13174 anchor: top_anchor,
13175 offset: gpui::Point::new(0.0, 0.5),
13176 },
13177 window,
13178 cx,
13179 );
13180 });
13181 assert!(!(*is_still_following.borrow()));
13182}
13183
13184#[gpui::test]
13185async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13186 init_test(cx, |_| {});
13187
13188 let fs = FakeFs::new(cx.executor());
13189 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13190 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13191 let pane = workspace
13192 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13193 .unwrap();
13194
13195 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13196
13197 let leader = pane.update_in(cx, |_, window, cx| {
13198 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13199 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13200 });
13201
13202 // Start following the editor when it has no excerpts.
13203 let mut state_message =
13204 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13205 let workspace_entity = workspace.root(cx).unwrap();
13206 let follower_1 = cx
13207 .update_window(*workspace.deref(), |_, window, cx| {
13208 Editor::from_state_proto(
13209 workspace_entity,
13210 ViewId {
13211 creator: CollaboratorId::PeerId(PeerId::default()),
13212 id: 0,
13213 },
13214 &mut state_message,
13215 window,
13216 cx,
13217 )
13218 })
13219 .unwrap()
13220 .unwrap()
13221 .await
13222 .unwrap();
13223
13224 let update_message = Rc::new(RefCell::new(None));
13225 follower_1.update_in(cx, {
13226 let update = update_message.clone();
13227 |_, window, cx| {
13228 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13229 leader.read(cx).add_event_to_update_proto(
13230 event,
13231 &mut update.borrow_mut(),
13232 window,
13233 cx,
13234 );
13235 })
13236 .detach();
13237 }
13238 });
13239
13240 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13241 (
13242 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13243 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13244 )
13245 });
13246
13247 // Insert some excerpts.
13248 leader.update(cx, |leader, cx| {
13249 leader.buffer.update(cx, |multibuffer, cx| {
13250 multibuffer.set_excerpts_for_path(
13251 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13252 buffer_1.clone(),
13253 vec![
13254 Point::row_range(0..3),
13255 Point::row_range(1..6),
13256 Point::row_range(12..15),
13257 ],
13258 0,
13259 cx,
13260 );
13261 multibuffer.set_excerpts_for_path(
13262 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13263 buffer_2.clone(),
13264 vec![Point::row_range(0..6), Point::row_range(8..12)],
13265 0,
13266 cx,
13267 );
13268 });
13269 });
13270
13271 // Apply the update of adding the excerpts.
13272 follower_1
13273 .update_in(cx, |follower, window, cx| {
13274 follower.apply_update_proto(
13275 &project,
13276 update_message.borrow().clone().unwrap(),
13277 window,
13278 cx,
13279 )
13280 })
13281 .await
13282 .unwrap();
13283 assert_eq!(
13284 follower_1.update(cx, |editor, cx| editor.text(cx)),
13285 leader.update(cx, |editor, cx| editor.text(cx))
13286 );
13287 update_message.borrow_mut().take();
13288
13289 // Start following separately after it already has excerpts.
13290 let mut state_message =
13291 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13292 let workspace_entity = workspace.root(cx).unwrap();
13293 let follower_2 = cx
13294 .update_window(*workspace.deref(), |_, window, cx| {
13295 Editor::from_state_proto(
13296 workspace_entity,
13297 ViewId {
13298 creator: CollaboratorId::PeerId(PeerId::default()),
13299 id: 0,
13300 },
13301 &mut state_message,
13302 window,
13303 cx,
13304 )
13305 })
13306 .unwrap()
13307 .unwrap()
13308 .await
13309 .unwrap();
13310 assert_eq!(
13311 follower_2.update(cx, |editor, cx| editor.text(cx)),
13312 leader.update(cx, |editor, cx| editor.text(cx))
13313 );
13314
13315 // Remove some excerpts.
13316 leader.update(cx, |leader, cx| {
13317 leader.buffer.update(cx, |multibuffer, cx| {
13318 let excerpt_ids = multibuffer.excerpt_ids();
13319 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13320 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13321 });
13322 });
13323
13324 // Apply the update of removing the excerpts.
13325 follower_1
13326 .update_in(cx, |follower, window, cx| {
13327 follower.apply_update_proto(
13328 &project,
13329 update_message.borrow().clone().unwrap(),
13330 window,
13331 cx,
13332 )
13333 })
13334 .await
13335 .unwrap();
13336 follower_2
13337 .update_in(cx, |follower, window, cx| {
13338 follower.apply_update_proto(
13339 &project,
13340 update_message.borrow().clone().unwrap(),
13341 window,
13342 cx,
13343 )
13344 })
13345 .await
13346 .unwrap();
13347 update_message.borrow_mut().take();
13348 assert_eq!(
13349 follower_1.update(cx, |editor, cx| editor.text(cx)),
13350 leader.update(cx, |editor, cx| editor.text(cx))
13351 );
13352}
13353
13354#[gpui::test]
13355async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13356 init_test(cx, |_| {});
13357
13358 let mut cx = EditorTestContext::new(cx).await;
13359 let lsp_store =
13360 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13361
13362 cx.set_state(indoc! {"
13363 ˇfn func(abc def: i32) -> u32 {
13364 }
13365 "});
13366
13367 cx.update(|_, cx| {
13368 lsp_store.update(cx, |lsp_store, cx| {
13369 lsp_store
13370 .update_diagnostics(
13371 LanguageServerId(0),
13372 lsp::PublishDiagnosticsParams {
13373 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13374 version: None,
13375 diagnostics: vec![
13376 lsp::Diagnostic {
13377 range: lsp::Range::new(
13378 lsp::Position::new(0, 11),
13379 lsp::Position::new(0, 12),
13380 ),
13381 severity: Some(lsp::DiagnosticSeverity::ERROR),
13382 ..Default::default()
13383 },
13384 lsp::Diagnostic {
13385 range: lsp::Range::new(
13386 lsp::Position::new(0, 12),
13387 lsp::Position::new(0, 15),
13388 ),
13389 severity: Some(lsp::DiagnosticSeverity::ERROR),
13390 ..Default::default()
13391 },
13392 lsp::Diagnostic {
13393 range: lsp::Range::new(
13394 lsp::Position::new(0, 25),
13395 lsp::Position::new(0, 28),
13396 ),
13397 severity: Some(lsp::DiagnosticSeverity::ERROR),
13398 ..Default::default()
13399 },
13400 ],
13401 },
13402 &[],
13403 cx,
13404 )
13405 .unwrap()
13406 });
13407 });
13408
13409 executor.run_until_parked();
13410
13411 cx.update_editor(|editor, window, cx| {
13412 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13413 });
13414
13415 cx.assert_editor_state(indoc! {"
13416 fn func(abc def: i32) -> ˇu32 {
13417 }
13418 "});
13419
13420 cx.update_editor(|editor, window, cx| {
13421 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13422 });
13423
13424 cx.assert_editor_state(indoc! {"
13425 fn func(abc ˇdef: i32) -> u32 {
13426 }
13427 "});
13428
13429 cx.update_editor(|editor, window, cx| {
13430 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13431 });
13432
13433 cx.assert_editor_state(indoc! {"
13434 fn func(abcˇ def: i32) -> u32 {
13435 }
13436 "});
13437
13438 cx.update_editor(|editor, window, cx| {
13439 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13440 });
13441
13442 cx.assert_editor_state(indoc! {"
13443 fn func(abc def: i32) -> ˇu32 {
13444 }
13445 "});
13446}
13447
13448#[gpui::test]
13449async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13450 init_test(cx, |_| {});
13451
13452 let mut cx = EditorTestContext::new(cx).await;
13453
13454 let diff_base = r#"
13455 use some::mod;
13456
13457 const A: u32 = 42;
13458
13459 fn main() {
13460 println!("hello");
13461
13462 println!("world");
13463 }
13464 "#
13465 .unindent();
13466
13467 // Edits are modified, removed, modified, added
13468 cx.set_state(
13469 &r#"
13470 use some::modified;
13471
13472 ˇ
13473 fn main() {
13474 println!("hello there");
13475
13476 println!("around the");
13477 println!("world");
13478 }
13479 "#
13480 .unindent(),
13481 );
13482
13483 cx.set_head_text(&diff_base);
13484 executor.run_until_parked();
13485
13486 cx.update_editor(|editor, window, cx| {
13487 //Wrap around the bottom of the buffer
13488 for _ in 0..3 {
13489 editor.go_to_next_hunk(&GoToHunk, window, cx);
13490 }
13491 });
13492
13493 cx.assert_editor_state(
13494 &r#"
13495 ˇuse some::modified;
13496
13497
13498 fn main() {
13499 println!("hello there");
13500
13501 println!("around the");
13502 println!("world");
13503 }
13504 "#
13505 .unindent(),
13506 );
13507
13508 cx.update_editor(|editor, window, cx| {
13509 //Wrap around the top of the buffer
13510 for _ in 0..2 {
13511 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13512 }
13513 });
13514
13515 cx.assert_editor_state(
13516 &r#"
13517 use some::modified;
13518
13519
13520 fn main() {
13521 ˇ println!("hello there");
13522
13523 println!("around the");
13524 println!("world");
13525 }
13526 "#
13527 .unindent(),
13528 );
13529
13530 cx.update_editor(|editor, window, cx| {
13531 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13532 });
13533
13534 cx.assert_editor_state(
13535 &r#"
13536 use some::modified;
13537
13538 ˇ
13539 fn main() {
13540 println!("hello there");
13541
13542 println!("around the");
13543 println!("world");
13544 }
13545 "#
13546 .unindent(),
13547 );
13548
13549 cx.update_editor(|editor, window, cx| {
13550 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13551 });
13552
13553 cx.assert_editor_state(
13554 &r#"
13555 ˇuse some::modified;
13556
13557
13558 fn main() {
13559 println!("hello there");
13560
13561 println!("around the");
13562 println!("world");
13563 }
13564 "#
13565 .unindent(),
13566 );
13567
13568 cx.update_editor(|editor, window, cx| {
13569 for _ in 0..2 {
13570 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13571 }
13572 });
13573
13574 cx.assert_editor_state(
13575 &r#"
13576 use some::modified;
13577
13578
13579 fn main() {
13580 ˇ println!("hello there");
13581
13582 println!("around the");
13583 println!("world");
13584 }
13585 "#
13586 .unindent(),
13587 );
13588
13589 cx.update_editor(|editor, window, cx| {
13590 editor.fold(&Fold, window, cx);
13591 });
13592
13593 cx.update_editor(|editor, window, cx| {
13594 editor.go_to_next_hunk(&GoToHunk, window, cx);
13595 });
13596
13597 cx.assert_editor_state(
13598 &r#"
13599 ˇuse some::modified;
13600
13601
13602 fn main() {
13603 println!("hello there");
13604
13605 println!("around the");
13606 println!("world");
13607 }
13608 "#
13609 .unindent(),
13610 );
13611}
13612
13613#[test]
13614fn test_split_words() {
13615 fn split(text: &str) -> Vec<&str> {
13616 split_words(text).collect()
13617 }
13618
13619 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13620 assert_eq!(split("hello_world"), &["hello_", "world"]);
13621 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13622 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13623 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13624 assert_eq!(split("helloworld"), &["helloworld"]);
13625
13626 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13627}
13628
13629#[gpui::test]
13630async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13631 init_test(cx, |_| {});
13632
13633 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13634 let mut assert = |before, after| {
13635 let _state_context = cx.set_state(before);
13636 cx.run_until_parked();
13637 cx.update_editor(|editor, window, cx| {
13638 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13639 });
13640 cx.run_until_parked();
13641 cx.assert_editor_state(after);
13642 };
13643
13644 // Outside bracket jumps to outside of matching bracket
13645 assert("console.logˇ(var);", "console.log(var)ˇ;");
13646 assert("console.log(var)ˇ;", "console.logˇ(var);");
13647
13648 // Inside bracket jumps to inside of matching bracket
13649 assert("console.log(ˇvar);", "console.log(varˇ);");
13650 assert("console.log(varˇ);", "console.log(ˇvar);");
13651
13652 // When outside a bracket and inside, favor jumping to the inside bracket
13653 assert(
13654 "console.log('foo', [1, 2, 3]ˇ);",
13655 "console.log(ˇ'foo', [1, 2, 3]);",
13656 );
13657 assert(
13658 "console.log(ˇ'foo', [1, 2, 3]);",
13659 "console.log('foo', [1, 2, 3]ˇ);",
13660 );
13661
13662 // Bias forward if two options are equally likely
13663 assert(
13664 "let result = curried_fun()ˇ();",
13665 "let result = curried_fun()()ˇ;",
13666 );
13667
13668 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13669 assert(
13670 indoc! {"
13671 function test() {
13672 console.log('test')ˇ
13673 }"},
13674 indoc! {"
13675 function test() {
13676 console.logˇ('test')
13677 }"},
13678 );
13679}
13680
13681#[gpui::test]
13682async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13683 init_test(cx, |_| {});
13684
13685 let fs = FakeFs::new(cx.executor());
13686 fs.insert_tree(
13687 path!("/a"),
13688 json!({
13689 "main.rs": "fn main() { let a = 5; }",
13690 "other.rs": "// Test file",
13691 }),
13692 )
13693 .await;
13694 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13695
13696 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13697 language_registry.add(Arc::new(Language::new(
13698 LanguageConfig {
13699 name: "Rust".into(),
13700 matcher: LanguageMatcher {
13701 path_suffixes: vec!["rs".to_string()],
13702 ..Default::default()
13703 },
13704 brackets: BracketPairConfig {
13705 pairs: vec![BracketPair {
13706 start: "{".to_string(),
13707 end: "}".to_string(),
13708 close: true,
13709 surround: true,
13710 newline: true,
13711 }],
13712 disabled_scopes_by_bracket_ix: Vec::new(),
13713 },
13714 ..Default::default()
13715 },
13716 Some(tree_sitter_rust::LANGUAGE.into()),
13717 )));
13718 let mut fake_servers = language_registry.register_fake_lsp(
13719 "Rust",
13720 FakeLspAdapter {
13721 capabilities: lsp::ServerCapabilities {
13722 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13723 first_trigger_character: "{".to_string(),
13724 more_trigger_character: None,
13725 }),
13726 ..Default::default()
13727 },
13728 ..Default::default()
13729 },
13730 );
13731
13732 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13733
13734 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13735
13736 let worktree_id = workspace
13737 .update(cx, |workspace, _, cx| {
13738 workspace.project().update(cx, |project, cx| {
13739 project.worktrees(cx).next().unwrap().read(cx).id()
13740 })
13741 })
13742 .unwrap();
13743
13744 let buffer = project
13745 .update(cx, |project, cx| {
13746 project.open_local_buffer(path!("/a/main.rs"), cx)
13747 })
13748 .await
13749 .unwrap();
13750 let editor_handle = workspace
13751 .update(cx, |workspace, window, cx| {
13752 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13753 })
13754 .unwrap()
13755 .await
13756 .unwrap()
13757 .downcast::<Editor>()
13758 .unwrap();
13759
13760 cx.executor().start_waiting();
13761 let fake_server = fake_servers.next().await.unwrap();
13762
13763 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13764 |params, _| async move {
13765 assert_eq!(
13766 params.text_document_position.text_document.uri,
13767 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13768 );
13769 assert_eq!(
13770 params.text_document_position.position,
13771 lsp::Position::new(0, 21),
13772 );
13773
13774 Ok(Some(vec![lsp::TextEdit {
13775 new_text: "]".to_string(),
13776 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13777 }]))
13778 },
13779 );
13780
13781 editor_handle.update_in(cx, |editor, window, cx| {
13782 window.focus(&editor.focus_handle(cx));
13783 editor.change_selections(None, window, cx, |s| {
13784 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13785 });
13786 editor.handle_input("{", window, cx);
13787 });
13788
13789 cx.executor().run_until_parked();
13790
13791 buffer.update(cx, |buffer, _| {
13792 assert_eq!(
13793 buffer.text(),
13794 "fn main() { let a = {5}; }",
13795 "No extra braces from on type formatting should appear in the buffer"
13796 )
13797 });
13798}
13799
13800#[gpui::test]
13801async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13802 init_test(cx, |_| {});
13803
13804 let fs = FakeFs::new(cx.executor());
13805 fs.insert_tree(
13806 path!("/a"),
13807 json!({
13808 "main.rs": "fn main() { let a = 5; }",
13809 "other.rs": "// Test file",
13810 }),
13811 )
13812 .await;
13813
13814 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13815
13816 let server_restarts = Arc::new(AtomicUsize::new(0));
13817 let closure_restarts = Arc::clone(&server_restarts);
13818 let language_server_name = "test language server";
13819 let language_name: LanguageName = "Rust".into();
13820
13821 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13822 language_registry.add(Arc::new(Language::new(
13823 LanguageConfig {
13824 name: language_name.clone(),
13825 matcher: LanguageMatcher {
13826 path_suffixes: vec!["rs".to_string()],
13827 ..Default::default()
13828 },
13829 ..Default::default()
13830 },
13831 Some(tree_sitter_rust::LANGUAGE.into()),
13832 )));
13833 let mut fake_servers = language_registry.register_fake_lsp(
13834 "Rust",
13835 FakeLspAdapter {
13836 name: language_server_name,
13837 initialization_options: Some(json!({
13838 "testOptionValue": true
13839 })),
13840 initializer: Some(Box::new(move |fake_server| {
13841 let task_restarts = Arc::clone(&closure_restarts);
13842 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13843 task_restarts.fetch_add(1, atomic::Ordering::Release);
13844 futures::future::ready(Ok(()))
13845 });
13846 })),
13847 ..Default::default()
13848 },
13849 );
13850
13851 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13852 let _buffer = project
13853 .update(cx, |project, cx| {
13854 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13855 })
13856 .await
13857 .unwrap();
13858 let _fake_server = fake_servers.next().await.unwrap();
13859 update_test_language_settings(cx, |language_settings| {
13860 language_settings.languages.insert(
13861 language_name.clone(),
13862 LanguageSettingsContent {
13863 tab_size: NonZeroU32::new(8),
13864 ..Default::default()
13865 },
13866 );
13867 });
13868 cx.executor().run_until_parked();
13869 assert_eq!(
13870 server_restarts.load(atomic::Ordering::Acquire),
13871 0,
13872 "Should not restart LSP server on an unrelated change"
13873 );
13874
13875 update_test_project_settings(cx, |project_settings| {
13876 project_settings.lsp.insert(
13877 "Some other server name".into(),
13878 LspSettings {
13879 binary: None,
13880 settings: None,
13881 initialization_options: Some(json!({
13882 "some other init value": false
13883 })),
13884 enable_lsp_tasks: false,
13885 },
13886 );
13887 });
13888 cx.executor().run_until_parked();
13889 assert_eq!(
13890 server_restarts.load(atomic::Ordering::Acquire),
13891 0,
13892 "Should not restart LSP server on an unrelated LSP settings change"
13893 );
13894
13895 update_test_project_settings(cx, |project_settings| {
13896 project_settings.lsp.insert(
13897 language_server_name.into(),
13898 LspSettings {
13899 binary: None,
13900 settings: None,
13901 initialization_options: Some(json!({
13902 "anotherInitValue": false
13903 })),
13904 enable_lsp_tasks: false,
13905 },
13906 );
13907 });
13908 cx.executor().run_until_parked();
13909 assert_eq!(
13910 server_restarts.load(atomic::Ordering::Acquire),
13911 1,
13912 "Should restart LSP server on a related LSP settings change"
13913 );
13914
13915 update_test_project_settings(cx, |project_settings| {
13916 project_settings.lsp.insert(
13917 language_server_name.into(),
13918 LspSettings {
13919 binary: None,
13920 settings: None,
13921 initialization_options: Some(json!({
13922 "anotherInitValue": false
13923 })),
13924 enable_lsp_tasks: false,
13925 },
13926 );
13927 });
13928 cx.executor().run_until_parked();
13929 assert_eq!(
13930 server_restarts.load(atomic::Ordering::Acquire),
13931 1,
13932 "Should not restart LSP server on a related LSP settings change that is the same"
13933 );
13934
13935 update_test_project_settings(cx, |project_settings| {
13936 project_settings.lsp.insert(
13937 language_server_name.into(),
13938 LspSettings {
13939 binary: None,
13940 settings: None,
13941 initialization_options: None,
13942 enable_lsp_tasks: false,
13943 },
13944 );
13945 });
13946 cx.executor().run_until_parked();
13947 assert_eq!(
13948 server_restarts.load(atomic::Ordering::Acquire),
13949 2,
13950 "Should restart LSP server on another related LSP settings change"
13951 );
13952}
13953
13954#[gpui::test]
13955async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13956 init_test(cx, |_| {});
13957
13958 let mut cx = EditorLspTestContext::new_rust(
13959 lsp::ServerCapabilities {
13960 completion_provider: Some(lsp::CompletionOptions {
13961 trigger_characters: Some(vec![".".to_string()]),
13962 resolve_provider: Some(true),
13963 ..Default::default()
13964 }),
13965 ..Default::default()
13966 },
13967 cx,
13968 )
13969 .await;
13970
13971 cx.set_state("fn main() { let a = 2ˇ; }");
13972 cx.simulate_keystroke(".");
13973 let completion_item = lsp::CompletionItem {
13974 label: "some".into(),
13975 kind: Some(lsp::CompletionItemKind::SNIPPET),
13976 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13977 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13978 kind: lsp::MarkupKind::Markdown,
13979 value: "```rust\nSome(2)\n```".to_string(),
13980 })),
13981 deprecated: Some(false),
13982 sort_text: Some("fffffff2".to_string()),
13983 filter_text: Some("some".to_string()),
13984 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13985 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13986 range: lsp::Range {
13987 start: lsp::Position {
13988 line: 0,
13989 character: 22,
13990 },
13991 end: lsp::Position {
13992 line: 0,
13993 character: 22,
13994 },
13995 },
13996 new_text: "Some(2)".to_string(),
13997 })),
13998 additional_text_edits: Some(vec![lsp::TextEdit {
13999 range: lsp::Range {
14000 start: lsp::Position {
14001 line: 0,
14002 character: 20,
14003 },
14004 end: lsp::Position {
14005 line: 0,
14006 character: 22,
14007 },
14008 },
14009 new_text: "".to_string(),
14010 }]),
14011 ..Default::default()
14012 };
14013
14014 let closure_completion_item = completion_item.clone();
14015 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14016 let task_completion_item = closure_completion_item.clone();
14017 async move {
14018 Ok(Some(lsp::CompletionResponse::Array(vec![
14019 task_completion_item,
14020 ])))
14021 }
14022 });
14023
14024 request.next().await;
14025
14026 cx.condition(|editor, _| editor.context_menu_visible())
14027 .await;
14028 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14029 editor
14030 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14031 .unwrap()
14032 });
14033 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14034
14035 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14036 let task_completion_item = completion_item.clone();
14037 async move { Ok(task_completion_item) }
14038 })
14039 .next()
14040 .await
14041 .unwrap();
14042 apply_additional_edits.await.unwrap();
14043 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14044}
14045
14046#[gpui::test]
14047async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14048 init_test(cx, |_| {});
14049
14050 let mut cx = EditorLspTestContext::new_rust(
14051 lsp::ServerCapabilities {
14052 completion_provider: Some(lsp::CompletionOptions {
14053 trigger_characters: Some(vec![".".to_string()]),
14054 resolve_provider: Some(true),
14055 ..Default::default()
14056 }),
14057 ..Default::default()
14058 },
14059 cx,
14060 )
14061 .await;
14062
14063 cx.set_state("fn main() { let a = 2ˇ; }");
14064 cx.simulate_keystroke(".");
14065
14066 let item1 = lsp::CompletionItem {
14067 label: "method id()".to_string(),
14068 filter_text: Some("id".to_string()),
14069 detail: None,
14070 documentation: None,
14071 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14072 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14073 new_text: ".id".to_string(),
14074 })),
14075 ..lsp::CompletionItem::default()
14076 };
14077
14078 let item2 = lsp::CompletionItem {
14079 label: "other".to_string(),
14080 filter_text: Some("other".to_string()),
14081 detail: None,
14082 documentation: None,
14083 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14084 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14085 new_text: ".other".to_string(),
14086 })),
14087 ..lsp::CompletionItem::default()
14088 };
14089
14090 let item1 = item1.clone();
14091 cx.set_request_handler::<lsp::request::Completion, _, _>({
14092 let item1 = item1.clone();
14093 move |_, _, _| {
14094 let item1 = item1.clone();
14095 let item2 = item2.clone();
14096 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14097 }
14098 })
14099 .next()
14100 .await;
14101
14102 cx.condition(|editor, _| editor.context_menu_visible())
14103 .await;
14104 cx.update_editor(|editor, _, _| {
14105 let context_menu = editor.context_menu.borrow_mut();
14106 let context_menu = context_menu
14107 .as_ref()
14108 .expect("Should have the context menu deployed");
14109 match context_menu {
14110 CodeContextMenu::Completions(completions_menu) => {
14111 let completions = completions_menu.completions.borrow_mut();
14112 assert_eq!(
14113 completions
14114 .iter()
14115 .map(|completion| &completion.label.text)
14116 .collect::<Vec<_>>(),
14117 vec!["method id()", "other"]
14118 )
14119 }
14120 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14121 }
14122 });
14123
14124 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14125 let item1 = item1.clone();
14126 move |_, item_to_resolve, _| {
14127 let item1 = item1.clone();
14128 async move {
14129 if item1 == item_to_resolve {
14130 Ok(lsp::CompletionItem {
14131 label: "method id()".to_string(),
14132 filter_text: Some("id".to_string()),
14133 detail: Some("Now resolved!".to_string()),
14134 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14135 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14136 range: lsp::Range::new(
14137 lsp::Position::new(0, 22),
14138 lsp::Position::new(0, 22),
14139 ),
14140 new_text: ".id".to_string(),
14141 })),
14142 ..lsp::CompletionItem::default()
14143 })
14144 } else {
14145 Ok(item_to_resolve)
14146 }
14147 }
14148 }
14149 })
14150 .next()
14151 .await
14152 .unwrap();
14153 cx.run_until_parked();
14154
14155 cx.update_editor(|editor, window, cx| {
14156 editor.context_menu_next(&Default::default(), window, cx);
14157 });
14158
14159 cx.update_editor(|editor, _, _| {
14160 let context_menu = editor.context_menu.borrow_mut();
14161 let context_menu = context_menu
14162 .as_ref()
14163 .expect("Should have the context menu deployed");
14164 match context_menu {
14165 CodeContextMenu::Completions(completions_menu) => {
14166 let completions = completions_menu.completions.borrow_mut();
14167 assert_eq!(
14168 completions
14169 .iter()
14170 .map(|completion| &completion.label.text)
14171 .collect::<Vec<_>>(),
14172 vec!["method id() Now resolved!", "other"],
14173 "Should update first completion label, but not second as the filter text did not match."
14174 );
14175 }
14176 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14177 }
14178 });
14179}
14180
14181#[gpui::test]
14182async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14183 init_test(cx, |_| {});
14184 let mut cx = EditorLspTestContext::new_rust(
14185 lsp::ServerCapabilities {
14186 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14187 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14188 completion_provider: Some(lsp::CompletionOptions {
14189 resolve_provider: Some(true),
14190 ..Default::default()
14191 }),
14192 ..Default::default()
14193 },
14194 cx,
14195 )
14196 .await;
14197 cx.set_state(indoc! {"
14198 struct TestStruct {
14199 field: i32
14200 }
14201
14202 fn mainˇ() {
14203 let unused_var = 42;
14204 let test_struct = TestStruct { field: 42 };
14205 }
14206 "});
14207 let symbol_range = cx.lsp_range(indoc! {"
14208 struct TestStruct {
14209 field: i32
14210 }
14211
14212 «fn main»() {
14213 let unused_var = 42;
14214 let test_struct = TestStruct { field: 42 };
14215 }
14216 "});
14217 let mut hover_requests =
14218 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14219 Ok(Some(lsp::Hover {
14220 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14221 kind: lsp::MarkupKind::Markdown,
14222 value: "Function documentation".to_string(),
14223 }),
14224 range: Some(symbol_range),
14225 }))
14226 });
14227
14228 // Case 1: Test that code action menu hide hover popover
14229 cx.dispatch_action(Hover);
14230 hover_requests.next().await;
14231 cx.condition(|editor, _| editor.hover_state.visible()).await;
14232 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14233 move |_, _, _| async move {
14234 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14235 lsp::CodeAction {
14236 title: "Remove unused variable".to_string(),
14237 kind: Some(CodeActionKind::QUICKFIX),
14238 edit: Some(lsp::WorkspaceEdit {
14239 changes: Some(
14240 [(
14241 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14242 vec![lsp::TextEdit {
14243 range: lsp::Range::new(
14244 lsp::Position::new(5, 4),
14245 lsp::Position::new(5, 27),
14246 ),
14247 new_text: "".to_string(),
14248 }],
14249 )]
14250 .into_iter()
14251 .collect(),
14252 ),
14253 ..Default::default()
14254 }),
14255 ..Default::default()
14256 },
14257 )]))
14258 },
14259 );
14260 cx.update_editor(|editor, window, cx| {
14261 editor.toggle_code_actions(
14262 &ToggleCodeActions {
14263 deployed_from: None,
14264 quick_launch: false,
14265 },
14266 window,
14267 cx,
14268 );
14269 });
14270 code_action_requests.next().await;
14271 cx.run_until_parked();
14272 cx.condition(|editor, _| editor.context_menu_visible())
14273 .await;
14274 cx.update_editor(|editor, _, _| {
14275 assert!(
14276 !editor.hover_state.visible(),
14277 "Hover popover should be hidden when code action menu is shown"
14278 );
14279 // Hide code actions
14280 editor.context_menu.take();
14281 });
14282
14283 // Case 2: Test that code completions hide hover popover
14284 cx.dispatch_action(Hover);
14285 hover_requests.next().await;
14286 cx.condition(|editor, _| editor.hover_state.visible()).await;
14287 let counter = Arc::new(AtomicUsize::new(0));
14288 let mut completion_requests =
14289 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14290 let counter = counter.clone();
14291 async move {
14292 counter.fetch_add(1, atomic::Ordering::Release);
14293 Ok(Some(lsp::CompletionResponse::Array(vec![
14294 lsp::CompletionItem {
14295 label: "main".into(),
14296 kind: Some(lsp::CompletionItemKind::FUNCTION),
14297 detail: Some("() -> ()".to_string()),
14298 ..Default::default()
14299 },
14300 lsp::CompletionItem {
14301 label: "TestStruct".into(),
14302 kind: Some(lsp::CompletionItemKind::STRUCT),
14303 detail: Some("struct TestStruct".to_string()),
14304 ..Default::default()
14305 },
14306 ])))
14307 }
14308 });
14309 cx.update_editor(|editor, window, cx| {
14310 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14311 });
14312 completion_requests.next().await;
14313 cx.condition(|editor, _| editor.context_menu_visible())
14314 .await;
14315 cx.update_editor(|editor, _, _| {
14316 assert!(
14317 !editor.hover_state.visible(),
14318 "Hover popover should be hidden when completion menu is shown"
14319 );
14320 });
14321}
14322
14323#[gpui::test]
14324async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14325 init_test(cx, |_| {});
14326
14327 let mut cx = EditorLspTestContext::new_rust(
14328 lsp::ServerCapabilities {
14329 completion_provider: Some(lsp::CompletionOptions {
14330 trigger_characters: Some(vec![".".to_string()]),
14331 resolve_provider: Some(true),
14332 ..Default::default()
14333 }),
14334 ..Default::default()
14335 },
14336 cx,
14337 )
14338 .await;
14339
14340 cx.set_state("fn main() { let a = 2ˇ; }");
14341 cx.simulate_keystroke(".");
14342
14343 let unresolved_item_1 = lsp::CompletionItem {
14344 label: "id".to_string(),
14345 filter_text: Some("id".to_string()),
14346 detail: None,
14347 documentation: None,
14348 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14349 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14350 new_text: ".id".to_string(),
14351 })),
14352 ..lsp::CompletionItem::default()
14353 };
14354 let resolved_item_1 = lsp::CompletionItem {
14355 additional_text_edits: Some(vec![lsp::TextEdit {
14356 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14357 new_text: "!!".to_string(),
14358 }]),
14359 ..unresolved_item_1.clone()
14360 };
14361 let unresolved_item_2 = lsp::CompletionItem {
14362 label: "other".to_string(),
14363 filter_text: Some("other".to_string()),
14364 detail: None,
14365 documentation: None,
14366 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14367 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14368 new_text: ".other".to_string(),
14369 })),
14370 ..lsp::CompletionItem::default()
14371 };
14372 let resolved_item_2 = lsp::CompletionItem {
14373 additional_text_edits: Some(vec![lsp::TextEdit {
14374 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14375 new_text: "??".to_string(),
14376 }]),
14377 ..unresolved_item_2.clone()
14378 };
14379
14380 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14381 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14382 cx.lsp
14383 .server
14384 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14385 let unresolved_item_1 = unresolved_item_1.clone();
14386 let resolved_item_1 = resolved_item_1.clone();
14387 let unresolved_item_2 = unresolved_item_2.clone();
14388 let resolved_item_2 = resolved_item_2.clone();
14389 let resolve_requests_1 = resolve_requests_1.clone();
14390 let resolve_requests_2 = resolve_requests_2.clone();
14391 move |unresolved_request, _| {
14392 let unresolved_item_1 = unresolved_item_1.clone();
14393 let resolved_item_1 = resolved_item_1.clone();
14394 let unresolved_item_2 = unresolved_item_2.clone();
14395 let resolved_item_2 = resolved_item_2.clone();
14396 let resolve_requests_1 = resolve_requests_1.clone();
14397 let resolve_requests_2 = resolve_requests_2.clone();
14398 async move {
14399 if unresolved_request == unresolved_item_1 {
14400 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14401 Ok(resolved_item_1.clone())
14402 } else if unresolved_request == unresolved_item_2 {
14403 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14404 Ok(resolved_item_2.clone())
14405 } else {
14406 panic!("Unexpected completion item {unresolved_request:?}")
14407 }
14408 }
14409 }
14410 })
14411 .detach();
14412
14413 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14414 let unresolved_item_1 = unresolved_item_1.clone();
14415 let unresolved_item_2 = unresolved_item_2.clone();
14416 async move {
14417 Ok(Some(lsp::CompletionResponse::Array(vec![
14418 unresolved_item_1,
14419 unresolved_item_2,
14420 ])))
14421 }
14422 })
14423 .next()
14424 .await;
14425
14426 cx.condition(|editor, _| editor.context_menu_visible())
14427 .await;
14428 cx.update_editor(|editor, _, _| {
14429 let context_menu = editor.context_menu.borrow_mut();
14430 let context_menu = context_menu
14431 .as_ref()
14432 .expect("Should have the context menu deployed");
14433 match context_menu {
14434 CodeContextMenu::Completions(completions_menu) => {
14435 let completions = completions_menu.completions.borrow_mut();
14436 assert_eq!(
14437 completions
14438 .iter()
14439 .map(|completion| &completion.label.text)
14440 .collect::<Vec<_>>(),
14441 vec!["id", "other"]
14442 )
14443 }
14444 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14445 }
14446 });
14447 cx.run_until_parked();
14448
14449 cx.update_editor(|editor, window, cx| {
14450 editor.context_menu_next(&ContextMenuNext, window, cx);
14451 });
14452 cx.run_until_parked();
14453 cx.update_editor(|editor, window, cx| {
14454 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14455 });
14456 cx.run_until_parked();
14457 cx.update_editor(|editor, window, cx| {
14458 editor.context_menu_next(&ContextMenuNext, window, cx);
14459 });
14460 cx.run_until_parked();
14461 cx.update_editor(|editor, window, cx| {
14462 editor
14463 .compose_completion(&ComposeCompletion::default(), window, cx)
14464 .expect("No task returned")
14465 })
14466 .await
14467 .expect("Completion failed");
14468 cx.run_until_parked();
14469
14470 cx.update_editor(|editor, _, cx| {
14471 assert_eq!(
14472 resolve_requests_1.load(atomic::Ordering::Acquire),
14473 1,
14474 "Should always resolve once despite multiple selections"
14475 );
14476 assert_eq!(
14477 resolve_requests_2.load(atomic::Ordering::Acquire),
14478 1,
14479 "Should always resolve once after multiple selections and applying the completion"
14480 );
14481 assert_eq!(
14482 editor.text(cx),
14483 "fn main() { let a = ??.other; }",
14484 "Should use resolved data when applying the completion"
14485 );
14486 });
14487}
14488
14489#[gpui::test]
14490async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14491 init_test(cx, |_| {});
14492
14493 let item_0 = lsp::CompletionItem {
14494 label: "abs".into(),
14495 insert_text: Some("abs".into()),
14496 data: Some(json!({ "very": "special"})),
14497 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14498 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14499 lsp::InsertReplaceEdit {
14500 new_text: "abs".to_string(),
14501 insert: lsp::Range::default(),
14502 replace: lsp::Range::default(),
14503 },
14504 )),
14505 ..lsp::CompletionItem::default()
14506 };
14507 let items = iter::once(item_0.clone())
14508 .chain((11..51).map(|i| lsp::CompletionItem {
14509 label: format!("item_{}", i),
14510 insert_text: Some(format!("item_{}", i)),
14511 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14512 ..lsp::CompletionItem::default()
14513 }))
14514 .collect::<Vec<_>>();
14515
14516 let default_commit_characters = vec!["?".to_string()];
14517 let default_data = json!({ "default": "data"});
14518 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14519 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14520 let default_edit_range = lsp::Range {
14521 start: lsp::Position {
14522 line: 0,
14523 character: 5,
14524 },
14525 end: lsp::Position {
14526 line: 0,
14527 character: 5,
14528 },
14529 };
14530
14531 let mut cx = EditorLspTestContext::new_rust(
14532 lsp::ServerCapabilities {
14533 completion_provider: Some(lsp::CompletionOptions {
14534 trigger_characters: Some(vec![".".to_string()]),
14535 resolve_provider: Some(true),
14536 ..Default::default()
14537 }),
14538 ..Default::default()
14539 },
14540 cx,
14541 )
14542 .await;
14543
14544 cx.set_state("fn main() { let a = 2ˇ; }");
14545 cx.simulate_keystroke(".");
14546
14547 let completion_data = default_data.clone();
14548 let completion_characters = default_commit_characters.clone();
14549 let completion_items = items.clone();
14550 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14551 let default_data = completion_data.clone();
14552 let default_commit_characters = completion_characters.clone();
14553 let items = completion_items.clone();
14554 async move {
14555 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14556 items,
14557 item_defaults: Some(lsp::CompletionListItemDefaults {
14558 data: Some(default_data.clone()),
14559 commit_characters: Some(default_commit_characters.clone()),
14560 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14561 default_edit_range,
14562 )),
14563 insert_text_format: Some(default_insert_text_format),
14564 insert_text_mode: Some(default_insert_text_mode),
14565 }),
14566 ..lsp::CompletionList::default()
14567 })))
14568 }
14569 })
14570 .next()
14571 .await;
14572
14573 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14574 cx.lsp
14575 .server
14576 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14577 let closure_resolved_items = resolved_items.clone();
14578 move |item_to_resolve, _| {
14579 let closure_resolved_items = closure_resolved_items.clone();
14580 async move {
14581 closure_resolved_items.lock().push(item_to_resolve.clone());
14582 Ok(item_to_resolve)
14583 }
14584 }
14585 })
14586 .detach();
14587
14588 cx.condition(|editor, _| editor.context_menu_visible())
14589 .await;
14590 cx.run_until_parked();
14591 cx.update_editor(|editor, _, _| {
14592 let menu = editor.context_menu.borrow_mut();
14593 match menu.as_ref().expect("should have the completions menu") {
14594 CodeContextMenu::Completions(completions_menu) => {
14595 assert_eq!(
14596 completions_menu
14597 .entries
14598 .borrow()
14599 .iter()
14600 .map(|mat| mat.string.clone())
14601 .collect::<Vec<String>>(),
14602 items
14603 .iter()
14604 .map(|completion| completion.label.clone())
14605 .collect::<Vec<String>>()
14606 );
14607 }
14608 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14609 }
14610 });
14611 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14612 // with 4 from the end.
14613 assert_eq!(
14614 *resolved_items.lock(),
14615 [&items[0..16], &items[items.len() - 4..items.len()]]
14616 .concat()
14617 .iter()
14618 .cloned()
14619 .map(|mut item| {
14620 if item.data.is_none() {
14621 item.data = Some(default_data.clone());
14622 }
14623 item
14624 })
14625 .collect::<Vec<lsp::CompletionItem>>(),
14626 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14627 );
14628 resolved_items.lock().clear();
14629
14630 cx.update_editor(|editor, window, cx| {
14631 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14632 });
14633 cx.run_until_parked();
14634 // Completions that have already been resolved are skipped.
14635 assert_eq!(
14636 *resolved_items.lock(),
14637 items[items.len() - 16..items.len() - 4]
14638 .iter()
14639 .cloned()
14640 .map(|mut item| {
14641 if item.data.is_none() {
14642 item.data = Some(default_data.clone());
14643 }
14644 item
14645 })
14646 .collect::<Vec<lsp::CompletionItem>>()
14647 );
14648 resolved_items.lock().clear();
14649}
14650
14651#[gpui::test]
14652async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14653 init_test(cx, |_| {});
14654
14655 let mut cx = EditorLspTestContext::new(
14656 Language::new(
14657 LanguageConfig {
14658 matcher: LanguageMatcher {
14659 path_suffixes: vec!["jsx".into()],
14660 ..Default::default()
14661 },
14662 overrides: [(
14663 "element".into(),
14664 LanguageConfigOverride {
14665 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14666 ..Default::default()
14667 },
14668 )]
14669 .into_iter()
14670 .collect(),
14671 ..Default::default()
14672 },
14673 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14674 )
14675 .with_override_query("(jsx_self_closing_element) @element")
14676 .unwrap(),
14677 lsp::ServerCapabilities {
14678 completion_provider: Some(lsp::CompletionOptions {
14679 trigger_characters: Some(vec![":".to_string()]),
14680 ..Default::default()
14681 }),
14682 ..Default::default()
14683 },
14684 cx,
14685 )
14686 .await;
14687
14688 cx.lsp
14689 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14690 Ok(Some(lsp::CompletionResponse::Array(vec![
14691 lsp::CompletionItem {
14692 label: "bg-blue".into(),
14693 ..Default::default()
14694 },
14695 lsp::CompletionItem {
14696 label: "bg-red".into(),
14697 ..Default::default()
14698 },
14699 lsp::CompletionItem {
14700 label: "bg-yellow".into(),
14701 ..Default::default()
14702 },
14703 ])))
14704 });
14705
14706 cx.set_state(r#"<p class="bgˇ" />"#);
14707
14708 // Trigger completion when typing a dash, because the dash is an extra
14709 // word character in the 'element' scope, which contains the cursor.
14710 cx.simulate_keystroke("-");
14711 cx.executor().run_until_parked();
14712 cx.update_editor(|editor, _, _| {
14713 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14714 {
14715 assert_eq!(
14716 completion_menu_entries(&menu),
14717 &["bg-red", "bg-blue", "bg-yellow"]
14718 );
14719 } else {
14720 panic!("expected completion menu to be open");
14721 }
14722 });
14723
14724 cx.simulate_keystroke("l");
14725 cx.executor().run_until_parked();
14726 cx.update_editor(|editor, _, _| {
14727 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14728 {
14729 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14730 } else {
14731 panic!("expected completion menu to be open");
14732 }
14733 });
14734
14735 // When filtering completions, consider the character after the '-' to
14736 // be the start of a subword.
14737 cx.set_state(r#"<p class="yelˇ" />"#);
14738 cx.simulate_keystroke("l");
14739 cx.executor().run_until_parked();
14740 cx.update_editor(|editor, _, _| {
14741 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14742 {
14743 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14744 } else {
14745 panic!("expected completion menu to be open");
14746 }
14747 });
14748}
14749
14750fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14751 let entries = menu.entries.borrow();
14752 entries.iter().map(|mat| mat.string.clone()).collect()
14753}
14754
14755#[gpui::test]
14756async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14757 init_test(cx, |settings| {
14758 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14759 FormatterList(vec![Formatter::Prettier].into()),
14760 ))
14761 });
14762
14763 let fs = FakeFs::new(cx.executor());
14764 fs.insert_file(path!("/file.ts"), Default::default()).await;
14765
14766 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14767 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14768
14769 language_registry.add(Arc::new(Language::new(
14770 LanguageConfig {
14771 name: "TypeScript".into(),
14772 matcher: LanguageMatcher {
14773 path_suffixes: vec!["ts".to_string()],
14774 ..Default::default()
14775 },
14776 ..Default::default()
14777 },
14778 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14779 )));
14780 update_test_language_settings(cx, |settings| {
14781 settings.defaults.prettier = Some(PrettierSettings {
14782 allowed: true,
14783 ..PrettierSettings::default()
14784 });
14785 });
14786
14787 let test_plugin = "test_plugin";
14788 let _ = language_registry.register_fake_lsp(
14789 "TypeScript",
14790 FakeLspAdapter {
14791 prettier_plugins: vec![test_plugin],
14792 ..Default::default()
14793 },
14794 );
14795
14796 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14797 let buffer = project
14798 .update(cx, |project, cx| {
14799 project.open_local_buffer(path!("/file.ts"), cx)
14800 })
14801 .await
14802 .unwrap();
14803
14804 let buffer_text = "one\ntwo\nthree\n";
14805 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14806 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14807 editor.update_in(cx, |editor, window, cx| {
14808 editor.set_text(buffer_text, window, cx)
14809 });
14810
14811 editor
14812 .update_in(cx, |editor, window, cx| {
14813 editor.perform_format(
14814 project.clone(),
14815 FormatTrigger::Manual,
14816 FormatTarget::Buffers,
14817 window,
14818 cx,
14819 )
14820 })
14821 .unwrap()
14822 .await;
14823 assert_eq!(
14824 editor.update(cx, |editor, cx| editor.text(cx)),
14825 buffer_text.to_string() + prettier_format_suffix,
14826 "Test prettier formatting was not applied to the original buffer text",
14827 );
14828
14829 update_test_language_settings(cx, |settings| {
14830 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14831 });
14832 let format = editor.update_in(cx, |editor, window, cx| {
14833 editor.perform_format(
14834 project.clone(),
14835 FormatTrigger::Manual,
14836 FormatTarget::Buffers,
14837 window,
14838 cx,
14839 )
14840 });
14841 format.await.unwrap();
14842 assert_eq!(
14843 editor.update(cx, |editor, cx| editor.text(cx)),
14844 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14845 "Autoformatting (via test prettier) was not applied to the original buffer text",
14846 );
14847}
14848
14849#[gpui::test]
14850async fn test_addition_reverts(cx: &mut TestAppContext) {
14851 init_test(cx, |_| {});
14852 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14853 let base_text = indoc! {r#"
14854 struct Row;
14855 struct Row1;
14856 struct Row2;
14857
14858 struct Row4;
14859 struct Row5;
14860 struct Row6;
14861
14862 struct Row8;
14863 struct Row9;
14864 struct Row10;"#};
14865
14866 // When addition hunks are not adjacent to carets, no hunk revert is performed
14867 assert_hunk_revert(
14868 indoc! {r#"struct Row;
14869 struct Row1;
14870 struct Row1.1;
14871 struct Row1.2;
14872 struct Row2;ˇ
14873
14874 struct Row4;
14875 struct Row5;
14876 struct Row6;
14877
14878 struct Row8;
14879 ˇstruct Row9;
14880 struct Row9.1;
14881 struct Row9.2;
14882 struct Row9.3;
14883 struct Row10;"#},
14884 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14885 indoc! {r#"struct Row;
14886 struct Row1;
14887 struct Row1.1;
14888 struct Row1.2;
14889 struct Row2;ˇ
14890
14891 struct Row4;
14892 struct Row5;
14893 struct Row6;
14894
14895 struct Row8;
14896 ˇstruct Row9;
14897 struct Row9.1;
14898 struct Row9.2;
14899 struct Row9.3;
14900 struct Row10;"#},
14901 base_text,
14902 &mut cx,
14903 );
14904 // Same for selections
14905 assert_hunk_revert(
14906 indoc! {r#"struct Row;
14907 struct Row1;
14908 struct Row2;
14909 struct Row2.1;
14910 struct Row2.2;
14911 «ˇ
14912 struct Row4;
14913 struct» Row5;
14914 «struct Row6;
14915 ˇ»
14916 struct Row9.1;
14917 struct Row9.2;
14918 struct Row9.3;
14919 struct Row8;
14920 struct Row9;
14921 struct Row10;"#},
14922 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14923 indoc! {r#"struct Row;
14924 struct Row1;
14925 struct Row2;
14926 struct Row2.1;
14927 struct Row2.2;
14928 «ˇ
14929 struct Row4;
14930 struct» Row5;
14931 «struct Row6;
14932 ˇ»
14933 struct Row9.1;
14934 struct Row9.2;
14935 struct Row9.3;
14936 struct Row8;
14937 struct Row9;
14938 struct Row10;"#},
14939 base_text,
14940 &mut cx,
14941 );
14942
14943 // When carets and selections intersect the addition hunks, those are reverted.
14944 // Adjacent carets got merged.
14945 assert_hunk_revert(
14946 indoc! {r#"struct Row;
14947 ˇ// something on the top
14948 struct Row1;
14949 struct Row2;
14950 struct Roˇw3.1;
14951 struct Row2.2;
14952 struct Row2.3;ˇ
14953
14954 struct Row4;
14955 struct ˇRow5.1;
14956 struct Row5.2;
14957 struct «Rowˇ»5.3;
14958 struct Row5;
14959 struct Row6;
14960 ˇ
14961 struct Row9.1;
14962 struct «Rowˇ»9.2;
14963 struct «ˇRow»9.3;
14964 struct Row8;
14965 struct Row9;
14966 «ˇ// something on bottom»
14967 struct Row10;"#},
14968 vec![
14969 DiffHunkStatusKind::Added,
14970 DiffHunkStatusKind::Added,
14971 DiffHunkStatusKind::Added,
14972 DiffHunkStatusKind::Added,
14973 DiffHunkStatusKind::Added,
14974 ],
14975 indoc! {r#"struct Row;
14976 ˇstruct Row1;
14977 struct Row2;
14978 ˇ
14979 struct Row4;
14980 ˇstruct Row5;
14981 struct Row6;
14982 ˇ
14983 ˇstruct Row8;
14984 struct Row9;
14985 ˇstruct Row10;"#},
14986 base_text,
14987 &mut cx,
14988 );
14989}
14990
14991#[gpui::test]
14992async fn test_modification_reverts(cx: &mut TestAppContext) {
14993 init_test(cx, |_| {});
14994 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14995 let base_text = indoc! {r#"
14996 struct Row;
14997 struct Row1;
14998 struct Row2;
14999
15000 struct Row4;
15001 struct Row5;
15002 struct Row6;
15003
15004 struct Row8;
15005 struct Row9;
15006 struct Row10;"#};
15007
15008 // Modification hunks behave the same as the addition ones.
15009 assert_hunk_revert(
15010 indoc! {r#"struct Row;
15011 struct Row1;
15012 struct Row33;
15013 ˇ
15014 struct Row4;
15015 struct Row5;
15016 struct Row6;
15017 ˇ
15018 struct Row99;
15019 struct Row9;
15020 struct Row10;"#},
15021 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15022 indoc! {r#"struct Row;
15023 struct Row1;
15024 struct Row33;
15025 ˇ
15026 struct Row4;
15027 struct Row5;
15028 struct Row6;
15029 ˇ
15030 struct Row99;
15031 struct Row9;
15032 struct Row10;"#},
15033 base_text,
15034 &mut cx,
15035 );
15036 assert_hunk_revert(
15037 indoc! {r#"struct Row;
15038 struct Row1;
15039 struct Row33;
15040 «ˇ
15041 struct Row4;
15042 struct» Row5;
15043 «struct Row6;
15044 ˇ»
15045 struct Row99;
15046 struct Row9;
15047 struct Row10;"#},
15048 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15049 indoc! {r#"struct Row;
15050 struct Row1;
15051 struct Row33;
15052 «ˇ
15053 struct Row4;
15054 struct» Row5;
15055 «struct Row6;
15056 ˇ»
15057 struct Row99;
15058 struct Row9;
15059 struct Row10;"#},
15060 base_text,
15061 &mut cx,
15062 );
15063
15064 assert_hunk_revert(
15065 indoc! {r#"ˇstruct Row1.1;
15066 struct Row1;
15067 «ˇstr»uct Row22;
15068
15069 struct ˇRow44;
15070 struct Row5;
15071 struct «Rˇ»ow66;ˇ
15072
15073 «struˇ»ct Row88;
15074 struct Row9;
15075 struct Row1011;ˇ"#},
15076 vec![
15077 DiffHunkStatusKind::Modified,
15078 DiffHunkStatusKind::Modified,
15079 DiffHunkStatusKind::Modified,
15080 DiffHunkStatusKind::Modified,
15081 DiffHunkStatusKind::Modified,
15082 DiffHunkStatusKind::Modified,
15083 ],
15084 indoc! {r#"struct Row;
15085 ˇstruct Row1;
15086 struct Row2;
15087 ˇ
15088 struct Row4;
15089 ˇstruct Row5;
15090 struct Row6;
15091 ˇ
15092 struct Row8;
15093 ˇstruct Row9;
15094 struct Row10;ˇ"#},
15095 base_text,
15096 &mut cx,
15097 );
15098}
15099
15100#[gpui::test]
15101async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15102 init_test(cx, |_| {});
15103 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15104 let base_text = indoc! {r#"
15105 one
15106
15107 two
15108 three
15109 "#};
15110
15111 cx.set_head_text(base_text);
15112 cx.set_state("\nˇ\n");
15113 cx.executor().run_until_parked();
15114 cx.update_editor(|editor, _window, cx| {
15115 editor.expand_selected_diff_hunks(cx);
15116 });
15117 cx.executor().run_until_parked();
15118 cx.update_editor(|editor, window, cx| {
15119 editor.backspace(&Default::default(), window, cx);
15120 });
15121 cx.run_until_parked();
15122 cx.assert_state_with_diff(
15123 indoc! {r#"
15124
15125 - two
15126 - threeˇ
15127 +
15128 "#}
15129 .to_string(),
15130 );
15131}
15132
15133#[gpui::test]
15134async fn test_deletion_reverts(cx: &mut TestAppContext) {
15135 init_test(cx, |_| {});
15136 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15137 let base_text = indoc! {r#"struct Row;
15138struct Row1;
15139struct Row2;
15140
15141struct Row4;
15142struct Row5;
15143struct Row6;
15144
15145struct Row8;
15146struct Row9;
15147struct Row10;"#};
15148
15149 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15150 assert_hunk_revert(
15151 indoc! {r#"struct Row;
15152 struct Row2;
15153
15154 ˇstruct Row4;
15155 struct Row5;
15156 struct Row6;
15157 ˇ
15158 struct Row8;
15159 struct Row10;"#},
15160 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15161 indoc! {r#"struct Row;
15162 struct Row2;
15163
15164 ˇstruct Row4;
15165 struct Row5;
15166 struct Row6;
15167 ˇ
15168 struct Row8;
15169 struct Row10;"#},
15170 base_text,
15171 &mut cx,
15172 );
15173 assert_hunk_revert(
15174 indoc! {r#"struct Row;
15175 struct Row2;
15176
15177 «ˇstruct Row4;
15178 struct» Row5;
15179 «struct Row6;
15180 ˇ»
15181 struct Row8;
15182 struct Row10;"#},
15183 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15184 indoc! {r#"struct Row;
15185 struct Row2;
15186
15187 «ˇstruct Row4;
15188 struct» Row5;
15189 «struct Row6;
15190 ˇ»
15191 struct Row8;
15192 struct Row10;"#},
15193 base_text,
15194 &mut cx,
15195 );
15196
15197 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15198 assert_hunk_revert(
15199 indoc! {r#"struct Row;
15200 ˇstruct Row2;
15201
15202 struct Row4;
15203 struct Row5;
15204 struct Row6;
15205
15206 struct Row8;ˇ
15207 struct Row10;"#},
15208 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15209 indoc! {r#"struct Row;
15210 struct Row1;
15211 ˇstruct Row2;
15212
15213 struct Row4;
15214 struct Row5;
15215 struct Row6;
15216
15217 struct Row8;ˇ
15218 struct Row9;
15219 struct Row10;"#},
15220 base_text,
15221 &mut cx,
15222 );
15223 assert_hunk_revert(
15224 indoc! {r#"struct Row;
15225 struct Row2«ˇ;
15226 struct Row4;
15227 struct» Row5;
15228 «struct Row6;
15229
15230 struct Row8;ˇ»
15231 struct Row10;"#},
15232 vec![
15233 DiffHunkStatusKind::Deleted,
15234 DiffHunkStatusKind::Deleted,
15235 DiffHunkStatusKind::Deleted,
15236 ],
15237 indoc! {r#"struct Row;
15238 struct Row1;
15239 struct Row2«ˇ;
15240
15241 struct Row4;
15242 struct» Row5;
15243 «struct Row6;
15244
15245 struct Row8;ˇ»
15246 struct Row9;
15247 struct Row10;"#},
15248 base_text,
15249 &mut cx,
15250 );
15251}
15252
15253#[gpui::test]
15254async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15255 init_test(cx, |_| {});
15256
15257 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15258 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15259 let base_text_3 =
15260 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15261
15262 let text_1 = edit_first_char_of_every_line(base_text_1);
15263 let text_2 = edit_first_char_of_every_line(base_text_2);
15264 let text_3 = edit_first_char_of_every_line(base_text_3);
15265
15266 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15267 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15268 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15269
15270 let multibuffer = cx.new(|cx| {
15271 let mut multibuffer = MultiBuffer::new(ReadWrite);
15272 multibuffer.push_excerpts(
15273 buffer_1.clone(),
15274 [
15275 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15276 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15277 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15278 ],
15279 cx,
15280 );
15281 multibuffer.push_excerpts(
15282 buffer_2.clone(),
15283 [
15284 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15285 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15286 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15287 ],
15288 cx,
15289 );
15290 multibuffer.push_excerpts(
15291 buffer_3.clone(),
15292 [
15293 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15294 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15295 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15296 ],
15297 cx,
15298 );
15299 multibuffer
15300 });
15301
15302 let fs = FakeFs::new(cx.executor());
15303 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15304 let (editor, cx) = cx
15305 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15306 editor.update_in(cx, |editor, _window, cx| {
15307 for (buffer, diff_base) in [
15308 (buffer_1.clone(), base_text_1),
15309 (buffer_2.clone(), base_text_2),
15310 (buffer_3.clone(), base_text_3),
15311 ] {
15312 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15313 editor
15314 .buffer
15315 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15316 }
15317 });
15318 cx.executor().run_until_parked();
15319
15320 editor.update_in(cx, |editor, window, cx| {
15321 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}");
15322 editor.select_all(&SelectAll, window, cx);
15323 editor.git_restore(&Default::default(), window, cx);
15324 });
15325 cx.executor().run_until_parked();
15326
15327 // When all ranges are selected, all buffer hunks are reverted.
15328 editor.update(cx, |editor, cx| {
15329 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");
15330 });
15331 buffer_1.update(cx, |buffer, _| {
15332 assert_eq!(buffer.text(), base_text_1);
15333 });
15334 buffer_2.update(cx, |buffer, _| {
15335 assert_eq!(buffer.text(), base_text_2);
15336 });
15337 buffer_3.update(cx, |buffer, _| {
15338 assert_eq!(buffer.text(), base_text_3);
15339 });
15340
15341 editor.update_in(cx, |editor, window, cx| {
15342 editor.undo(&Default::default(), window, cx);
15343 });
15344
15345 editor.update_in(cx, |editor, window, cx| {
15346 editor.change_selections(None, window, cx, |s| {
15347 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15348 });
15349 editor.git_restore(&Default::default(), window, cx);
15350 });
15351
15352 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15353 // but not affect buffer_2 and its related excerpts.
15354 editor.update(cx, |editor, cx| {
15355 assert_eq!(
15356 editor.text(cx),
15357 "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}"
15358 );
15359 });
15360 buffer_1.update(cx, |buffer, _| {
15361 assert_eq!(buffer.text(), base_text_1);
15362 });
15363 buffer_2.update(cx, |buffer, _| {
15364 assert_eq!(
15365 buffer.text(),
15366 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15367 );
15368 });
15369 buffer_3.update(cx, |buffer, _| {
15370 assert_eq!(
15371 buffer.text(),
15372 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15373 );
15374 });
15375
15376 fn edit_first_char_of_every_line(text: &str) -> String {
15377 text.split('\n')
15378 .map(|line| format!("X{}", &line[1..]))
15379 .collect::<Vec<_>>()
15380 .join("\n")
15381 }
15382}
15383
15384#[gpui::test]
15385async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15386 init_test(cx, |_| {});
15387
15388 let cols = 4;
15389 let rows = 10;
15390 let sample_text_1 = sample_text(rows, cols, 'a');
15391 assert_eq!(
15392 sample_text_1,
15393 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15394 );
15395 let sample_text_2 = sample_text(rows, cols, 'l');
15396 assert_eq!(
15397 sample_text_2,
15398 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15399 );
15400 let sample_text_3 = sample_text(rows, cols, 'v');
15401 assert_eq!(
15402 sample_text_3,
15403 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15404 );
15405
15406 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15407 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15408 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15409
15410 let multi_buffer = cx.new(|cx| {
15411 let mut multibuffer = MultiBuffer::new(ReadWrite);
15412 multibuffer.push_excerpts(
15413 buffer_1.clone(),
15414 [
15415 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15416 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15417 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15418 ],
15419 cx,
15420 );
15421 multibuffer.push_excerpts(
15422 buffer_2.clone(),
15423 [
15424 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15425 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15426 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15427 ],
15428 cx,
15429 );
15430 multibuffer.push_excerpts(
15431 buffer_3.clone(),
15432 [
15433 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15434 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15435 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15436 ],
15437 cx,
15438 );
15439 multibuffer
15440 });
15441
15442 let fs = FakeFs::new(cx.executor());
15443 fs.insert_tree(
15444 "/a",
15445 json!({
15446 "main.rs": sample_text_1,
15447 "other.rs": sample_text_2,
15448 "lib.rs": sample_text_3,
15449 }),
15450 )
15451 .await;
15452 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15453 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15454 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15455 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15456 Editor::new(
15457 EditorMode::full(),
15458 multi_buffer,
15459 Some(project.clone()),
15460 window,
15461 cx,
15462 )
15463 });
15464 let multibuffer_item_id = workspace
15465 .update(cx, |workspace, window, cx| {
15466 assert!(
15467 workspace.active_item(cx).is_none(),
15468 "active item should be None before the first item is added"
15469 );
15470 workspace.add_item_to_active_pane(
15471 Box::new(multi_buffer_editor.clone()),
15472 None,
15473 true,
15474 window,
15475 cx,
15476 );
15477 let active_item = workspace
15478 .active_item(cx)
15479 .expect("should have an active item after adding the multi buffer");
15480 assert!(
15481 !active_item.is_singleton(cx),
15482 "A multi buffer was expected to active after adding"
15483 );
15484 active_item.item_id()
15485 })
15486 .unwrap();
15487 cx.executor().run_until_parked();
15488
15489 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15490 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15491 s.select_ranges(Some(1..2))
15492 });
15493 editor.open_excerpts(&OpenExcerpts, window, cx);
15494 });
15495 cx.executor().run_until_parked();
15496 let first_item_id = workspace
15497 .update(cx, |workspace, window, cx| {
15498 let active_item = workspace
15499 .active_item(cx)
15500 .expect("should have an active item after navigating into the 1st buffer");
15501 let first_item_id = active_item.item_id();
15502 assert_ne!(
15503 first_item_id, multibuffer_item_id,
15504 "Should navigate into the 1st buffer and activate it"
15505 );
15506 assert!(
15507 active_item.is_singleton(cx),
15508 "New active item should be a singleton buffer"
15509 );
15510 assert_eq!(
15511 active_item
15512 .act_as::<Editor>(cx)
15513 .expect("should have navigated into an editor for the 1st buffer")
15514 .read(cx)
15515 .text(cx),
15516 sample_text_1
15517 );
15518
15519 workspace
15520 .go_back(workspace.active_pane().downgrade(), window, cx)
15521 .detach_and_log_err(cx);
15522
15523 first_item_id
15524 })
15525 .unwrap();
15526 cx.executor().run_until_parked();
15527 workspace
15528 .update(cx, |workspace, _, cx| {
15529 let active_item = workspace
15530 .active_item(cx)
15531 .expect("should have an active item after navigating back");
15532 assert_eq!(
15533 active_item.item_id(),
15534 multibuffer_item_id,
15535 "Should navigate back to the multi buffer"
15536 );
15537 assert!(!active_item.is_singleton(cx));
15538 })
15539 .unwrap();
15540
15541 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15542 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15543 s.select_ranges(Some(39..40))
15544 });
15545 editor.open_excerpts(&OpenExcerpts, window, cx);
15546 });
15547 cx.executor().run_until_parked();
15548 let second_item_id = workspace
15549 .update(cx, |workspace, window, cx| {
15550 let active_item = workspace
15551 .active_item(cx)
15552 .expect("should have an active item after navigating into the 2nd buffer");
15553 let second_item_id = active_item.item_id();
15554 assert_ne!(
15555 second_item_id, multibuffer_item_id,
15556 "Should navigate away from the multibuffer"
15557 );
15558 assert_ne!(
15559 second_item_id, first_item_id,
15560 "Should navigate into the 2nd buffer and activate it"
15561 );
15562 assert!(
15563 active_item.is_singleton(cx),
15564 "New active item should be a singleton buffer"
15565 );
15566 assert_eq!(
15567 active_item
15568 .act_as::<Editor>(cx)
15569 .expect("should have navigated into an editor")
15570 .read(cx)
15571 .text(cx),
15572 sample_text_2
15573 );
15574
15575 workspace
15576 .go_back(workspace.active_pane().downgrade(), window, cx)
15577 .detach_and_log_err(cx);
15578
15579 second_item_id
15580 })
15581 .unwrap();
15582 cx.executor().run_until_parked();
15583 workspace
15584 .update(cx, |workspace, _, cx| {
15585 let active_item = workspace
15586 .active_item(cx)
15587 .expect("should have an active item after navigating back from the 2nd buffer");
15588 assert_eq!(
15589 active_item.item_id(),
15590 multibuffer_item_id,
15591 "Should navigate back from the 2nd buffer to the multi buffer"
15592 );
15593 assert!(!active_item.is_singleton(cx));
15594 })
15595 .unwrap();
15596
15597 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15598 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15599 s.select_ranges(Some(70..70))
15600 });
15601 editor.open_excerpts(&OpenExcerpts, window, cx);
15602 });
15603 cx.executor().run_until_parked();
15604 workspace
15605 .update(cx, |workspace, window, cx| {
15606 let active_item = workspace
15607 .active_item(cx)
15608 .expect("should have an active item after navigating into the 3rd buffer");
15609 let third_item_id = active_item.item_id();
15610 assert_ne!(
15611 third_item_id, multibuffer_item_id,
15612 "Should navigate into the 3rd buffer and activate it"
15613 );
15614 assert_ne!(third_item_id, first_item_id);
15615 assert_ne!(third_item_id, second_item_id);
15616 assert!(
15617 active_item.is_singleton(cx),
15618 "New active item should be a singleton buffer"
15619 );
15620 assert_eq!(
15621 active_item
15622 .act_as::<Editor>(cx)
15623 .expect("should have navigated into an editor")
15624 .read(cx)
15625 .text(cx),
15626 sample_text_3
15627 );
15628
15629 workspace
15630 .go_back(workspace.active_pane().downgrade(), window, cx)
15631 .detach_and_log_err(cx);
15632 })
15633 .unwrap();
15634 cx.executor().run_until_parked();
15635 workspace
15636 .update(cx, |workspace, _, cx| {
15637 let active_item = workspace
15638 .active_item(cx)
15639 .expect("should have an active item after navigating back from the 3rd buffer");
15640 assert_eq!(
15641 active_item.item_id(),
15642 multibuffer_item_id,
15643 "Should navigate back from the 3rd buffer to the multi buffer"
15644 );
15645 assert!(!active_item.is_singleton(cx));
15646 })
15647 .unwrap();
15648}
15649
15650#[gpui::test]
15651async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15652 init_test(cx, |_| {});
15653
15654 let mut cx = EditorTestContext::new(cx).await;
15655
15656 let diff_base = r#"
15657 use some::mod;
15658
15659 const A: u32 = 42;
15660
15661 fn main() {
15662 println!("hello");
15663
15664 println!("world");
15665 }
15666 "#
15667 .unindent();
15668
15669 cx.set_state(
15670 &r#"
15671 use some::modified;
15672
15673 ˇ
15674 fn main() {
15675 println!("hello there");
15676
15677 println!("around the");
15678 println!("world");
15679 }
15680 "#
15681 .unindent(),
15682 );
15683
15684 cx.set_head_text(&diff_base);
15685 executor.run_until_parked();
15686
15687 cx.update_editor(|editor, window, cx| {
15688 editor.go_to_next_hunk(&GoToHunk, window, cx);
15689 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15690 });
15691 executor.run_until_parked();
15692 cx.assert_state_with_diff(
15693 r#"
15694 use some::modified;
15695
15696
15697 fn main() {
15698 - println!("hello");
15699 + ˇ println!("hello there");
15700
15701 println!("around the");
15702 println!("world");
15703 }
15704 "#
15705 .unindent(),
15706 );
15707
15708 cx.update_editor(|editor, window, cx| {
15709 for _ in 0..2 {
15710 editor.go_to_next_hunk(&GoToHunk, window, cx);
15711 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15712 }
15713 });
15714 executor.run_until_parked();
15715 cx.assert_state_with_diff(
15716 r#"
15717 - use some::mod;
15718 + ˇuse some::modified;
15719
15720
15721 fn main() {
15722 - println!("hello");
15723 + println!("hello there");
15724
15725 + println!("around the");
15726 println!("world");
15727 }
15728 "#
15729 .unindent(),
15730 );
15731
15732 cx.update_editor(|editor, window, cx| {
15733 editor.go_to_next_hunk(&GoToHunk, window, cx);
15734 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15735 });
15736 executor.run_until_parked();
15737 cx.assert_state_with_diff(
15738 r#"
15739 - use some::mod;
15740 + use some::modified;
15741
15742 - const A: u32 = 42;
15743 ˇ
15744 fn main() {
15745 - println!("hello");
15746 + println!("hello there");
15747
15748 + println!("around the");
15749 println!("world");
15750 }
15751 "#
15752 .unindent(),
15753 );
15754
15755 cx.update_editor(|editor, window, cx| {
15756 editor.cancel(&Cancel, window, cx);
15757 });
15758
15759 cx.assert_state_with_diff(
15760 r#"
15761 use some::modified;
15762
15763 ˇ
15764 fn main() {
15765 println!("hello there");
15766
15767 println!("around the");
15768 println!("world");
15769 }
15770 "#
15771 .unindent(),
15772 );
15773}
15774
15775#[gpui::test]
15776async fn test_diff_base_change_with_expanded_diff_hunks(
15777 executor: BackgroundExecutor,
15778 cx: &mut TestAppContext,
15779) {
15780 init_test(cx, |_| {});
15781
15782 let mut cx = EditorTestContext::new(cx).await;
15783
15784 let diff_base = r#"
15785 use some::mod1;
15786 use some::mod2;
15787
15788 const A: u32 = 42;
15789 const B: u32 = 42;
15790 const C: u32 = 42;
15791
15792 fn main() {
15793 println!("hello");
15794
15795 println!("world");
15796 }
15797 "#
15798 .unindent();
15799
15800 cx.set_state(
15801 &r#"
15802 use some::mod2;
15803
15804 const A: u32 = 42;
15805 const C: u32 = 42;
15806
15807 fn main(ˇ) {
15808 //println!("hello");
15809
15810 println!("world");
15811 //
15812 //
15813 }
15814 "#
15815 .unindent(),
15816 );
15817
15818 cx.set_head_text(&diff_base);
15819 executor.run_until_parked();
15820
15821 cx.update_editor(|editor, window, cx| {
15822 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15823 });
15824 executor.run_until_parked();
15825 cx.assert_state_with_diff(
15826 r#"
15827 - use some::mod1;
15828 use some::mod2;
15829
15830 const A: u32 = 42;
15831 - const B: u32 = 42;
15832 const C: u32 = 42;
15833
15834 fn main(ˇ) {
15835 - println!("hello");
15836 + //println!("hello");
15837
15838 println!("world");
15839 + //
15840 + //
15841 }
15842 "#
15843 .unindent(),
15844 );
15845
15846 cx.set_head_text("new diff base!");
15847 executor.run_until_parked();
15848 cx.assert_state_with_diff(
15849 r#"
15850 - new diff base!
15851 + use some::mod2;
15852 +
15853 + const A: u32 = 42;
15854 + const C: u32 = 42;
15855 +
15856 + fn main(ˇ) {
15857 + //println!("hello");
15858 +
15859 + println!("world");
15860 + //
15861 + //
15862 + }
15863 "#
15864 .unindent(),
15865 );
15866}
15867
15868#[gpui::test]
15869async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15870 init_test(cx, |_| {});
15871
15872 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15873 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15874 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15875 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15876 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15877 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15878
15879 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15880 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15881 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15882
15883 let multi_buffer = cx.new(|cx| {
15884 let mut multibuffer = MultiBuffer::new(ReadWrite);
15885 multibuffer.push_excerpts(
15886 buffer_1.clone(),
15887 [
15888 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15889 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15890 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15891 ],
15892 cx,
15893 );
15894 multibuffer.push_excerpts(
15895 buffer_2.clone(),
15896 [
15897 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15898 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15899 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15900 ],
15901 cx,
15902 );
15903 multibuffer.push_excerpts(
15904 buffer_3.clone(),
15905 [
15906 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15907 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15908 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15909 ],
15910 cx,
15911 );
15912 multibuffer
15913 });
15914
15915 let editor =
15916 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15917 editor
15918 .update(cx, |editor, _window, cx| {
15919 for (buffer, diff_base) in [
15920 (buffer_1.clone(), file_1_old),
15921 (buffer_2.clone(), file_2_old),
15922 (buffer_3.clone(), file_3_old),
15923 ] {
15924 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15925 editor
15926 .buffer
15927 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15928 }
15929 })
15930 .unwrap();
15931
15932 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15933 cx.run_until_parked();
15934
15935 cx.assert_editor_state(
15936 &"
15937 ˇaaa
15938 ccc
15939 ddd
15940
15941 ggg
15942 hhh
15943
15944
15945 lll
15946 mmm
15947 NNN
15948
15949 qqq
15950 rrr
15951
15952 uuu
15953 111
15954 222
15955 333
15956
15957 666
15958 777
15959
15960 000
15961 !!!"
15962 .unindent(),
15963 );
15964
15965 cx.update_editor(|editor, window, cx| {
15966 editor.select_all(&SelectAll, window, cx);
15967 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15968 });
15969 cx.executor().run_until_parked();
15970
15971 cx.assert_state_with_diff(
15972 "
15973 «aaa
15974 - bbb
15975 ccc
15976 ddd
15977
15978 ggg
15979 hhh
15980
15981
15982 lll
15983 mmm
15984 - nnn
15985 + NNN
15986
15987 qqq
15988 rrr
15989
15990 uuu
15991 111
15992 222
15993 333
15994
15995 + 666
15996 777
15997
15998 000
15999 !!!ˇ»"
16000 .unindent(),
16001 );
16002}
16003
16004#[gpui::test]
16005async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
16006 init_test(cx, |_| {});
16007
16008 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
16009 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
16010
16011 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
16012 let multi_buffer = cx.new(|cx| {
16013 let mut multibuffer = MultiBuffer::new(ReadWrite);
16014 multibuffer.push_excerpts(
16015 buffer.clone(),
16016 [
16017 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16018 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16019 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16020 ],
16021 cx,
16022 );
16023 multibuffer
16024 });
16025
16026 let editor =
16027 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16028 editor
16029 .update(cx, |editor, _window, cx| {
16030 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16031 editor
16032 .buffer
16033 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16034 })
16035 .unwrap();
16036
16037 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16038 cx.run_until_parked();
16039
16040 cx.update_editor(|editor, window, cx| {
16041 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16042 });
16043 cx.executor().run_until_parked();
16044
16045 // When the start of a hunk coincides with the start of its excerpt,
16046 // the hunk is expanded. When the start of a a hunk is earlier than
16047 // the start of its excerpt, the hunk is not expanded.
16048 cx.assert_state_with_diff(
16049 "
16050 ˇaaa
16051 - bbb
16052 + BBB
16053
16054 - ddd
16055 - eee
16056 + DDD
16057 + EEE
16058 fff
16059
16060 iii
16061 "
16062 .unindent(),
16063 );
16064}
16065
16066#[gpui::test]
16067async fn test_edits_around_expanded_insertion_hunks(
16068 executor: BackgroundExecutor,
16069 cx: &mut TestAppContext,
16070) {
16071 init_test(cx, |_| {});
16072
16073 let mut cx = EditorTestContext::new(cx).await;
16074
16075 let diff_base = r#"
16076 use some::mod1;
16077 use some::mod2;
16078
16079 const A: u32 = 42;
16080
16081 fn main() {
16082 println!("hello");
16083
16084 println!("world");
16085 }
16086 "#
16087 .unindent();
16088 executor.run_until_parked();
16089 cx.set_state(
16090 &r#"
16091 use some::mod1;
16092 use some::mod2;
16093
16094 const A: u32 = 42;
16095 const B: u32 = 42;
16096 const C: u32 = 42;
16097 ˇ
16098
16099 fn main() {
16100 println!("hello");
16101
16102 println!("world");
16103 }
16104 "#
16105 .unindent(),
16106 );
16107
16108 cx.set_head_text(&diff_base);
16109 executor.run_until_parked();
16110
16111 cx.update_editor(|editor, window, cx| {
16112 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16113 });
16114 executor.run_until_parked();
16115
16116 cx.assert_state_with_diff(
16117 r#"
16118 use some::mod1;
16119 use some::mod2;
16120
16121 const A: u32 = 42;
16122 + const B: u32 = 42;
16123 + const C: u32 = 42;
16124 + ˇ
16125
16126 fn main() {
16127 println!("hello");
16128
16129 println!("world");
16130 }
16131 "#
16132 .unindent(),
16133 );
16134
16135 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16136 executor.run_until_parked();
16137
16138 cx.assert_state_with_diff(
16139 r#"
16140 use some::mod1;
16141 use some::mod2;
16142
16143 const A: u32 = 42;
16144 + const B: u32 = 42;
16145 + const C: u32 = 42;
16146 + const D: u32 = 42;
16147 + ˇ
16148
16149 fn main() {
16150 println!("hello");
16151
16152 println!("world");
16153 }
16154 "#
16155 .unindent(),
16156 );
16157
16158 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16159 executor.run_until_parked();
16160
16161 cx.assert_state_with_diff(
16162 r#"
16163 use some::mod1;
16164 use some::mod2;
16165
16166 const A: u32 = 42;
16167 + const B: u32 = 42;
16168 + const C: u32 = 42;
16169 + const D: u32 = 42;
16170 + const E: u32 = 42;
16171 + ˇ
16172
16173 fn main() {
16174 println!("hello");
16175
16176 println!("world");
16177 }
16178 "#
16179 .unindent(),
16180 );
16181
16182 cx.update_editor(|editor, window, cx| {
16183 editor.delete_line(&DeleteLine, window, cx);
16184 });
16185 executor.run_until_parked();
16186
16187 cx.assert_state_with_diff(
16188 r#"
16189 use some::mod1;
16190 use some::mod2;
16191
16192 const A: u32 = 42;
16193 + const B: u32 = 42;
16194 + const C: u32 = 42;
16195 + const D: u32 = 42;
16196 + const E: u32 = 42;
16197 ˇ
16198 fn main() {
16199 println!("hello");
16200
16201 println!("world");
16202 }
16203 "#
16204 .unindent(),
16205 );
16206
16207 cx.update_editor(|editor, window, cx| {
16208 editor.move_up(&MoveUp, window, cx);
16209 editor.delete_line(&DeleteLine, window, cx);
16210 editor.move_up(&MoveUp, window, cx);
16211 editor.delete_line(&DeleteLine, window, cx);
16212 editor.move_up(&MoveUp, window, cx);
16213 editor.delete_line(&DeleteLine, window, cx);
16214 });
16215 executor.run_until_parked();
16216 cx.assert_state_with_diff(
16217 r#"
16218 use some::mod1;
16219 use some::mod2;
16220
16221 const A: u32 = 42;
16222 + const B: u32 = 42;
16223 ˇ
16224 fn main() {
16225 println!("hello");
16226
16227 println!("world");
16228 }
16229 "#
16230 .unindent(),
16231 );
16232
16233 cx.update_editor(|editor, window, cx| {
16234 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16235 editor.delete_line(&DeleteLine, window, cx);
16236 });
16237 executor.run_until_parked();
16238 cx.assert_state_with_diff(
16239 r#"
16240 ˇ
16241 fn main() {
16242 println!("hello");
16243
16244 println!("world");
16245 }
16246 "#
16247 .unindent(),
16248 );
16249}
16250
16251#[gpui::test]
16252async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16253 init_test(cx, |_| {});
16254
16255 let mut cx = EditorTestContext::new(cx).await;
16256 cx.set_head_text(indoc! { "
16257 one
16258 two
16259 three
16260 four
16261 five
16262 "
16263 });
16264 cx.set_state(indoc! { "
16265 one
16266 ˇthree
16267 five
16268 "});
16269 cx.run_until_parked();
16270 cx.update_editor(|editor, window, cx| {
16271 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16272 });
16273 cx.assert_state_with_diff(
16274 indoc! { "
16275 one
16276 - two
16277 ˇthree
16278 - four
16279 five
16280 "}
16281 .to_string(),
16282 );
16283 cx.update_editor(|editor, window, cx| {
16284 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16285 });
16286
16287 cx.assert_state_with_diff(
16288 indoc! { "
16289 one
16290 ˇthree
16291 five
16292 "}
16293 .to_string(),
16294 );
16295
16296 cx.set_state(indoc! { "
16297 one
16298 ˇTWO
16299 three
16300 four
16301 five
16302 "});
16303 cx.run_until_parked();
16304 cx.update_editor(|editor, window, cx| {
16305 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16306 });
16307
16308 cx.assert_state_with_diff(
16309 indoc! { "
16310 one
16311 - two
16312 + ˇTWO
16313 three
16314 four
16315 five
16316 "}
16317 .to_string(),
16318 );
16319 cx.update_editor(|editor, window, cx| {
16320 editor.move_up(&Default::default(), window, cx);
16321 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16322 });
16323 cx.assert_state_with_diff(
16324 indoc! { "
16325 one
16326 ˇTWO
16327 three
16328 four
16329 five
16330 "}
16331 .to_string(),
16332 );
16333}
16334
16335#[gpui::test]
16336async fn test_edits_around_expanded_deletion_hunks(
16337 executor: BackgroundExecutor,
16338 cx: &mut TestAppContext,
16339) {
16340 init_test(cx, |_| {});
16341
16342 let mut cx = EditorTestContext::new(cx).await;
16343
16344 let diff_base = r#"
16345 use some::mod1;
16346 use some::mod2;
16347
16348 const A: u32 = 42;
16349 const B: u32 = 42;
16350 const C: u32 = 42;
16351
16352
16353 fn main() {
16354 println!("hello");
16355
16356 println!("world");
16357 }
16358 "#
16359 .unindent();
16360 executor.run_until_parked();
16361 cx.set_state(
16362 &r#"
16363 use some::mod1;
16364 use some::mod2;
16365
16366 ˇconst B: u32 = 42;
16367 const C: u32 = 42;
16368
16369
16370 fn main() {
16371 println!("hello");
16372
16373 println!("world");
16374 }
16375 "#
16376 .unindent(),
16377 );
16378
16379 cx.set_head_text(&diff_base);
16380 executor.run_until_parked();
16381
16382 cx.update_editor(|editor, window, cx| {
16383 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16384 });
16385 executor.run_until_parked();
16386
16387 cx.assert_state_with_diff(
16388 r#"
16389 use some::mod1;
16390 use some::mod2;
16391
16392 - const A: u32 = 42;
16393 ˇconst B: u32 = 42;
16394 const C: u32 = 42;
16395
16396
16397 fn main() {
16398 println!("hello");
16399
16400 println!("world");
16401 }
16402 "#
16403 .unindent(),
16404 );
16405
16406 cx.update_editor(|editor, window, cx| {
16407 editor.delete_line(&DeleteLine, window, cx);
16408 });
16409 executor.run_until_parked();
16410 cx.assert_state_with_diff(
16411 r#"
16412 use some::mod1;
16413 use some::mod2;
16414
16415 - const A: u32 = 42;
16416 - const B: u32 = 42;
16417 ˇconst C: u32 = 42;
16418
16419
16420 fn main() {
16421 println!("hello");
16422
16423 println!("world");
16424 }
16425 "#
16426 .unindent(),
16427 );
16428
16429 cx.update_editor(|editor, window, cx| {
16430 editor.delete_line(&DeleteLine, window, cx);
16431 });
16432 executor.run_until_parked();
16433 cx.assert_state_with_diff(
16434 r#"
16435 use some::mod1;
16436 use some::mod2;
16437
16438 - const A: u32 = 42;
16439 - const B: u32 = 42;
16440 - const C: u32 = 42;
16441 ˇ
16442
16443 fn main() {
16444 println!("hello");
16445
16446 println!("world");
16447 }
16448 "#
16449 .unindent(),
16450 );
16451
16452 cx.update_editor(|editor, window, cx| {
16453 editor.handle_input("replacement", window, cx);
16454 });
16455 executor.run_until_parked();
16456 cx.assert_state_with_diff(
16457 r#"
16458 use some::mod1;
16459 use some::mod2;
16460
16461 - const A: u32 = 42;
16462 - const B: u32 = 42;
16463 - const C: u32 = 42;
16464 -
16465 + replacementˇ
16466
16467 fn main() {
16468 println!("hello");
16469
16470 println!("world");
16471 }
16472 "#
16473 .unindent(),
16474 );
16475}
16476
16477#[gpui::test]
16478async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16479 init_test(cx, |_| {});
16480
16481 let mut cx = EditorTestContext::new(cx).await;
16482
16483 let base_text = r#"
16484 one
16485 two
16486 three
16487 four
16488 five
16489 "#
16490 .unindent();
16491 executor.run_until_parked();
16492 cx.set_state(
16493 &r#"
16494 one
16495 two
16496 fˇour
16497 five
16498 "#
16499 .unindent(),
16500 );
16501
16502 cx.set_head_text(&base_text);
16503 executor.run_until_parked();
16504
16505 cx.update_editor(|editor, window, cx| {
16506 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16507 });
16508 executor.run_until_parked();
16509
16510 cx.assert_state_with_diff(
16511 r#"
16512 one
16513 two
16514 - three
16515 fˇour
16516 five
16517 "#
16518 .unindent(),
16519 );
16520
16521 cx.update_editor(|editor, window, cx| {
16522 editor.backspace(&Backspace, window, cx);
16523 editor.backspace(&Backspace, window, cx);
16524 });
16525 executor.run_until_parked();
16526 cx.assert_state_with_diff(
16527 r#"
16528 one
16529 two
16530 - threeˇ
16531 - four
16532 + our
16533 five
16534 "#
16535 .unindent(),
16536 );
16537}
16538
16539#[gpui::test]
16540async fn test_edit_after_expanded_modification_hunk(
16541 executor: BackgroundExecutor,
16542 cx: &mut TestAppContext,
16543) {
16544 init_test(cx, |_| {});
16545
16546 let mut cx = EditorTestContext::new(cx).await;
16547
16548 let diff_base = r#"
16549 use some::mod1;
16550 use some::mod2;
16551
16552 const A: u32 = 42;
16553 const B: u32 = 42;
16554 const C: u32 = 42;
16555 const D: u32 = 42;
16556
16557
16558 fn main() {
16559 println!("hello");
16560
16561 println!("world");
16562 }"#
16563 .unindent();
16564
16565 cx.set_state(
16566 &r#"
16567 use some::mod1;
16568 use some::mod2;
16569
16570 const A: u32 = 42;
16571 const B: u32 = 42;
16572 const C: u32 = 43ˇ
16573 const D: u32 = 42;
16574
16575
16576 fn main() {
16577 println!("hello");
16578
16579 println!("world");
16580 }"#
16581 .unindent(),
16582 );
16583
16584 cx.set_head_text(&diff_base);
16585 executor.run_until_parked();
16586 cx.update_editor(|editor, window, cx| {
16587 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16588 });
16589 executor.run_until_parked();
16590
16591 cx.assert_state_with_diff(
16592 r#"
16593 use some::mod1;
16594 use some::mod2;
16595
16596 const A: u32 = 42;
16597 const B: u32 = 42;
16598 - const C: u32 = 42;
16599 + const C: u32 = 43ˇ
16600 const D: u32 = 42;
16601
16602
16603 fn main() {
16604 println!("hello");
16605
16606 println!("world");
16607 }"#
16608 .unindent(),
16609 );
16610
16611 cx.update_editor(|editor, window, cx| {
16612 editor.handle_input("\nnew_line\n", window, cx);
16613 });
16614 executor.run_until_parked();
16615
16616 cx.assert_state_with_diff(
16617 r#"
16618 use some::mod1;
16619 use some::mod2;
16620
16621 const A: u32 = 42;
16622 const B: u32 = 42;
16623 - const C: u32 = 42;
16624 + const C: u32 = 43
16625 + new_line
16626 + ˇ
16627 const D: u32 = 42;
16628
16629
16630 fn main() {
16631 println!("hello");
16632
16633 println!("world");
16634 }"#
16635 .unindent(),
16636 );
16637}
16638
16639#[gpui::test]
16640async fn test_stage_and_unstage_added_file_hunk(
16641 executor: BackgroundExecutor,
16642 cx: &mut TestAppContext,
16643) {
16644 init_test(cx, |_| {});
16645
16646 let mut cx = EditorTestContext::new(cx).await;
16647 cx.update_editor(|editor, _, cx| {
16648 editor.set_expand_all_diff_hunks(cx);
16649 });
16650
16651 let working_copy = r#"
16652 ˇfn main() {
16653 println!("hello, world!");
16654 }
16655 "#
16656 .unindent();
16657
16658 cx.set_state(&working_copy);
16659 executor.run_until_parked();
16660
16661 cx.assert_state_with_diff(
16662 r#"
16663 + ˇfn main() {
16664 + println!("hello, world!");
16665 + }
16666 "#
16667 .unindent(),
16668 );
16669 cx.assert_index_text(None);
16670
16671 cx.update_editor(|editor, window, cx| {
16672 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16673 });
16674 executor.run_until_parked();
16675 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16676 cx.assert_state_with_diff(
16677 r#"
16678 + ˇfn main() {
16679 + println!("hello, world!");
16680 + }
16681 "#
16682 .unindent(),
16683 );
16684
16685 cx.update_editor(|editor, window, cx| {
16686 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16687 });
16688 executor.run_until_parked();
16689 cx.assert_index_text(None);
16690}
16691
16692async fn setup_indent_guides_editor(
16693 text: &str,
16694 cx: &mut TestAppContext,
16695) -> (BufferId, EditorTestContext) {
16696 init_test(cx, |_| {});
16697
16698 let mut cx = EditorTestContext::new(cx).await;
16699
16700 let buffer_id = cx.update_editor(|editor, window, cx| {
16701 editor.set_text(text, window, cx);
16702 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16703
16704 buffer_ids[0]
16705 });
16706
16707 (buffer_id, cx)
16708}
16709
16710fn assert_indent_guides(
16711 range: Range<u32>,
16712 expected: Vec<IndentGuide>,
16713 active_indices: Option<Vec<usize>>,
16714 cx: &mut EditorTestContext,
16715) {
16716 let indent_guides = cx.update_editor(|editor, window, cx| {
16717 let snapshot = editor.snapshot(window, cx).display_snapshot;
16718 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16719 editor,
16720 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16721 true,
16722 &snapshot,
16723 cx,
16724 );
16725
16726 indent_guides.sort_by(|a, b| {
16727 a.depth.cmp(&b.depth).then(
16728 a.start_row
16729 .cmp(&b.start_row)
16730 .then(a.end_row.cmp(&b.end_row)),
16731 )
16732 });
16733 indent_guides
16734 });
16735
16736 if let Some(expected) = active_indices {
16737 let active_indices = cx.update_editor(|editor, window, cx| {
16738 let snapshot = editor.snapshot(window, cx).display_snapshot;
16739 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16740 });
16741
16742 assert_eq!(
16743 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16744 expected,
16745 "Active indent guide indices do not match"
16746 );
16747 }
16748
16749 assert_eq!(indent_guides, expected, "Indent guides do not match");
16750}
16751
16752fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16753 IndentGuide {
16754 buffer_id,
16755 start_row: MultiBufferRow(start_row),
16756 end_row: MultiBufferRow(end_row),
16757 depth,
16758 tab_size: 4,
16759 settings: IndentGuideSettings {
16760 enabled: true,
16761 line_width: 1,
16762 active_line_width: 1,
16763 ..Default::default()
16764 },
16765 }
16766}
16767
16768#[gpui::test]
16769async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16770 let (buffer_id, mut cx) = setup_indent_guides_editor(
16771 &"
16772 fn main() {
16773 let a = 1;
16774 }"
16775 .unindent(),
16776 cx,
16777 )
16778 .await;
16779
16780 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16781}
16782
16783#[gpui::test]
16784async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16785 let (buffer_id, mut cx) = setup_indent_guides_editor(
16786 &"
16787 fn main() {
16788 let a = 1;
16789 let b = 2;
16790 }"
16791 .unindent(),
16792 cx,
16793 )
16794 .await;
16795
16796 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16797}
16798
16799#[gpui::test]
16800async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16801 let (buffer_id, mut cx) = setup_indent_guides_editor(
16802 &"
16803 fn main() {
16804 let a = 1;
16805 if a == 3 {
16806 let b = 2;
16807 } else {
16808 let c = 3;
16809 }
16810 }"
16811 .unindent(),
16812 cx,
16813 )
16814 .await;
16815
16816 assert_indent_guides(
16817 0..8,
16818 vec![
16819 indent_guide(buffer_id, 1, 6, 0),
16820 indent_guide(buffer_id, 3, 3, 1),
16821 indent_guide(buffer_id, 5, 5, 1),
16822 ],
16823 None,
16824 &mut cx,
16825 );
16826}
16827
16828#[gpui::test]
16829async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16830 let (buffer_id, mut cx) = setup_indent_guides_editor(
16831 &"
16832 fn main() {
16833 let a = 1;
16834 let b = 2;
16835 let c = 3;
16836 }"
16837 .unindent(),
16838 cx,
16839 )
16840 .await;
16841
16842 assert_indent_guides(
16843 0..5,
16844 vec![
16845 indent_guide(buffer_id, 1, 3, 0),
16846 indent_guide(buffer_id, 2, 2, 1),
16847 ],
16848 None,
16849 &mut cx,
16850 );
16851}
16852
16853#[gpui::test]
16854async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16855 let (buffer_id, mut cx) = setup_indent_guides_editor(
16856 &"
16857 fn main() {
16858 let a = 1;
16859
16860 let c = 3;
16861 }"
16862 .unindent(),
16863 cx,
16864 )
16865 .await;
16866
16867 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16868}
16869
16870#[gpui::test]
16871async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16872 let (buffer_id, mut cx) = setup_indent_guides_editor(
16873 &"
16874 fn main() {
16875 let a = 1;
16876
16877 let c = 3;
16878
16879 if a == 3 {
16880 let b = 2;
16881 } else {
16882 let c = 3;
16883 }
16884 }"
16885 .unindent(),
16886 cx,
16887 )
16888 .await;
16889
16890 assert_indent_guides(
16891 0..11,
16892 vec![
16893 indent_guide(buffer_id, 1, 9, 0),
16894 indent_guide(buffer_id, 6, 6, 1),
16895 indent_guide(buffer_id, 8, 8, 1),
16896 ],
16897 None,
16898 &mut cx,
16899 );
16900}
16901
16902#[gpui::test]
16903async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16904 let (buffer_id, mut cx) = setup_indent_guides_editor(
16905 &"
16906 fn main() {
16907 let a = 1;
16908
16909 let c = 3;
16910
16911 if a == 3 {
16912 let b = 2;
16913 } else {
16914 let c = 3;
16915 }
16916 }"
16917 .unindent(),
16918 cx,
16919 )
16920 .await;
16921
16922 assert_indent_guides(
16923 1..11,
16924 vec![
16925 indent_guide(buffer_id, 1, 9, 0),
16926 indent_guide(buffer_id, 6, 6, 1),
16927 indent_guide(buffer_id, 8, 8, 1),
16928 ],
16929 None,
16930 &mut cx,
16931 );
16932}
16933
16934#[gpui::test]
16935async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16936 let (buffer_id, mut cx) = setup_indent_guides_editor(
16937 &"
16938 fn main() {
16939 let a = 1;
16940
16941 let c = 3;
16942
16943 if a == 3 {
16944 let b = 2;
16945 } else {
16946 let c = 3;
16947 }
16948 }"
16949 .unindent(),
16950 cx,
16951 )
16952 .await;
16953
16954 assert_indent_guides(
16955 1..10,
16956 vec![
16957 indent_guide(buffer_id, 1, 9, 0),
16958 indent_guide(buffer_id, 6, 6, 1),
16959 indent_guide(buffer_id, 8, 8, 1),
16960 ],
16961 None,
16962 &mut cx,
16963 );
16964}
16965
16966#[gpui::test]
16967async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
16968 let (buffer_id, mut cx) = setup_indent_guides_editor(
16969 &"
16970 fn main() {
16971 if a {
16972 b(
16973 c,
16974 d,
16975 )
16976 } else {
16977 e(
16978 f
16979 )
16980 }
16981 }"
16982 .unindent(),
16983 cx,
16984 )
16985 .await;
16986
16987 assert_indent_guides(
16988 0..11,
16989 vec![
16990 indent_guide(buffer_id, 1, 10, 0),
16991 indent_guide(buffer_id, 2, 5, 1),
16992 indent_guide(buffer_id, 7, 9, 1),
16993 indent_guide(buffer_id, 3, 4, 2),
16994 indent_guide(buffer_id, 8, 8, 2),
16995 ],
16996 None,
16997 &mut cx,
16998 );
16999
17000 cx.update_editor(|editor, window, cx| {
17001 editor.fold_at(MultiBufferRow(2), window, cx);
17002 assert_eq!(
17003 editor.display_text(cx),
17004 "
17005 fn main() {
17006 if a {
17007 b(⋯
17008 )
17009 } else {
17010 e(
17011 f
17012 )
17013 }
17014 }"
17015 .unindent()
17016 );
17017 });
17018
17019 assert_indent_guides(
17020 0..11,
17021 vec![
17022 indent_guide(buffer_id, 1, 10, 0),
17023 indent_guide(buffer_id, 2, 5, 1),
17024 indent_guide(buffer_id, 7, 9, 1),
17025 indent_guide(buffer_id, 8, 8, 2),
17026 ],
17027 None,
17028 &mut cx,
17029 );
17030}
17031
17032#[gpui::test]
17033async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
17034 let (buffer_id, mut cx) = setup_indent_guides_editor(
17035 &"
17036 block1
17037 block2
17038 block3
17039 block4
17040 block2
17041 block1
17042 block1"
17043 .unindent(),
17044 cx,
17045 )
17046 .await;
17047
17048 assert_indent_guides(
17049 1..10,
17050 vec![
17051 indent_guide(buffer_id, 1, 4, 0),
17052 indent_guide(buffer_id, 2, 3, 1),
17053 indent_guide(buffer_id, 3, 3, 2),
17054 ],
17055 None,
17056 &mut cx,
17057 );
17058}
17059
17060#[gpui::test]
17061async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
17062 let (buffer_id, mut cx) = setup_indent_guides_editor(
17063 &"
17064 block1
17065 block2
17066 block3
17067
17068 block1
17069 block1"
17070 .unindent(),
17071 cx,
17072 )
17073 .await;
17074
17075 assert_indent_guides(
17076 0..6,
17077 vec![
17078 indent_guide(buffer_id, 1, 2, 0),
17079 indent_guide(buffer_id, 2, 2, 1),
17080 ],
17081 None,
17082 &mut cx,
17083 );
17084}
17085
17086#[gpui::test]
17087async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17088 let (buffer_id, mut cx) = setup_indent_guides_editor(
17089 &"
17090 block1
17091
17092
17093
17094 block2
17095 "
17096 .unindent(),
17097 cx,
17098 )
17099 .await;
17100
17101 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17102}
17103
17104#[gpui::test]
17105async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17106 let (buffer_id, mut cx) = setup_indent_guides_editor(
17107 &"
17108 def a:
17109 \tb = 3
17110 \tif True:
17111 \t\tc = 4
17112 \t\td = 5
17113 \tprint(b)
17114 "
17115 .unindent(),
17116 cx,
17117 )
17118 .await;
17119
17120 assert_indent_guides(
17121 0..6,
17122 vec![
17123 indent_guide(buffer_id, 1, 5, 0),
17124 indent_guide(buffer_id, 3, 4, 1),
17125 ],
17126 None,
17127 &mut cx,
17128 );
17129}
17130
17131#[gpui::test]
17132async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17133 let (buffer_id, mut cx) = setup_indent_guides_editor(
17134 &"
17135 fn main() {
17136 let a = 1;
17137 }"
17138 .unindent(),
17139 cx,
17140 )
17141 .await;
17142
17143 cx.update_editor(|editor, window, cx| {
17144 editor.change_selections(None, window, cx, |s| {
17145 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17146 });
17147 });
17148
17149 assert_indent_guides(
17150 0..3,
17151 vec![indent_guide(buffer_id, 1, 1, 0)],
17152 Some(vec![0]),
17153 &mut cx,
17154 );
17155}
17156
17157#[gpui::test]
17158async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17159 let (buffer_id, mut cx) = setup_indent_guides_editor(
17160 &"
17161 fn main() {
17162 if 1 == 2 {
17163 let a = 1;
17164 }
17165 }"
17166 .unindent(),
17167 cx,
17168 )
17169 .await;
17170
17171 cx.update_editor(|editor, window, cx| {
17172 editor.change_selections(None, window, cx, |s| {
17173 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17174 });
17175 });
17176
17177 assert_indent_guides(
17178 0..4,
17179 vec![
17180 indent_guide(buffer_id, 1, 3, 0),
17181 indent_guide(buffer_id, 2, 2, 1),
17182 ],
17183 Some(vec![1]),
17184 &mut cx,
17185 );
17186
17187 cx.update_editor(|editor, window, cx| {
17188 editor.change_selections(None, window, cx, |s| {
17189 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17190 });
17191 });
17192
17193 assert_indent_guides(
17194 0..4,
17195 vec![
17196 indent_guide(buffer_id, 1, 3, 0),
17197 indent_guide(buffer_id, 2, 2, 1),
17198 ],
17199 Some(vec![1]),
17200 &mut cx,
17201 );
17202
17203 cx.update_editor(|editor, window, cx| {
17204 editor.change_selections(None, window, cx, |s| {
17205 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17206 });
17207 });
17208
17209 assert_indent_guides(
17210 0..4,
17211 vec![
17212 indent_guide(buffer_id, 1, 3, 0),
17213 indent_guide(buffer_id, 2, 2, 1),
17214 ],
17215 Some(vec![0]),
17216 &mut cx,
17217 );
17218}
17219
17220#[gpui::test]
17221async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17222 let (buffer_id, mut cx) = setup_indent_guides_editor(
17223 &"
17224 fn main() {
17225 let a = 1;
17226
17227 let b = 2;
17228 }"
17229 .unindent(),
17230 cx,
17231 )
17232 .await;
17233
17234 cx.update_editor(|editor, window, cx| {
17235 editor.change_selections(None, window, cx, |s| {
17236 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17237 });
17238 });
17239
17240 assert_indent_guides(
17241 0..5,
17242 vec![indent_guide(buffer_id, 1, 3, 0)],
17243 Some(vec![0]),
17244 &mut cx,
17245 );
17246}
17247
17248#[gpui::test]
17249async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17250 let (buffer_id, mut cx) = setup_indent_guides_editor(
17251 &"
17252 def m:
17253 a = 1
17254 pass"
17255 .unindent(),
17256 cx,
17257 )
17258 .await;
17259
17260 cx.update_editor(|editor, window, cx| {
17261 editor.change_selections(None, window, cx, |s| {
17262 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17263 });
17264 });
17265
17266 assert_indent_guides(
17267 0..3,
17268 vec![indent_guide(buffer_id, 1, 2, 0)],
17269 Some(vec![0]),
17270 &mut cx,
17271 );
17272}
17273
17274#[gpui::test]
17275async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17276 init_test(cx, |_| {});
17277 let mut cx = EditorTestContext::new(cx).await;
17278 let text = indoc! {
17279 "
17280 impl A {
17281 fn b() {
17282 0;
17283 3;
17284 5;
17285 6;
17286 7;
17287 }
17288 }
17289 "
17290 };
17291 let base_text = indoc! {
17292 "
17293 impl A {
17294 fn b() {
17295 0;
17296 1;
17297 2;
17298 3;
17299 4;
17300 }
17301 fn c() {
17302 5;
17303 6;
17304 7;
17305 }
17306 }
17307 "
17308 };
17309
17310 cx.update_editor(|editor, window, cx| {
17311 editor.set_text(text, window, cx);
17312
17313 editor.buffer().update(cx, |multibuffer, cx| {
17314 let buffer = multibuffer.as_singleton().unwrap();
17315 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17316
17317 multibuffer.set_all_diff_hunks_expanded(cx);
17318 multibuffer.add_diff(diff, cx);
17319
17320 buffer.read(cx).remote_id()
17321 })
17322 });
17323 cx.run_until_parked();
17324
17325 cx.assert_state_with_diff(
17326 indoc! { "
17327 impl A {
17328 fn b() {
17329 0;
17330 - 1;
17331 - 2;
17332 3;
17333 - 4;
17334 - }
17335 - fn c() {
17336 5;
17337 6;
17338 7;
17339 }
17340 }
17341 ˇ"
17342 }
17343 .to_string(),
17344 );
17345
17346 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17347 editor
17348 .snapshot(window, cx)
17349 .buffer_snapshot
17350 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17351 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17352 .collect::<Vec<_>>()
17353 });
17354 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17355 assert_eq!(
17356 actual_guides,
17357 vec![
17358 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17359 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17360 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17361 ]
17362 );
17363}
17364
17365#[gpui::test]
17366async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17367 init_test(cx, |_| {});
17368 let mut cx = EditorTestContext::new(cx).await;
17369
17370 let diff_base = r#"
17371 a
17372 b
17373 c
17374 "#
17375 .unindent();
17376
17377 cx.set_state(
17378 &r#"
17379 ˇA
17380 b
17381 C
17382 "#
17383 .unindent(),
17384 );
17385 cx.set_head_text(&diff_base);
17386 cx.update_editor(|editor, window, cx| {
17387 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17388 });
17389 executor.run_until_parked();
17390
17391 let both_hunks_expanded = r#"
17392 - a
17393 + ˇA
17394 b
17395 - c
17396 + C
17397 "#
17398 .unindent();
17399
17400 cx.assert_state_with_diff(both_hunks_expanded.clone());
17401
17402 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17403 let snapshot = editor.snapshot(window, cx);
17404 let hunks = editor
17405 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17406 .collect::<Vec<_>>();
17407 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17408 let buffer_id = hunks[0].buffer_id;
17409 hunks
17410 .into_iter()
17411 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17412 .collect::<Vec<_>>()
17413 });
17414 assert_eq!(hunk_ranges.len(), 2);
17415
17416 cx.update_editor(|editor, _, cx| {
17417 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17418 });
17419 executor.run_until_parked();
17420
17421 let second_hunk_expanded = r#"
17422 ˇA
17423 b
17424 - c
17425 + C
17426 "#
17427 .unindent();
17428
17429 cx.assert_state_with_diff(second_hunk_expanded);
17430
17431 cx.update_editor(|editor, _, cx| {
17432 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17433 });
17434 executor.run_until_parked();
17435
17436 cx.assert_state_with_diff(both_hunks_expanded.clone());
17437
17438 cx.update_editor(|editor, _, cx| {
17439 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17440 });
17441 executor.run_until_parked();
17442
17443 let first_hunk_expanded = r#"
17444 - a
17445 + ˇA
17446 b
17447 C
17448 "#
17449 .unindent();
17450
17451 cx.assert_state_with_diff(first_hunk_expanded);
17452
17453 cx.update_editor(|editor, _, cx| {
17454 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17455 });
17456 executor.run_until_parked();
17457
17458 cx.assert_state_with_diff(both_hunks_expanded);
17459
17460 cx.set_state(
17461 &r#"
17462 ˇA
17463 b
17464 "#
17465 .unindent(),
17466 );
17467 cx.run_until_parked();
17468
17469 // TODO this cursor position seems bad
17470 cx.assert_state_with_diff(
17471 r#"
17472 - ˇa
17473 + A
17474 b
17475 "#
17476 .unindent(),
17477 );
17478
17479 cx.update_editor(|editor, window, cx| {
17480 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17481 });
17482
17483 cx.assert_state_with_diff(
17484 r#"
17485 - ˇa
17486 + A
17487 b
17488 - c
17489 "#
17490 .unindent(),
17491 );
17492
17493 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17494 let snapshot = editor.snapshot(window, cx);
17495 let hunks = editor
17496 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17497 .collect::<Vec<_>>();
17498 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17499 let buffer_id = hunks[0].buffer_id;
17500 hunks
17501 .into_iter()
17502 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17503 .collect::<Vec<_>>()
17504 });
17505 assert_eq!(hunk_ranges.len(), 2);
17506
17507 cx.update_editor(|editor, _, cx| {
17508 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17509 });
17510 executor.run_until_parked();
17511
17512 cx.assert_state_with_diff(
17513 r#"
17514 - ˇa
17515 + A
17516 b
17517 "#
17518 .unindent(),
17519 );
17520}
17521
17522#[gpui::test]
17523async fn test_toggle_deletion_hunk_at_start_of_file(
17524 executor: BackgroundExecutor,
17525 cx: &mut TestAppContext,
17526) {
17527 init_test(cx, |_| {});
17528 let mut cx = EditorTestContext::new(cx).await;
17529
17530 let diff_base = r#"
17531 a
17532 b
17533 c
17534 "#
17535 .unindent();
17536
17537 cx.set_state(
17538 &r#"
17539 ˇb
17540 c
17541 "#
17542 .unindent(),
17543 );
17544 cx.set_head_text(&diff_base);
17545 cx.update_editor(|editor, window, cx| {
17546 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17547 });
17548 executor.run_until_parked();
17549
17550 let hunk_expanded = r#"
17551 - a
17552 ˇb
17553 c
17554 "#
17555 .unindent();
17556
17557 cx.assert_state_with_diff(hunk_expanded.clone());
17558
17559 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17560 let snapshot = editor.snapshot(window, cx);
17561 let hunks = editor
17562 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17563 .collect::<Vec<_>>();
17564 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17565 let buffer_id = hunks[0].buffer_id;
17566 hunks
17567 .into_iter()
17568 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17569 .collect::<Vec<_>>()
17570 });
17571 assert_eq!(hunk_ranges.len(), 1);
17572
17573 cx.update_editor(|editor, _, cx| {
17574 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17575 });
17576 executor.run_until_parked();
17577
17578 let hunk_collapsed = r#"
17579 ˇb
17580 c
17581 "#
17582 .unindent();
17583
17584 cx.assert_state_with_diff(hunk_collapsed);
17585
17586 cx.update_editor(|editor, _, cx| {
17587 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17588 });
17589 executor.run_until_parked();
17590
17591 cx.assert_state_with_diff(hunk_expanded.clone());
17592}
17593
17594#[gpui::test]
17595async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17596 init_test(cx, |_| {});
17597
17598 let fs = FakeFs::new(cx.executor());
17599 fs.insert_tree(
17600 path!("/test"),
17601 json!({
17602 ".git": {},
17603 "file-1": "ONE\n",
17604 "file-2": "TWO\n",
17605 "file-3": "THREE\n",
17606 }),
17607 )
17608 .await;
17609
17610 fs.set_head_for_repo(
17611 path!("/test/.git").as_ref(),
17612 &[
17613 ("file-1".into(), "one\n".into()),
17614 ("file-2".into(), "two\n".into()),
17615 ("file-3".into(), "three\n".into()),
17616 ],
17617 );
17618
17619 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17620 let mut buffers = vec![];
17621 for i in 1..=3 {
17622 let buffer = project
17623 .update(cx, |project, cx| {
17624 let path = format!(path!("/test/file-{}"), i);
17625 project.open_local_buffer(path, cx)
17626 })
17627 .await
17628 .unwrap();
17629 buffers.push(buffer);
17630 }
17631
17632 let multibuffer = cx.new(|cx| {
17633 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17634 multibuffer.set_all_diff_hunks_expanded(cx);
17635 for buffer in &buffers {
17636 let snapshot = buffer.read(cx).snapshot();
17637 multibuffer.set_excerpts_for_path(
17638 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17639 buffer.clone(),
17640 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17641 DEFAULT_MULTIBUFFER_CONTEXT,
17642 cx,
17643 );
17644 }
17645 multibuffer
17646 });
17647
17648 let editor = cx.add_window(|window, cx| {
17649 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17650 });
17651 cx.run_until_parked();
17652
17653 let snapshot = editor
17654 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17655 .unwrap();
17656 let hunks = snapshot
17657 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17658 .map(|hunk| match hunk {
17659 DisplayDiffHunk::Unfolded {
17660 display_row_range, ..
17661 } => display_row_range,
17662 DisplayDiffHunk::Folded { .. } => unreachable!(),
17663 })
17664 .collect::<Vec<_>>();
17665 assert_eq!(
17666 hunks,
17667 [
17668 DisplayRow(2)..DisplayRow(4),
17669 DisplayRow(7)..DisplayRow(9),
17670 DisplayRow(12)..DisplayRow(14),
17671 ]
17672 );
17673}
17674
17675#[gpui::test]
17676async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17677 init_test(cx, |_| {});
17678
17679 let mut cx = EditorTestContext::new(cx).await;
17680 cx.set_head_text(indoc! { "
17681 one
17682 two
17683 three
17684 four
17685 five
17686 "
17687 });
17688 cx.set_index_text(indoc! { "
17689 one
17690 two
17691 three
17692 four
17693 five
17694 "
17695 });
17696 cx.set_state(indoc! {"
17697 one
17698 TWO
17699 ˇTHREE
17700 FOUR
17701 five
17702 "});
17703 cx.run_until_parked();
17704 cx.update_editor(|editor, window, cx| {
17705 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17706 });
17707 cx.run_until_parked();
17708 cx.assert_index_text(Some(indoc! {"
17709 one
17710 TWO
17711 THREE
17712 FOUR
17713 five
17714 "}));
17715 cx.set_state(indoc! { "
17716 one
17717 TWO
17718 ˇTHREE-HUNDRED
17719 FOUR
17720 five
17721 "});
17722 cx.run_until_parked();
17723 cx.update_editor(|editor, window, cx| {
17724 let snapshot = editor.snapshot(window, cx);
17725 let hunks = editor
17726 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17727 .collect::<Vec<_>>();
17728 assert_eq!(hunks.len(), 1);
17729 assert_eq!(
17730 hunks[0].status(),
17731 DiffHunkStatus {
17732 kind: DiffHunkStatusKind::Modified,
17733 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17734 }
17735 );
17736
17737 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17738 });
17739 cx.run_until_parked();
17740 cx.assert_index_text(Some(indoc! {"
17741 one
17742 TWO
17743 THREE-HUNDRED
17744 FOUR
17745 five
17746 "}));
17747}
17748
17749#[gpui::test]
17750fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17751 init_test(cx, |_| {});
17752
17753 let editor = cx.add_window(|window, cx| {
17754 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17755 build_editor(buffer, window, cx)
17756 });
17757
17758 let render_args = Arc::new(Mutex::new(None));
17759 let snapshot = editor
17760 .update(cx, |editor, window, cx| {
17761 let snapshot = editor.buffer().read(cx).snapshot(cx);
17762 let range =
17763 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17764
17765 struct RenderArgs {
17766 row: MultiBufferRow,
17767 folded: bool,
17768 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17769 }
17770
17771 let crease = Crease::inline(
17772 range,
17773 FoldPlaceholder::test(),
17774 {
17775 let toggle_callback = render_args.clone();
17776 move |row, folded, callback, _window, _cx| {
17777 *toggle_callback.lock() = Some(RenderArgs {
17778 row,
17779 folded,
17780 callback,
17781 });
17782 div()
17783 }
17784 },
17785 |_row, _folded, _window, _cx| div(),
17786 );
17787
17788 editor.insert_creases(Some(crease), cx);
17789 let snapshot = editor.snapshot(window, cx);
17790 let _div = snapshot.render_crease_toggle(
17791 MultiBufferRow(1),
17792 false,
17793 cx.entity().clone(),
17794 window,
17795 cx,
17796 );
17797 snapshot
17798 })
17799 .unwrap();
17800
17801 let render_args = render_args.lock().take().unwrap();
17802 assert_eq!(render_args.row, MultiBufferRow(1));
17803 assert!(!render_args.folded);
17804 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17805
17806 cx.update_window(*editor, |_, window, cx| {
17807 (render_args.callback)(true, window, cx)
17808 })
17809 .unwrap();
17810 let snapshot = editor
17811 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17812 .unwrap();
17813 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17814
17815 cx.update_window(*editor, |_, window, cx| {
17816 (render_args.callback)(false, window, cx)
17817 })
17818 .unwrap();
17819 let snapshot = editor
17820 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17821 .unwrap();
17822 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17823}
17824
17825#[gpui::test]
17826async fn test_input_text(cx: &mut TestAppContext) {
17827 init_test(cx, |_| {});
17828 let mut cx = EditorTestContext::new(cx).await;
17829
17830 cx.set_state(
17831 &r#"ˇone
17832 two
17833
17834 three
17835 fourˇ
17836 five
17837
17838 siˇx"#
17839 .unindent(),
17840 );
17841
17842 cx.dispatch_action(HandleInput(String::new()));
17843 cx.assert_editor_state(
17844 &r#"ˇone
17845 two
17846
17847 three
17848 fourˇ
17849 five
17850
17851 siˇx"#
17852 .unindent(),
17853 );
17854
17855 cx.dispatch_action(HandleInput("AAAA".to_string()));
17856 cx.assert_editor_state(
17857 &r#"AAAAˇone
17858 two
17859
17860 three
17861 fourAAAAˇ
17862 five
17863
17864 siAAAAˇx"#
17865 .unindent(),
17866 );
17867}
17868
17869#[gpui::test]
17870async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17871 init_test(cx, |_| {});
17872
17873 let mut cx = EditorTestContext::new(cx).await;
17874 cx.set_state(
17875 r#"let foo = 1;
17876let foo = 2;
17877let foo = 3;
17878let fooˇ = 4;
17879let foo = 5;
17880let foo = 6;
17881let foo = 7;
17882let foo = 8;
17883let foo = 9;
17884let foo = 10;
17885let foo = 11;
17886let foo = 12;
17887let foo = 13;
17888let foo = 14;
17889let foo = 15;"#,
17890 );
17891
17892 cx.update_editor(|e, window, cx| {
17893 assert_eq!(
17894 e.next_scroll_position,
17895 NextScrollCursorCenterTopBottom::Center,
17896 "Default next scroll direction is center",
17897 );
17898
17899 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17900 assert_eq!(
17901 e.next_scroll_position,
17902 NextScrollCursorCenterTopBottom::Top,
17903 "After center, next scroll direction should be top",
17904 );
17905
17906 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17907 assert_eq!(
17908 e.next_scroll_position,
17909 NextScrollCursorCenterTopBottom::Bottom,
17910 "After top, next scroll direction should be bottom",
17911 );
17912
17913 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17914 assert_eq!(
17915 e.next_scroll_position,
17916 NextScrollCursorCenterTopBottom::Center,
17917 "After bottom, scrolling should start over",
17918 );
17919
17920 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17921 assert_eq!(
17922 e.next_scroll_position,
17923 NextScrollCursorCenterTopBottom::Top,
17924 "Scrolling continues if retriggered fast enough"
17925 );
17926 });
17927
17928 cx.executor()
17929 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17930 cx.executor().run_until_parked();
17931 cx.update_editor(|e, _, _| {
17932 assert_eq!(
17933 e.next_scroll_position,
17934 NextScrollCursorCenterTopBottom::Center,
17935 "If scrolling is not triggered fast enough, it should reset"
17936 );
17937 });
17938}
17939
17940#[gpui::test]
17941async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17942 init_test(cx, |_| {});
17943 let mut cx = EditorLspTestContext::new_rust(
17944 lsp::ServerCapabilities {
17945 definition_provider: Some(lsp::OneOf::Left(true)),
17946 references_provider: Some(lsp::OneOf::Left(true)),
17947 ..lsp::ServerCapabilities::default()
17948 },
17949 cx,
17950 )
17951 .await;
17952
17953 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17954 let go_to_definition = cx
17955 .lsp
17956 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17957 move |params, _| async move {
17958 if empty_go_to_definition {
17959 Ok(None)
17960 } else {
17961 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17962 uri: params.text_document_position_params.text_document.uri,
17963 range: lsp::Range::new(
17964 lsp::Position::new(4, 3),
17965 lsp::Position::new(4, 6),
17966 ),
17967 })))
17968 }
17969 },
17970 );
17971 let references = cx
17972 .lsp
17973 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17974 Ok(Some(vec![lsp::Location {
17975 uri: params.text_document_position.text_document.uri,
17976 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17977 }]))
17978 });
17979 (go_to_definition, references)
17980 };
17981
17982 cx.set_state(
17983 &r#"fn one() {
17984 let mut a = ˇtwo();
17985 }
17986
17987 fn two() {}"#
17988 .unindent(),
17989 );
17990 set_up_lsp_handlers(false, &mut cx);
17991 let navigated = cx
17992 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17993 .await
17994 .expect("Failed to navigate to definition");
17995 assert_eq!(
17996 navigated,
17997 Navigated::Yes,
17998 "Should have navigated to definition from the GetDefinition response"
17999 );
18000 cx.assert_editor_state(
18001 &r#"fn one() {
18002 let mut a = two();
18003 }
18004
18005 fn «twoˇ»() {}"#
18006 .unindent(),
18007 );
18008
18009 let editors = cx.update_workspace(|workspace, _, cx| {
18010 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18011 });
18012 cx.update_editor(|_, _, test_editor_cx| {
18013 assert_eq!(
18014 editors.len(),
18015 1,
18016 "Initially, only one, test, editor should be open in the workspace"
18017 );
18018 assert_eq!(
18019 test_editor_cx.entity(),
18020 editors.last().expect("Asserted len is 1").clone()
18021 );
18022 });
18023
18024 set_up_lsp_handlers(true, &mut cx);
18025 let navigated = cx
18026 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18027 .await
18028 .expect("Failed to navigate to lookup references");
18029 assert_eq!(
18030 navigated,
18031 Navigated::Yes,
18032 "Should have navigated to references as a fallback after empty GoToDefinition response"
18033 );
18034 // We should not change the selections in the existing file,
18035 // if opening another milti buffer with the references
18036 cx.assert_editor_state(
18037 &r#"fn one() {
18038 let mut a = two();
18039 }
18040
18041 fn «twoˇ»() {}"#
18042 .unindent(),
18043 );
18044 let editors = cx.update_workspace(|workspace, _, cx| {
18045 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18046 });
18047 cx.update_editor(|_, _, test_editor_cx| {
18048 assert_eq!(
18049 editors.len(),
18050 2,
18051 "After falling back to references search, we open a new editor with the results"
18052 );
18053 let references_fallback_text = editors
18054 .into_iter()
18055 .find(|new_editor| *new_editor != test_editor_cx.entity())
18056 .expect("Should have one non-test editor now")
18057 .read(test_editor_cx)
18058 .text(test_editor_cx);
18059 assert_eq!(
18060 references_fallback_text, "fn one() {\n let mut a = two();\n}",
18061 "Should use the range from the references response and not the GoToDefinition one"
18062 );
18063 });
18064}
18065
18066#[gpui::test]
18067async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
18068 init_test(cx, |_| {});
18069 cx.update(|cx| {
18070 let mut editor_settings = EditorSettings::get_global(cx).clone();
18071 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
18072 EditorSettings::override_global(editor_settings, cx);
18073 });
18074 let mut cx = EditorLspTestContext::new_rust(
18075 lsp::ServerCapabilities {
18076 definition_provider: Some(lsp::OneOf::Left(true)),
18077 references_provider: Some(lsp::OneOf::Left(true)),
18078 ..lsp::ServerCapabilities::default()
18079 },
18080 cx,
18081 )
18082 .await;
18083 let original_state = r#"fn one() {
18084 let mut a = ˇtwo();
18085 }
18086
18087 fn two() {}"#
18088 .unindent();
18089 cx.set_state(&original_state);
18090
18091 let mut go_to_definition = cx
18092 .lsp
18093 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18094 move |_, _| async move { Ok(None) },
18095 );
18096 let _references = cx
18097 .lsp
18098 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18099 panic!("Should not call for references with no go to definition fallback")
18100 });
18101
18102 let navigated = cx
18103 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18104 .await
18105 .expect("Failed to navigate to lookup references");
18106 go_to_definition
18107 .next()
18108 .await
18109 .expect("Should have called the go_to_definition handler");
18110
18111 assert_eq!(
18112 navigated,
18113 Navigated::No,
18114 "Should have navigated to references as a fallback after empty GoToDefinition response"
18115 );
18116 cx.assert_editor_state(&original_state);
18117 let editors = cx.update_workspace(|workspace, _, cx| {
18118 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18119 });
18120 cx.update_editor(|_, _, _| {
18121 assert_eq!(
18122 editors.len(),
18123 1,
18124 "After unsuccessful fallback, no other editor should have been opened"
18125 );
18126 });
18127}
18128
18129#[gpui::test]
18130async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18131 init_test(cx, |_| {});
18132
18133 let language = Arc::new(Language::new(
18134 LanguageConfig::default(),
18135 Some(tree_sitter_rust::LANGUAGE.into()),
18136 ));
18137
18138 let text = r#"
18139 #[cfg(test)]
18140 mod tests() {
18141 #[test]
18142 fn runnable_1() {
18143 let a = 1;
18144 }
18145
18146 #[test]
18147 fn runnable_2() {
18148 let a = 1;
18149 let b = 2;
18150 }
18151 }
18152 "#
18153 .unindent();
18154
18155 let fs = FakeFs::new(cx.executor());
18156 fs.insert_file("/file.rs", Default::default()).await;
18157
18158 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18159 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18160 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18161 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18162 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18163
18164 let editor = cx.new_window_entity(|window, cx| {
18165 Editor::new(
18166 EditorMode::full(),
18167 multi_buffer,
18168 Some(project.clone()),
18169 window,
18170 cx,
18171 )
18172 });
18173
18174 editor.update_in(cx, |editor, window, cx| {
18175 let snapshot = editor.buffer().read(cx).snapshot(cx);
18176 editor.tasks.insert(
18177 (buffer.read(cx).remote_id(), 3),
18178 RunnableTasks {
18179 templates: vec![],
18180 offset: snapshot.anchor_before(43),
18181 column: 0,
18182 extra_variables: HashMap::default(),
18183 context_range: BufferOffset(43)..BufferOffset(85),
18184 },
18185 );
18186 editor.tasks.insert(
18187 (buffer.read(cx).remote_id(), 8),
18188 RunnableTasks {
18189 templates: vec![],
18190 offset: snapshot.anchor_before(86),
18191 column: 0,
18192 extra_variables: HashMap::default(),
18193 context_range: BufferOffset(86)..BufferOffset(191),
18194 },
18195 );
18196
18197 // Test finding task when cursor is inside function body
18198 editor.change_selections(None, window, cx, |s| {
18199 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18200 });
18201 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18202 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18203
18204 // Test finding task when cursor is on function name
18205 editor.change_selections(None, window, cx, |s| {
18206 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18207 });
18208 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18209 assert_eq!(row, 8, "Should find task when cursor is on function name");
18210 });
18211}
18212
18213#[gpui::test]
18214async fn test_folding_buffers(cx: &mut TestAppContext) {
18215 init_test(cx, |_| {});
18216
18217 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18218 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18219 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18220
18221 let fs = FakeFs::new(cx.executor());
18222 fs.insert_tree(
18223 path!("/a"),
18224 json!({
18225 "first.rs": sample_text_1,
18226 "second.rs": sample_text_2,
18227 "third.rs": sample_text_3,
18228 }),
18229 )
18230 .await;
18231 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18232 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18233 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18234 let worktree = project.update(cx, |project, cx| {
18235 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18236 assert_eq!(worktrees.len(), 1);
18237 worktrees.pop().unwrap()
18238 });
18239 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18240
18241 let buffer_1 = project
18242 .update(cx, |project, cx| {
18243 project.open_buffer((worktree_id, "first.rs"), cx)
18244 })
18245 .await
18246 .unwrap();
18247 let buffer_2 = project
18248 .update(cx, |project, cx| {
18249 project.open_buffer((worktree_id, "second.rs"), cx)
18250 })
18251 .await
18252 .unwrap();
18253 let buffer_3 = project
18254 .update(cx, |project, cx| {
18255 project.open_buffer((worktree_id, "third.rs"), cx)
18256 })
18257 .await
18258 .unwrap();
18259
18260 let multi_buffer = cx.new(|cx| {
18261 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18262 multi_buffer.push_excerpts(
18263 buffer_1.clone(),
18264 [
18265 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18266 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18267 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18268 ],
18269 cx,
18270 );
18271 multi_buffer.push_excerpts(
18272 buffer_2.clone(),
18273 [
18274 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18275 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18276 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18277 ],
18278 cx,
18279 );
18280 multi_buffer.push_excerpts(
18281 buffer_3.clone(),
18282 [
18283 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18284 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18285 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18286 ],
18287 cx,
18288 );
18289 multi_buffer
18290 });
18291 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18292 Editor::new(
18293 EditorMode::full(),
18294 multi_buffer.clone(),
18295 Some(project.clone()),
18296 window,
18297 cx,
18298 )
18299 });
18300
18301 assert_eq!(
18302 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18303 "\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",
18304 );
18305
18306 multi_buffer_editor.update(cx, |editor, cx| {
18307 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18308 });
18309 assert_eq!(
18310 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18311 "\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",
18312 "After folding the first buffer, its text should not be displayed"
18313 );
18314
18315 multi_buffer_editor.update(cx, |editor, cx| {
18316 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18317 });
18318 assert_eq!(
18319 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18320 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18321 "After folding the second buffer, its text should not be displayed"
18322 );
18323
18324 multi_buffer_editor.update(cx, |editor, cx| {
18325 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18326 });
18327 assert_eq!(
18328 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18329 "\n\n\n\n\n",
18330 "After folding the third buffer, its text should not be displayed"
18331 );
18332
18333 // Emulate selection inside the fold logic, that should work
18334 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18335 editor
18336 .snapshot(window, cx)
18337 .next_line_boundary(Point::new(0, 4));
18338 });
18339
18340 multi_buffer_editor.update(cx, |editor, cx| {
18341 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18342 });
18343 assert_eq!(
18344 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18345 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18346 "After unfolding the second buffer, its text should be displayed"
18347 );
18348
18349 // Typing inside of buffer 1 causes that buffer to be unfolded.
18350 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18351 assert_eq!(
18352 multi_buffer
18353 .read(cx)
18354 .snapshot(cx)
18355 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18356 .collect::<String>(),
18357 "bbbb"
18358 );
18359 editor.change_selections(None, window, cx, |selections| {
18360 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18361 });
18362 editor.handle_input("B", window, cx);
18363 });
18364
18365 assert_eq!(
18366 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18367 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18368 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18369 );
18370
18371 multi_buffer_editor.update(cx, |editor, cx| {
18372 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18373 });
18374 assert_eq!(
18375 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18376 "\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",
18377 "After unfolding the all buffers, all original text should be displayed"
18378 );
18379}
18380
18381#[gpui::test]
18382async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18383 init_test(cx, |_| {});
18384
18385 let sample_text_1 = "1111\n2222\n3333".to_string();
18386 let sample_text_2 = "4444\n5555\n6666".to_string();
18387 let sample_text_3 = "7777\n8888\n9999".to_string();
18388
18389 let fs = FakeFs::new(cx.executor());
18390 fs.insert_tree(
18391 path!("/a"),
18392 json!({
18393 "first.rs": sample_text_1,
18394 "second.rs": sample_text_2,
18395 "third.rs": sample_text_3,
18396 }),
18397 )
18398 .await;
18399 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18400 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18401 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18402 let worktree = project.update(cx, |project, cx| {
18403 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18404 assert_eq!(worktrees.len(), 1);
18405 worktrees.pop().unwrap()
18406 });
18407 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18408
18409 let buffer_1 = project
18410 .update(cx, |project, cx| {
18411 project.open_buffer((worktree_id, "first.rs"), cx)
18412 })
18413 .await
18414 .unwrap();
18415 let buffer_2 = project
18416 .update(cx, |project, cx| {
18417 project.open_buffer((worktree_id, "second.rs"), cx)
18418 })
18419 .await
18420 .unwrap();
18421 let buffer_3 = project
18422 .update(cx, |project, cx| {
18423 project.open_buffer((worktree_id, "third.rs"), cx)
18424 })
18425 .await
18426 .unwrap();
18427
18428 let multi_buffer = cx.new(|cx| {
18429 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18430 multi_buffer.push_excerpts(
18431 buffer_1.clone(),
18432 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18433 cx,
18434 );
18435 multi_buffer.push_excerpts(
18436 buffer_2.clone(),
18437 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18438 cx,
18439 );
18440 multi_buffer.push_excerpts(
18441 buffer_3.clone(),
18442 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18443 cx,
18444 );
18445 multi_buffer
18446 });
18447
18448 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18449 Editor::new(
18450 EditorMode::full(),
18451 multi_buffer,
18452 Some(project.clone()),
18453 window,
18454 cx,
18455 )
18456 });
18457
18458 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18459 assert_eq!(
18460 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18461 full_text,
18462 );
18463
18464 multi_buffer_editor.update(cx, |editor, cx| {
18465 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18466 });
18467 assert_eq!(
18468 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18469 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18470 "After folding the first buffer, its text should not be displayed"
18471 );
18472
18473 multi_buffer_editor.update(cx, |editor, cx| {
18474 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18475 });
18476
18477 assert_eq!(
18478 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18479 "\n\n\n\n\n\n7777\n8888\n9999",
18480 "After folding the second buffer, its text should not be displayed"
18481 );
18482
18483 multi_buffer_editor.update(cx, |editor, cx| {
18484 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18485 });
18486 assert_eq!(
18487 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18488 "\n\n\n\n\n",
18489 "After folding the third buffer, its text should not be displayed"
18490 );
18491
18492 multi_buffer_editor.update(cx, |editor, cx| {
18493 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18494 });
18495 assert_eq!(
18496 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18497 "\n\n\n\n4444\n5555\n6666\n\n",
18498 "After unfolding the second buffer, its text should be displayed"
18499 );
18500
18501 multi_buffer_editor.update(cx, |editor, cx| {
18502 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18503 });
18504 assert_eq!(
18505 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18506 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18507 "After unfolding the first buffer, its text should be displayed"
18508 );
18509
18510 multi_buffer_editor.update(cx, |editor, cx| {
18511 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18512 });
18513 assert_eq!(
18514 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18515 full_text,
18516 "After unfolding all buffers, all original text should be displayed"
18517 );
18518}
18519
18520#[gpui::test]
18521async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18522 init_test(cx, |_| {});
18523
18524 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18525
18526 let fs = FakeFs::new(cx.executor());
18527 fs.insert_tree(
18528 path!("/a"),
18529 json!({
18530 "main.rs": sample_text,
18531 }),
18532 )
18533 .await;
18534 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18535 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18536 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18537 let worktree = project.update(cx, |project, cx| {
18538 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18539 assert_eq!(worktrees.len(), 1);
18540 worktrees.pop().unwrap()
18541 });
18542 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18543
18544 let buffer_1 = project
18545 .update(cx, |project, cx| {
18546 project.open_buffer((worktree_id, "main.rs"), cx)
18547 })
18548 .await
18549 .unwrap();
18550
18551 let multi_buffer = cx.new(|cx| {
18552 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18553 multi_buffer.push_excerpts(
18554 buffer_1.clone(),
18555 [ExcerptRange::new(
18556 Point::new(0, 0)
18557 ..Point::new(
18558 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18559 0,
18560 ),
18561 )],
18562 cx,
18563 );
18564 multi_buffer
18565 });
18566 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18567 Editor::new(
18568 EditorMode::full(),
18569 multi_buffer,
18570 Some(project.clone()),
18571 window,
18572 cx,
18573 )
18574 });
18575
18576 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18577 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18578 enum TestHighlight {}
18579 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18580 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18581 editor.highlight_text::<TestHighlight>(
18582 vec![highlight_range.clone()],
18583 HighlightStyle::color(Hsla::green()),
18584 cx,
18585 );
18586 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18587 });
18588
18589 let full_text = format!("\n\n{sample_text}");
18590 assert_eq!(
18591 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18592 full_text,
18593 );
18594}
18595
18596#[gpui::test]
18597async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18598 init_test(cx, |_| {});
18599 cx.update(|cx| {
18600 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18601 "keymaps/default-linux.json",
18602 cx,
18603 )
18604 .unwrap();
18605 cx.bind_keys(default_key_bindings);
18606 });
18607
18608 let (editor, cx) = cx.add_window_view(|window, cx| {
18609 let multi_buffer = MultiBuffer::build_multi(
18610 [
18611 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18612 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18613 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18614 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18615 ],
18616 cx,
18617 );
18618 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18619
18620 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18621 // fold all but the second buffer, so that we test navigating between two
18622 // adjacent folded buffers, as well as folded buffers at the start and
18623 // end the multibuffer
18624 editor.fold_buffer(buffer_ids[0], cx);
18625 editor.fold_buffer(buffer_ids[2], cx);
18626 editor.fold_buffer(buffer_ids[3], cx);
18627
18628 editor
18629 });
18630 cx.simulate_resize(size(px(1000.), px(1000.)));
18631
18632 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18633 cx.assert_excerpts_with_selections(indoc! {"
18634 [EXCERPT]
18635 ˇ[FOLDED]
18636 [EXCERPT]
18637 a1
18638 b1
18639 [EXCERPT]
18640 [FOLDED]
18641 [EXCERPT]
18642 [FOLDED]
18643 "
18644 });
18645 cx.simulate_keystroke("down");
18646 cx.assert_excerpts_with_selections(indoc! {"
18647 [EXCERPT]
18648 [FOLDED]
18649 [EXCERPT]
18650 ˇa1
18651 b1
18652 [EXCERPT]
18653 [FOLDED]
18654 [EXCERPT]
18655 [FOLDED]
18656 "
18657 });
18658 cx.simulate_keystroke("down");
18659 cx.assert_excerpts_with_selections(indoc! {"
18660 [EXCERPT]
18661 [FOLDED]
18662 [EXCERPT]
18663 a1
18664 ˇb1
18665 [EXCERPT]
18666 [FOLDED]
18667 [EXCERPT]
18668 [FOLDED]
18669 "
18670 });
18671 cx.simulate_keystroke("down");
18672 cx.assert_excerpts_with_selections(indoc! {"
18673 [EXCERPT]
18674 [FOLDED]
18675 [EXCERPT]
18676 a1
18677 b1
18678 ˇ[EXCERPT]
18679 [FOLDED]
18680 [EXCERPT]
18681 [FOLDED]
18682 "
18683 });
18684 cx.simulate_keystroke("down");
18685 cx.assert_excerpts_with_selections(indoc! {"
18686 [EXCERPT]
18687 [FOLDED]
18688 [EXCERPT]
18689 a1
18690 b1
18691 [EXCERPT]
18692 ˇ[FOLDED]
18693 [EXCERPT]
18694 [FOLDED]
18695 "
18696 });
18697 for _ in 0..5 {
18698 cx.simulate_keystroke("down");
18699 cx.assert_excerpts_with_selections(indoc! {"
18700 [EXCERPT]
18701 [FOLDED]
18702 [EXCERPT]
18703 a1
18704 b1
18705 [EXCERPT]
18706 [FOLDED]
18707 [EXCERPT]
18708 ˇ[FOLDED]
18709 "
18710 });
18711 }
18712
18713 cx.simulate_keystroke("up");
18714 cx.assert_excerpts_with_selections(indoc! {"
18715 [EXCERPT]
18716 [FOLDED]
18717 [EXCERPT]
18718 a1
18719 b1
18720 [EXCERPT]
18721 ˇ[FOLDED]
18722 [EXCERPT]
18723 [FOLDED]
18724 "
18725 });
18726 cx.simulate_keystroke("up");
18727 cx.assert_excerpts_with_selections(indoc! {"
18728 [EXCERPT]
18729 [FOLDED]
18730 [EXCERPT]
18731 a1
18732 b1
18733 ˇ[EXCERPT]
18734 [FOLDED]
18735 [EXCERPT]
18736 [FOLDED]
18737 "
18738 });
18739 cx.simulate_keystroke("up");
18740 cx.assert_excerpts_with_selections(indoc! {"
18741 [EXCERPT]
18742 [FOLDED]
18743 [EXCERPT]
18744 a1
18745 ˇb1
18746 [EXCERPT]
18747 [FOLDED]
18748 [EXCERPT]
18749 [FOLDED]
18750 "
18751 });
18752 cx.simulate_keystroke("up");
18753 cx.assert_excerpts_with_selections(indoc! {"
18754 [EXCERPT]
18755 [FOLDED]
18756 [EXCERPT]
18757 ˇa1
18758 b1
18759 [EXCERPT]
18760 [FOLDED]
18761 [EXCERPT]
18762 [FOLDED]
18763 "
18764 });
18765 for _ in 0..5 {
18766 cx.simulate_keystroke("up");
18767 cx.assert_excerpts_with_selections(indoc! {"
18768 [EXCERPT]
18769 ˇ[FOLDED]
18770 [EXCERPT]
18771 a1
18772 b1
18773 [EXCERPT]
18774 [FOLDED]
18775 [EXCERPT]
18776 [FOLDED]
18777 "
18778 });
18779 }
18780}
18781
18782#[gpui::test]
18783async fn test_inline_completion_text(cx: &mut TestAppContext) {
18784 init_test(cx, |_| {});
18785
18786 // Simple insertion
18787 assert_highlighted_edits(
18788 "Hello, world!",
18789 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18790 true,
18791 cx,
18792 |highlighted_edits, cx| {
18793 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18794 assert_eq!(highlighted_edits.highlights.len(), 1);
18795 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18796 assert_eq!(
18797 highlighted_edits.highlights[0].1.background_color,
18798 Some(cx.theme().status().created_background)
18799 );
18800 },
18801 )
18802 .await;
18803
18804 // Replacement
18805 assert_highlighted_edits(
18806 "This is a test.",
18807 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18808 false,
18809 cx,
18810 |highlighted_edits, cx| {
18811 assert_eq!(highlighted_edits.text, "That is a test.");
18812 assert_eq!(highlighted_edits.highlights.len(), 1);
18813 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18814 assert_eq!(
18815 highlighted_edits.highlights[0].1.background_color,
18816 Some(cx.theme().status().created_background)
18817 );
18818 },
18819 )
18820 .await;
18821
18822 // Multiple edits
18823 assert_highlighted_edits(
18824 "Hello, world!",
18825 vec![
18826 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18827 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18828 ],
18829 false,
18830 cx,
18831 |highlighted_edits, cx| {
18832 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18833 assert_eq!(highlighted_edits.highlights.len(), 2);
18834 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18835 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18836 assert_eq!(
18837 highlighted_edits.highlights[0].1.background_color,
18838 Some(cx.theme().status().created_background)
18839 );
18840 assert_eq!(
18841 highlighted_edits.highlights[1].1.background_color,
18842 Some(cx.theme().status().created_background)
18843 );
18844 },
18845 )
18846 .await;
18847
18848 // Multiple lines with edits
18849 assert_highlighted_edits(
18850 "First line\nSecond line\nThird line\nFourth line",
18851 vec![
18852 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18853 (
18854 Point::new(2, 0)..Point::new(2, 10),
18855 "New third line".to_string(),
18856 ),
18857 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18858 ],
18859 false,
18860 cx,
18861 |highlighted_edits, cx| {
18862 assert_eq!(
18863 highlighted_edits.text,
18864 "Second modified\nNew third line\nFourth updated line"
18865 );
18866 assert_eq!(highlighted_edits.highlights.len(), 3);
18867 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18868 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18869 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18870 for highlight in &highlighted_edits.highlights {
18871 assert_eq!(
18872 highlight.1.background_color,
18873 Some(cx.theme().status().created_background)
18874 );
18875 }
18876 },
18877 )
18878 .await;
18879}
18880
18881#[gpui::test]
18882async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18883 init_test(cx, |_| {});
18884
18885 // Deletion
18886 assert_highlighted_edits(
18887 "Hello, world!",
18888 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18889 true,
18890 cx,
18891 |highlighted_edits, cx| {
18892 assert_eq!(highlighted_edits.text, "Hello, world!");
18893 assert_eq!(highlighted_edits.highlights.len(), 1);
18894 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18895 assert_eq!(
18896 highlighted_edits.highlights[0].1.background_color,
18897 Some(cx.theme().status().deleted_background)
18898 );
18899 },
18900 )
18901 .await;
18902
18903 // Insertion
18904 assert_highlighted_edits(
18905 "Hello, world!",
18906 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18907 true,
18908 cx,
18909 |highlighted_edits, cx| {
18910 assert_eq!(highlighted_edits.highlights.len(), 1);
18911 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18912 assert_eq!(
18913 highlighted_edits.highlights[0].1.background_color,
18914 Some(cx.theme().status().created_background)
18915 );
18916 },
18917 )
18918 .await;
18919}
18920
18921async fn assert_highlighted_edits(
18922 text: &str,
18923 edits: Vec<(Range<Point>, String)>,
18924 include_deletions: bool,
18925 cx: &mut TestAppContext,
18926 assertion_fn: impl Fn(HighlightedText, &App),
18927) {
18928 let window = cx.add_window(|window, cx| {
18929 let buffer = MultiBuffer::build_simple(text, cx);
18930 Editor::new(EditorMode::full(), buffer, None, window, cx)
18931 });
18932 let cx = &mut VisualTestContext::from_window(*window, cx);
18933
18934 let (buffer, snapshot) = window
18935 .update(cx, |editor, _window, cx| {
18936 (
18937 editor.buffer().clone(),
18938 editor.buffer().read(cx).snapshot(cx),
18939 )
18940 })
18941 .unwrap();
18942
18943 let edits = edits
18944 .into_iter()
18945 .map(|(range, edit)| {
18946 (
18947 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18948 edit,
18949 )
18950 })
18951 .collect::<Vec<_>>();
18952
18953 let text_anchor_edits = edits
18954 .clone()
18955 .into_iter()
18956 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18957 .collect::<Vec<_>>();
18958
18959 let edit_preview = window
18960 .update(cx, |_, _window, cx| {
18961 buffer
18962 .read(cx)
18963 .as_singleton()
18964 .unwrap()
18965 .read(cx)
18966 .preview_edits(text_anchor_edits.into(), cx)
18967 })
18968 .unwrap()
18969 .await;
18970
18971 cx.update(|_window, cx| {
18972 let highlighted_edits = inline_completion_edit_text(
18973 &snapshot.as_singleton().unwrap().2,
18974 &edits,
18975 &edit_preview,
18976 include_deletions,
18977 cx,
18978 );
18979 assertion_fn(highlighted_edits, cx)
18980 });
18981}
18982
18983#[track_caller]
18984fn assert_breakpoint(
18985 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18986 path: &Arc<Path>,
18987 expected: Vec<(u32, Breakpoint)>,
18988) {
18989 if expected.len() == 0usize {
18990 assert!(!breakpoints.contains_key(path), "{}", path.display());
18991 } else {
18992 let mut breakpoint = breakpoints
18993 .get(path)
18994 .unwrap()
18995 .into_iter()
18996 .map(|breakpoint| {
18997 (
18998 breakpoint.row,
18999 Breakpoint {
19000 message: breakpoint.message.clone(),
19001 state: breakpoint.state,
19002 condition: breakpoint.condition.clone(),
19003 hit_condition: breakpoint.hit_condition.clone(),
19004 },
19005 )
19006 })
19007 .collect::<Vec<_>>();
19008
19009 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
19010
19011 assert_eq!(expected, breakpoint);
19012 }
19013}
19014
19015fn add_log_breakpoint_at_cursor(
19016 editor: &mut Editor,
19017 log_message: &str,
19018 window: &mut Window,
19019 cx: &mut Context<Editor>,
19020) {
19021 let (anchor, bp) = editor
19022 .breakpoints_at_cursors(window, cx)
19023 .first()
19024 .and_then(|(anchor, bp)| {
19025 if let Some(bp) = bp {
19026 Some((*anchor, bp.clone()))
19027 } else {
19028 None
19029 }
19030 })
19031 .unwrap_or_else(|| {
19032 let cursor_position: Point = editor.selections.newest(cx).head();
19033
19034 let breakpoint_position = editor
19035 .snapshot(window, cx)
19036 .display_snapshot
19037 .buffer_snapshot
19038 .anchor_before(Point::new(cursor_position.row, 0));
19039
19040 (breakpoint_position, Breakpoint::new_log(&log_message))
19041 });
19042
19043 editor.edit_breakpoint_at_anchor(
19044 anchor,
19045 bp,
19046 BreakpointEditAction::EditLogMessage(log_message.into()),
19047 cx,
19048 );
19049}
19050
19051#[gpui::test]
19052async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
19053 init_test(cx, |_| {});
19054
19055 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19056 let fs = FakeFs::new(cx.executor());
19057 fs.insert_tree(
19058 path!("/a"),
19059 json!({
19060 "main.rs": sample_text,
19061 }),
19062 )
19063 .await;
19064 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19065 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19066 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19067
19068 let fs = FakeFs::new(cx.executor());
19069 fs.insert_tree(
19070 path!("/a"),
19071 json!({
19072 "main.rs": sample_text,
19073 }),
19074 )
19075 .await;
19076 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19077 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19078 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19079 let worktree_id = workspace
19080 .update(cx, |workspace, _window, cx| {
19081 workspace.project().update(cx, |project, cx| {
19082 project.worktrees(cx).next().unwrap().read(cx).id()
19083 })
19084 })
19085 .unwrap();
19086
19087 let buffer = project
19088 .update(cx, |project, cx| {
19089 project.open_buffer((worktree_id, "main.rs"), cx)
19090 })
19091 .await
19092 .unwrap();
19093
19094 let (editor, cx) = cx.add_window_view(|window, cx| {
19095 Editor::new(
19096 EditorMode::full(),
19097 MultiBuffer::build_from_buffer(buffer, cx),
19098 Some(project.clone()),
19099 window,
19100 cx,
19101 )
19102 });
19103
19104 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19105 let abs_path = project.read_with(cx, |project, cx| {
19106 project
19107 .absolute_path(&project_path, cx)
19108 .map(|path_buf| Arc::from(path_buf.to_owned()))
19109 .unwrap()
19110 });
19111
19112 // assert we can add breakpoint on the first line
19113 editor.update_in(cx, |editor, window, cx| {
19114 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19115 editor.move_to_end(&MoveToEnd, window, cx);
19116 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19117 });
19118
19119 let breakpoints = editor.update(cx, |editor, cx| {
19120 editor
19121 .breakpoint_store()
19122 .as_ref()
19123 .unwrap()
19124 .read(cx)
19125 .all_source_breakpoints(cx)
19126 .clone()
19127 });
19128
19129 assert_eq!(1, breakpoints.len());
19130 assert_breakpoint(
19131 &breakpoints,
19132 &abs_path,
19133 vec![
19134 (0, Breakpoint::new_standard()),
19135 (3, Breakpoint::new_standard()),
19136 ],
19137 );
19138
19139 editor.update_in(cx, |editor, window, cx| {
19140 editor.move_to_beginning(&MoveToBeginning, window, cx);
19141 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19142 });
19143
19144 let breakpoints = editor.update(cx, |editor, cx| {
19145 editor
19146 .breakpoint_store()
19147 .as_ref()
19148 .unwrap()
19149 .read(cx)
19150 .all_source_breakpoints(cx)
19151 .clone()
19152 });
19153
19154 assert_eq!(1, breakpoints.len());
19155 assert_breakpoint(
19156 &breakpoints,
19157 &abs_path,
19158 vec![(3, Breakpoint::new_standard())],
19159 );
19160
19161 editor.update_in(cx, |editor, window, cx| {
19162 editor.move_to_end(&MoveToEnd, window, cx);
19163 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19164 });
19165
19166 let breakpoints = editor.update(cx, |editor, cx| {
19167 editor
19168 .breakpoint_store()
19169 .as_ref()
19170 .unwrap()
19171 .read(cx)
19172 .all_source_breakpoints(cx)
19173 .clone()
19174 });
19175
19176 assert_eq!(0, breakpoints.len());
19177 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19178}
19179
19180#[gpui::test]
19181async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19182 init_test(cx, |_| {});
19183
19184 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19185
19186 let fs = FakeFs::new(cx.executor());
19187 fs.insert_tree(
19188 path!("/a"),
19189 json!({
19190 "main.rs": sample_text,
19191 }),
19192 )
19193 .await;
19194 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19195 let (workspace, cx) =
19196 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19197
19198 let worktree_id = workspace.update(cx, |workspace, cx| {
19199 workspace.project().update(cx, |project, cx| {
19200 project.worktrees(cx).next().unwrap().read(cx).id()
19201 })
19202 });
19203
19204 let buffer = project
19205 .update(cx, |project, cx| {
19206 project.open_buffer((worktree_id, "main.rs"), cx)
19207 })
19208 .await
19209 .unwrap();
19210
19211 let (editor, cx) = cx.add_window_view(|window, cx| {
19212 Editor::new(
19213 EditorMode::full(),
19214 MultiBuffer::build_from_buffer(buffer, cx),
19215 Some(project.clone()),
19216 window,
19217 cx,
19218 )
19219 });
19220
19221 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19222 let abs_path = project.read_with(cx, |project, cx| {
19223 project
19224 .absolute_path(&project_path, cx)
19225 .map(|path_buf| Arc::from(path_buf.to_owned()))
19226 .unwrap()
19227 });
19228
19229 editor.update_in(cx, |editor, window, cx| {
19230 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19231 });
19232
19233 let breakpoints = editor.update(cx, |editor, cx| {
19234 editor
19235 .breakpoint_store()
19236 .as_ref()
19237 .unwrap()
19238 .read(cx)
19239 .all_source_breakpoints(cx)
19240 .clone()
19241 });
19242
19243 assert_breakpoint(
19244 &breakpoints,
19245 &abs_path,
19246 vec![(0, Breakpoint::new_log("hello world"))],
19247 );
19248
19249 // Removing a log message from a log breakpoint should remove it
19250 editor.update_in(cx, |editor, window, cx| {
19251 add_log_breakpoint_at_cursor(editor, "", window, cx);
19252 });
19253
19254 let breakpoints = editor.update(cx, |editor, cx| {
19255 editor
19256 .breakpoint_store()
19257 .as_ref()
19258 .unwrap()
19259 .read(cx)
19260 .all_source_breakpoints(cx)
19261 .clone()
19262 });
19263
19264 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19265
19266 editor.update_in(cx, |editor, window, cx| {
19267 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19268 editor.move_to_end(&MoveToEnd, window, cx);
19269 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19270 // Not adding a log message to a standard breakpoint shouldn't remove it
19271 add_log_breakpoint_at_cursor(editor, "", window, cx);
19272 });
19273
19274 let breakpoints = editor.update(cx, |editor, cx| {
19275 editor
19276 .breakpoint_store()
19277 .as_ref()
19278 .unwrap()
19279 .read(cx)
19280 .all_source_breakpoints(cx)
19281 .clone()
19282 });
19283
19284 assert_breakpoint(
19285 &breakpoints,
19286 &abs_path,
19287 vec![
19288 (0, Breakpoint::new_standard()),
19289 (3, Breakpoint::new_standard()),
19290 ],
19291 );
19292
19293 editor.update_in(cx, |editor, window, cx| {
19294 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19295 });
19296
19297 let breakpoints = editor.update(cx, |editor, cx| {
19298 editor
19299 .breakpoint_store()
19300 .as_ref()
19301 .unwrap()
19302 .read(cx)
19303 .all_source_breakpoints(cx)
19304 .clone()
19305 });
19306
19307 assert_breakpoint(
19308 &breakpoints,
19309 &abs_path,
19310 vec![
19311 (0, Breakpoint::new_standard()),
19312 (3, Breakpoint::new_log("hello world")),
19313 ],
19314 );
19315
19316 editor.update_in(cx, |editor, window, cx| {
19317 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19318 });
19319
19320 let breakpoints = editor.update(cx, |editor, cx| {
19321 editor
19322 .breakpoint_store()
19323 .as_ref()
19324 .unwrap()
19325 .read(cx)
19326 .all_source_breakpoints(cx)
19327 .clone()
19328 });
19329
19330 assert_breakpoint(
19331 &breakpoints,
19332 &abs_path,
19333 vec![
19334 (0, Breakpoint::new_standard()),
19335 (3, Breakpoint::new_log("hello Earth!!")),
19336 ],
19337 );
19338}
19339
19340/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19341/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19342/// or when breakpoints were placed out of order. This tests for a regression too
19343#[gpui::test]
19344async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19345 init_test(cx, |_| {});
19346
19347 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19348 let fs = FakeFs::new(cx.executor());
19349 fs.insert_tree(
19350 path!("/a"),
19351 json!({
19352 "main.rs": sample_text,
19353 }),
19354 )
19355 .await;
19356 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19357 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19358 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19359
19360 let fs = FakeFs::new(cx.executor());
19361 fs.insert_tree(
19362 path!("/a"),
19363 json!({
19364 "main.rs": sample_text,
19365 }),
19366 )
19367 .await;
19368 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19369 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19370 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19371 let worktree_id = workspace
19372 .update(cx, |workspace, _window, cx| {
19373 workspace.project().update(cx, |project, cx| {
19374 project.worktrees(cx).next().unwrap().read(cx).id()
19375 })
19376 })
19377 .unwrap();
19378
19379 let buffer = project
19380 .update(cx, |project, cx| {
19381 project.open_buffer((worktree_id, "main.rs"), cx)
19382 })
19383 .await
19384 .unwrap();
19385
19386 let (editor, cx) = cx.add_window_view(|window, cx| {
19387 Editor::new(
19388 EditorMode::full(),
19389 MultiBuffer::build_from_buffer(buffer, cx),
19390 Some(project.clone()),
19391 window,
19392 cx,
19393 )
19394 });
19395
19396 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19397 let abs_path = project.read_with(cx, |project, cx| {
19398 project
19399 .absolute_path(&project_path, cx)
19400 .map(|path_buf| Arc::from(path_buf.to_owned()))
19401 .unwrap()
19402 });
19403
19404 // assert we can add breakpoint on the first line
19405 editor.update_in(cx, |editor, window, cx| {
19406 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19407 editor.move_to_end(&MoveToEnd, window, cx);
19408 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19409 editor.move_up(&MoveUp, window, cx);
19410 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19411 });
19412
19413 let breakpoints = editor.update(cx, |editor, cx| {
19414 editor
19415 .breakpoint_store()
19416 .as_ref()
19417 .unwrap()
19418 .read(cx)
19419 .all_source_breakpoints(cx)
19420 .clone()
19421 });
19422
19423 assert_eq!(1, breakpoints.len());
19424 assert_breakpoint(
19425 &breakpoints,
19426 &abs_path,
19427 vec![
19428 (0, Breakpoint::new_standard()),
19429 (2, Breakpoint::new_standard()),
19430 (3, Breakpoint::new_standard()),
19431 ],
19432 );
19433
19434 editor.update_in(cx, |editor, window, cx| {
19435 editor.move_to_beginning(&MoveToBeginning, window, cx);
19436 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19437 editor.move_to_end(&MoveToEnd, window, cx);
19438 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19439 // Disabling a breakpoint that doesn't exist should do nothing
19440 editor.move_up(&MoveUp, window, cx);
19441 editor.move_up(&MoveUp, window, cx);
19442 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19443 });
19444
19445 let breakpoints = editor.update(cx, |editor, cx| {
19446 editor
19447 .breakpoint_store()
19448 .as_ref()
19449 .unwrap()
19450 .read(cx)
19451 .all_source_breakpoints(cx)
19452 .clone()
19453 });
19454
19455 let disable_breakpoint = {
19456 let mut bp = Breakpoint::new_standard();
19457 bp.state = BreakpointState::Disabled;
19458 bp
19459 };
19460
19461 assert_eq!(1, breakpoints.len());
19462 assert_breakpoint(
19463 &breakpoints,
19464 &abs_path,
19465 vec![
19466 (0, disable_breakpoint.clone()),
19467 (2, Breakpoint::new_standard()),
19468 (3, disable_breakpoint.clone()),
19469 ],
19470 );
19471
19472 editor.update_in(cx, |editor, window, cx| {
19473 editor.move_to_beginning(&MoveToBeginning, window, cx);
19474 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19475 editor.move_to_end(&MoveToEnd, window, cx);
19476 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19477 editor.move_up(&MoveUp, window, cx);
19478 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19479 });
19480
19481 let breakpoints = editor.update(cx, |editor, cx| {
19482 editor
19483 .breakpoint_store()
19484 .as_ref()
19485 .unwrap()
19486 .read(cx)
19487 .all_source_breakpoints(cx)
19488 .clone()
19489 });
19490
19491 assert_eq!(1, breakpoints.len());
19492 assert_breakpoint(
19493 &breakpoints,
19494 &abs_path,
19495 vec![
19496 (0, Breakpoint::new_standard()),
19497 (2, disable_breakpoint),
19498 (3, Breakpoint::new_standard()),
19499 ],
19500 );
19501}
19502
19503#[gpui::test]
19504async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19505 init_test(cx, |_| {});
19506 let capabilities = lsp::ServerCapabilities {
19507 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19508 prepare_provider: Some(true),
19509 work_done_progress_options: Default::default(),
19510 })),
19511 ..Default::default()
19512 };
19513 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19514
19515 cx.set_state(indoc! {"
19516 struct Fˇoo {}
19517 "});
19518
19519 cx.update_editor(|editor, _, cx| {
19520 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19521 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19522 editor.highlight_background::<DocumentHighlightRead>(
19523 &[highlight_range],
19524 |c| c.editor_document_highlight_read_background,
19525 cx,
19526 );
19527 });
19528
19529 let mut prepare_rename_handler = cx
19530 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19531 move |_, _, _| async move {
19532 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19533 start: lsp::Position {
19534 line: 0,
19535 character: 7,
19536 },
19537 end: lsp::Position {
19538 line: 0,
19539 character: 10,
19540 },
19541 })))
19542 },
19543 );
19544 let prepare_rename_task = cx
19545 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19546 .expect("Prepare rename was not started");
19547 prepare_rename_handler.next().await.unwrap();
19548 prepare_rename_task.await.expect("Prepare rename failed");
19549
19550 let mut rename_handler =
19551 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19552 let edit = lsp::TextEdit {
19553 range: lsp::Range {
19554 start: lsp::Position {
19555 line: 0,
19556 character: 7,
19557 },
19558 end: lsp::Position {
19559 line: 0,
19560 character: 10,
19561 },
19562 },
19563 new_text: "FooRenamed".to_string(),
19564 };
19565 Ok(Some(lsp::WorkspaceEdit::new(
19566 // Specify the same edit twice
19567 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19568 )))
19569 });
19570 let rename_task = cx
19571 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19572 .expect("Confirm rename was not started");
19573 rename_handler.next().await.unwrap();
19574 rename_task.await.expect("Confirm rename failed");
19575 cx.run_until_parked();
19576
19577 // Despite two edits, only one is actually applied as those are identical
19578 cx.assert_editor_state(indoc! {"
19579 struct FooRenamedˇ {}
19580 "});
19581}
19582
19583#[gpui::test]
19584async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19585 init_test(cx, |_| {});
19586 // These capabilities indicate that the server does not support prepare rename.
19587 let capabilities = lsp::ServerCapabilities {
19588 rename_provider: Some(lsp::OneOf::Left(true)),
19589 ..Default::default()
19590 };
19591 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19592
19593 cx.set_state(indoc! {"
19594 struct Fˇoo {}
19595 "});
19596
19597 cx.update_editor(|editor, _window, cx| {
19598 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19599 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19600 editor.highlight_background::<DocumentHighlightRead>(
19601 &[highlight_range],
19602 |c| c.editor_document_highlight_read_background,
19603 cx,
19604 );
19605 });
19606
19607 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19608 .expect("Prepare rename was not started")
19609 .await
19610 .expect("Prepare rename failed");
19611
19612 let mut rename_handler =
19613 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19614 let edit = lsp::TextEdit {
19615 range: lsp::Range {
19616 start: lsp::Position {
19617 line: 0,
19618 character: 7,
19619 },
19620 end: lsp::Position {
19621 line: 0,
19622 character: 10,
19623 },
19624 },
19625 new_text: "FooRenamed".to_string(),
19626 };
19627 Ok(Some(lsp::WorkspaceEdit::new(
19628 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19629 )))
19630 });
19631 let rename_task = cx
19632 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19633 .expect("Confirm rename was not started");
19634 rename_handler.next().await.unwrap();
19635 rename_task.await.expect("Confirm rename failed");
19636 cx.run_until_parked();
19637
19638 // Correct range is renamed, as `surrounding_word` is used to find it.
19639 cx.assert_editor_state(indoc! {"
19640 struct FooRenamedˇ {}
19641 "});
19642}
19643
19644#[gpui::test]
19645async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19646 init_test(cx, |_| {});
19647 let mut cx = EditorTestContext::new(cx).await;
19648
19649 let language = Arc::new(
19650 Language::new(
19651 LanguageConfig::default(),
19652 Some(tree_sitter_html::LANGUAGE.into()),
19653 )
19654 .with_brackets_query(
19655 r#"
19656 ("<" @open "/>" @close)
19657 ("</" @open ">" @close)
19658 ("<" @open ">" @close)
19659 ("\"" @open "\"" @close)
19660 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19661 "#,
19662 )
19663 .unwrap(),
19664 );
19665 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19666
19667 cx.set_state(indoc! {"
19668 <span>ˇ</span>
19669 "});
19670 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19671 cx.assert_editor_state(indoc! {"
19672 <span>
19673 ˇ
19674 </span>
19675 "});
19676
19677 cx.set_state(indoc! {"
19678 <span><span></span>ˇ</span>
19679 "});
19680 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19681 cx.assert_editor_state(indoc! {"
19682 <span><span></span>
19683 ˇ</span>
19684 "});
19685
19686 cx.set_state(indoc! {"
19687 <span>ˇ
19688 </span>
19689 "});
19690 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19691 cx.assert_editor_state(indoc! {"
19692 <span>
19693 ˇ
19694 </span>
19695 "});
19696}
19697
19698#[gpui::test(iterations = 10)]
19699async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19700 init_test(cx, |_| {});
19701
19702 let fs = FakeFs::new(cx.executor());
19703 fs.insert_tree(
19704 path!("/dir"),
19705 json!({
19706 "a.ts": "a",
19707 }),
19708 )
19709 .await;
19710
19711 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19712 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19713 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19714
19715 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19716 language_registry.add(Arc::new(Language::new(
19717 LanguageConfig {
19718 name: "TypeScript".into(),
19719 matcher: LanguageMatcher {
19720 path_suffixes: vec!["ts".to_string()],
19721 ..Default::default()
19722 },
19723 ..Default::default()
19724 },
19725 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19726 )));
19727 let mut fake_language_servers = language_registry.register_fake_lsp(
19728 "TypeScript",
19729 FakeLspAdapter {
19730 capabilities: lsp::ServerCapabilities {
19731 code_lens_provider: Some(lsp::CodeLensOptions {
19732 resolve_provider: Some(true),
19733 }),
19734 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19735 commands: vec!["_the/command".to_string()],
19736 ..lsp::ExecuteCommandOptions::default()
19737 }),
19738 ..lsp::ServerCapabilities::default()
19739 },
19740 ..FakeLspAdapter::default()
19741 },
19742 );
19743
19744 let (buffer, _handle) = project
19745 .update(cx, |p, cx| {
19746 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19747 })
19748 .await
19749 .unwrap();
19750 cx.executor().run_until_parked();
19751
19752 let fake_server = fake_language_servers.next().await.unwrap();
19753
19754 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19755 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19756 drop(buffer_snapshot);
19757 let actions = cx
19758 .update_window(*workspace, |_, window, cx| {
19759 project.code_actions(&buffer, anchor..anchor, window, cx)
19760 })
19761 .unwrap();
19762
19763 fake_server
19764 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19765 Ok(Some(vec![
19766 lsp::CodeLens {
19767 range: lsp::Range::default(),
19768 command: Some(lsp::Command {
19769 title: "Code lens command".to_owned(),
19770 command: "_the/command".to_owned(),
19771 arguments: None,
19772 }),
19773 data: None,
19774 },
19775 lsp::CodeLens {
19776 range: lsp::Range::default(),
19777 command: Some(lsp::Command {
19778 title: "Command not in capabilities".to_owned(),
19779 command: "not in capabilities".to_owned(),
19780 arguments: None,
19781 }),
19782 data: None,
19783 },
19784 lsp::CodeLens {
19785 range: lsp::Range {
19786 start: lsp::Position {
19787 line: 1,
19788 character: 1,
19789 },
19790 end: lsp::Position {
19791 line: 1,
19792 character: 1,
19793 },
19794 },
19795 command: Some(lsp::Command {
19796 title: "Command not in range".to_owned(),
19797 command: "_the/command".to_owned(),
19798 arguments: None,
19799 }),
19800 data: None,
19801 },
19802 ]))
19803 })
19804 .next()
19805 .await;
19806
19807 let actions = actions.await.unwrap();
19808 assert_eq!(
19809 actions.len(),
19810 1,
19811 "Should have only one valid action for the 0..0 range"
19812 );
19813 let action = actions[0].clone();
19814 let apply = project.update(cx, |project, cx| {
19815 project.apply_code_action(buffer.clone(), action, true, cx)
19816 });
19817
19818 // Resolving the code action does not populate its edits. In absence of
19819 // edits, we must execute the given command.
19820 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19821 |mut lens, _| async move {
19822 let lens_command = lens.command.as_mut().expect("should have a command");
19823 assert_eq!(lens_command.title, "Code lens command");
19824 lens_command.arguments = Some(vec![json!("the-argument")]);
19825 Ok(lens)
19826 },
19827 );
19828
19829 // While executing the command, the language server sends the editor
19830 // a `workspaceEdit` request.
19831 fake_server
19832 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19833 let fake = fake_server.clone();
19834 move |params, _| {
19835 assert_eq!(params.command, "_the/command");
19836 let fake = fake.clone();
19837 async move {
19838 fake.server
19839 .request::<lsp::request::ApplyWorkspaceEdit>(
19840 lsp::ApplyWorkspaceEditParams {
19841 label: None,
19842 edit: lsp::WorkspaceEdit {
19843 changes: Some(
19844 [(
19845 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19846 vec![lsp::TextEdit {
19847 range: lsp::Range::new(
19848 lsp::Position::new(0, 0),
19849 lsp::Position::new(0, 0),
19850 ),
19851 new_text: "X".into(),
19852 }],
19853 )]
19854 .into_iter()
19855 .collect(),
19856 ),
19857 ..Default::default()
19858 },
19859 },
19860 )
19861 .await
19862 .into_response()
19863 .unwrap();
19864 Ok(Some(json!(null)))
19865 }
19866 }
19867 })
19868 .next()
19869 .await;
19870
19871 // Applying the code lens command returns a project transaction containing the edits
19872 // sent by the language server in its `workspaceEdit` request.
19873 let transaction = apply.await.unwrap();
19874 assert!(transaction.0.contains_key(&buffer));
19875 buffer.update(cx, |buffer, cx| {
19876 assert_eq!(buffer.text(), "Xa");
19877 buffer.undo(cx);
19878 assert_eq!(buffer.text(), "a");
19879 });
19880}
19881
19882#[gpui::test]
19883async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19884 init_test(cx, |_| {});
19885
19886 let fs = FakeFs::new(cx.executor());
19887 let main_text = r#"fn main() {
19888println!("1");
19889println!("2");
19890println!("3");
19891println!("4");
19892println!("5");
19893}"#;
19894 let lib_text = "mod foo {}";
19895 fs.insert_tree(
19896 path!("/a"),
19897 json!({
19898 "lib.rs": lib_text,
19899 "main.rs": main_text,
19900 }),
19901 )
19902 .await;
19903
19904 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19905 let (workspace, cx) =
19906 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19907 let worktree_id = workspace.update(cx, |workspace, cx| {
19908 workspace.project().update(cx, |project, cx| {
19909 project.worktrees(cx).next().unwrap().read(cx).id()
19910 })
19911 });
19912
19913 let expected_ranges = vec![
19914 Point::new(0, 0)..Point::new(0, 0),
19915 Point::new(1, 0)..Point::new(1, 1),
19916 Point::new(2, 0)..Point::new(2, 2),
19917 Point::new(3, 0)..Point::new(3, 3),
19918 ];
19919
19920 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19921 let editor_1 = workspace
19922 .update_in(cx, |workspace, window, cx| {
19923 workspace.open_path(
19924 (worktree_id, "main.rs"),
19925 Some(pane_1.downgrade()),
19926 true,
19927 window,
19928 cx,
19929 )
19930 })
19931 .unwrap()
19932 .await
19933 .downcast::<Editor>()
19934 .unwrap();
19935 pane_1.update(cx, |pane, cx| {
19936 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19937 open_editor.update(cx, |editor, cx| {
19938 assert_eq!(
19939 editor.display_text(cx),
19940 main_text,
19941 "Original main.rs text on initial open",
19942 );
19943 assert_eq!(
19944 editor
19945 .selections
19946 .all::<Point>(cx)
19947 .into_iter()
19948 .map(|s| s.range())
19949 .collect::<Vec<_>>(),
19950 vec![Point::zero()..Point::zero()],
19951 "Default selections on initial open",
19952 );
19953 })
19954 });
19955 editor_1.update_in(cx, |editor, window, cx| {
19956 editor.change_selections(None, window, cx, |s| {
19957 s.select_ranges(expected_ranges.clone());
19958 });
19959 });
19960
19961 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19962 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19963 });
19964 let editor_2 = workspace
19965 .update_in(cx, |workspace, window, cx| {
19966 workspace.open_path(
19967 (worktree_id, "main.rs"),
19968 Some(pane_2.downgrade()),
19969 true,
19970 window,
19971 cx,
19972 )
19973 })
19974 .unwrap()
19975 .await
19976 .downcast::<Editor>()
19977 .unwrap();
19978 pane_2.update(cx, |pane, cx| {
19979 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19980 open_editor.update(cx, |editor, cx| {
19981 assert_eq!(
19982 editor.display_text(cx),
19983 main_text,
19984 "Original main.rs text on initial open in another panel",
19985 );
19986 assert_eq!(
19987 editor
19988 .selections
19989 .all::<Point>(cx)
19990 .into_iter()
19991 .map(|s| s.range())
19992 .collect::<Vec<_>>(),
19993 vec![Point::zero()..Point::zero()],
19994 "Default selections on initial open in another panel",
19995 );
19996 })
19997 });
19998
19999 editor_2.update_in(cx, |editor, window, cx| {
20000 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
20001 });
20002
20003 let _other_editor_1 = workspace
20004 .update_in(cx, |workspace, window, cx| {
20005 workspace.open_path(
20006 (worktree_id, "lib.rs"),
20007 Some(pane_1.downgrade()),
20008 true,
20009 window,
20010 cx,
20011 )
20012 })
20013 .unwrap()
20014 .await
20015 .downcast::<Editor>()
20016 .unwrap();
20017 pane_1
20018 .update_in(cx, |pane, window, cx| {
20019 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20020 .unwrap()
20021 })
20022 .await
20023 .unwrap();
20024 drop(editor_1);
20025 pane_1.update(cx, |pane, cx| {
20026 pane.active_item()
20027 .unwrap()
20028 .downcast::<Editor>()
20029 .unwrap()
20030 .update(cx, |editor, cx| {
20031 assert_eq!(
20032 editor.display_text(cx),
20033 lib_text,
20034 "Other file should be open and active",
20035 );
20036 });
20037 assert_eq!(pane.items().count(), 1, "No other editors should be open");
20038 });
20039
20040 let _other_editor_2 = workspace
20041 .update_in(cx, |workspace, window, cx| {
20042 workspace.open_path(
20043 (worktree_id, "lib.rs"),
20044 Some(pane_2.downgrade()),
20045 true,
20046 window,
20047 cx,
20048 )
20049 })
20050 .unwrap()
20051 .await
20052 .downcast::<Editor>()
20053 .unwrap();
20054 pane_2
20055 .update_in(cx, |pane, window, cx| {
20056 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20057 .unwrap()
20058 })
20059 .await
20060 .unwrap();
20061 drop(editor_2);
20062 pane_2.update(cx, |pane, cx| {
20063 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20064 open_editor.update(cx, |editor, cx| {
20065 assert_eq!(
20066 editor.display_text(cx),
20067 lib_text,
20068 "Other file should be open and active in another panel too",
20069 );
20070 });
20071 assert_eq!(
20072 pane.items().count(),
20073 1,
20074 "No other editors should be open in another pane",
20075 );
20076 });
20077
20078 let _editor_1_reopened = workspace
20079 .update_in(cx, |workspace, window, cx| {
20080 workspace.open_path(
20081 (worktree_id, "main.rs"),
20082 Some(pane_1.downgrade()),
20083 true,
20084 window,
20085 cx,
20086 )
20087 })
20088 .unwrap()
20089 .await
20090 .downcast::<Editor>()
20091 .unwrap();
20092 let _editor_2_reopened = workspace
20093 .update_in(cx, |workspace, window, cx| {
20094 workspace.open_path(
20095 (worktree_id, "main.rs"),
20096 Some(pane_2.downgrade()),
20097 true,
20098 window,
20099 cx,
20100 )
20101 })
20102 .unwrap()
20103 .await
20104 .downcast::<Editor>()
20105 .unwrap();
20106 pane_1.update(cx, |pane, cx| {
20107 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20108 open_editor.update(cx, |editor, cx| {
20109 assert_eq!(
20110 editor.display_text(cx),
20111 main_text,
20112 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20113 );
20114 assert_eq!(
20115 editor
20116 .selections
20117 .all::<Point>(cx)
20118 .into_iter()
20119 .map(|s| s.range())
20120 .collect::<Vec<_>>(),
20121 expected_ranges,
20122 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20123 );
20124 })
20125 });
20126 pane_2.update(cx, |pane, cx| {
20127 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20128 open_editor.update(cx, |editor, cx| {
20129 assert_eq!(
20130 editor.display_text(cx),
20131 r#"fn main() {
20132⋯rintln!("1");
20133⋯intln!("2");
20134⋯ntln!("3");
20135println!("4");
20136println!("5");
20137}"#,
20138 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20139 );
20140 assert_eq!(
20141 editor
20142 .selections
20143 .all::<Point>(cx)
20144 .into_iter()
20145 .map(|s| s.range())
20146 .collect::<Vec<_>>(),
20147 vec![Point::zero()..Point::zero()],
20148 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20149 );
20150 })
20151 });
20152}
20153
20154#[gpui::test]
20155async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20156 init_test(cx, |_| {});
20157
20158 let fs = FakeFs::new(cx.executor());
20159 let main_text = r#"fn main() {
20160println!("1");
20161println!("2");
20162println!("3");
20163println!("4");
20164println!("5");
20165}"#;
20166 let lib_text = "mod foo {}";
20167 fs.insert_tree(
20168 path!("/a"),
20169 json!({
20170 "lib.rs": lib_text,
20171 "main.rs": main_text,
20172 }),
20173 )
20174 .await;
20175
20176 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20177 let (workspace, cx) =
20178 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20179 let worktree_id = workspace.update(cx, |workspace, cx| {
20180 workspace.project().update(cx, |project, cx| {
20181 project.worktrees(cx).next().unwrap().read(cx).id()
20182 })
20183 });
20184
20185 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20186 let editor = workspace
20187 .update_in(cx, |workspace, window, cx| {
20188 workspace.open_path(
20189 (worktree_id, "main.rs"),
20190 Some(pane.downgrade()),
20191 true,
20192 window,
20193 cx,
20194 )
20195 })
20196 .unwrap()
20197 .await
20198 .downcast::<Editor>()
20199 .unwrap();
20200 pane.update(cx, |pane, cx| {
20201 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20202 open_editor.update(cx, |editor, cx| {
20203 assert_eq!(
20204 editor.display_text(cx),
20205 main_text,
20206 "Original main.rs text on initial open",
20207 );
20208 })
20209 });
20210 editor.update_in(cx, |editor, window, cx| {
20211 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20212 });
20213
20214 cx.update_global(|store: &mut SettingsStore, cx| {
20215 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20216 s.restore_on_file_reopen = Some(false);
20217 });
20218 });
20219 editor.update_in(cx, |editor, window, cx| {
20220 editor.fold_ranges(
20221 vec![
20222 Point::new(1, 0)..Point::new(1, 1),
20223 Point::new(2, 0)..Point::new(2, 2),
20224 Point::new(3, 0)..Point::new(3, 3),
20225 ],
20226 false,
20227 window,
20228 cx,
20229 );
20230 });
20231 pane.update_in(cx, |pane, window, cx| {
20232 pane.close_all_items(&CloseAllItems::default(), window, cx)
20233 .unwrap()
20234 })
20235 .await
20236 .unwrap();
20237 pane.update(cx, |pane, _| {
20238 assert!(pane.active_item().is_none());
20239 });
20240 cx.update_global(|store: &mut SettingsStore, cx| {
20241 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20242 s.restore_on_file_reopen = Some(true);
20243 });
20244 });
20245
20246 let _editor_reopened = workspace
20247 .update_in(cx, |workspace, window, cx| {
20248 workspace.open_path(
20249 (worktree_id, "main.rs"),
20250 Some(pane.downgrade()),
20251 true,
20252 window,
20253 cx,
20254 )
20255 })
20256 .unwrap()
20257 .await
20258 .downcast::<Editor>()
20259 .unwrap();
20260 pane.update(cx, |pane, cx| {
20261 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20262 open_editor.update(cx, |editor, cx| {
20263 assert_eq!(
20264 editor.display_text(cx),
20265 main_text,
20266 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20267 );
20268 })
20269 });
20270}
20271
20272#[gpui::test]
20273async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20274 struct EmptyModalView {
20275 focus_handle: gpui::FocusHandle,
20276 }
20277 impl EventEmitter<DismissEvent> for EmptyModalView {}
20278 impl Render for EmptyModalView {
20279 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20280 div()
20281 }
20282 }
20283 impl Focusable for EmptyModalView {
20284 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20285 self.focus_handle.clone()
20286 }
20287 }
20288 impl workspace::ModalView for EmptyModalView {}
20289 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20290 EmptyModalView {
20291 focus_handle: cx.focus_handle(),
20292 }
20293 }
20294
20295 init_test(cx, |_| {});
20296
20297 let fs = FakeFs::new(cx.executor());
20298 let project = Project::test(fs, [], cx).await;
20299 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20300 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20301 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20302 let editor = cx.new_window_entity(|window, cx| {
20303 Editor::new(
20304 EditorMode::full(),
20305 buffer,
20306 Some(project.clone()),
20307 window,
20308 cx,
20309 )
20310 });
20311 workspace
20312 .update(cx, |workspace, window, cx| {
20313 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20314 })
20315 .unwrap();
20316 editor.update_in(cx, |editor, window, cx| {
20317 editor.open_context_menu(&OpenContextMenu, window, cx);
20318 assert!(editor.mouse_context_menu.is_some());
20319 });
20320 workspace
20321 .update(cx, |workspace, window, cx| {
20322 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20323 })
20324 .unwrap();
20325 cx.read(|cx| {
20326 assert!(editor.read(cx).mouse_context_menu.is_none());
20327 });
20328}
20329
20330#[gpui::test]
20331async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20332 init_test(cx, |_| {});
20333
20334 let fs = FakeFs::new(cx.executor());
20335 fs.insert_file(path!("/file.html"), Default::default())
20336 .await;
20337
20338 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20339
20340 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20341 let html_language = Arc::new(Language::new(
20342 LanguageConfig {
20343 name: "HTML".into(),
20344 matcher: LanguageMatcher {
20345 path_suffixes: vec!["html".to_string()],
20346 ..LanguageMatcher::default()
20347 },
20348 brackets: BracketPairConfig {
20349 pairs: vec![BracketPair {
20350 start: "<".into(),
20351 end: ">".into(),
20352 close: true,
20353 ..Default::default()
20354 }],
20355 ..Default::default()
20356 },
20357 ..Default::default()
20358 },
20359 Some(tree_sitter_html::LANGUAGE.into()),
20360 ));
20361 language_registry.add(html_language);
20362 let mut fake_servers = language_registry.register_fake_lsp(
20363 "HTML",
20364 FakeLspAdapter {
20365 capabilities: lsp::ServerCapabilities {
20366 completion_provider: Some(lsp::CompletionOptions {
20367 resolve_provider: Some(true),
20368 ..Default::default()
20369 }),
20370 ..Default::default()
20371 },
20372 ..Default::default()
20373 },
20374 );
20375
20376 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20377 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20378
20379 let worktree_id = workspace
20380 .update(cx, |workspace, _window, cx| {
20381 workspace.project().update(cx, |project, cx| {
20382 project.worktrees(cx).next().unwrap().read(cx).id()
20383 })
20384 })
20385 .unwrap();
20386 project
20387 .update(cx, |project, cx| {
20388 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20389 })
20390 .await
20391 .unwrap();
20392 let editor = workspace
20393 .update(cx, |workspace, window, cx| {
20394 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20395 })
20396 .unwrap()
20397 .await
20398 .unwrap()
20399 .downcast::<Editor>()
20400 .unwrap();
20401
20402 let fake_server = fake_servers.next().await.unwrap();
20403 editor.update_in(cx, |editor, window, cx| {
20404 editor.set_text("<ad></ad>", window, cx);
20405 editor.change_selections(None, window, cx, |selections| {
20406 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20407 });
20408 let Some((buffer, _)) = editor
20409 .buffer
20410 .read(cx)
20411 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20412 else {
20413 panic!("Failed to get buffer for selection position");
20414 };
20415 let buffer = buffer.read(cx);
20416 let buffer_id = buffer.remote_id();
20417 let opening_range =
20418 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20419 let closing_range =
20420 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20421 let mut linked_ranges = HashMap::default();
20422 linked_ranges.insert(
20423 buffer_id,
20424 vec![(opening_range.clone(), vec![closing_range.clone()])],
20425 );
20426 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20427 });
20428 let mut completion_handle =
20429 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20430 Ok(Some(lsp::CompletionResponse::Array(vec![
20431 lsp::CompletionItem {
20432 label: "head".to_string(),
20433 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20434 lsp::InsertReplaceEdit {
20435 new_text: "head".to_string(),
20436 insert: lsp::Range::new(
20437 lsp::Position::new(0, 1),
20438 lsp::Position::new(0, 3),
20439 ),
20440 replace: lsp::Range::new(
20441 lsp::Position::new(0, 1),
20442 lsp::Position::new(0, 3),
20443 ),
20444 },
20445 )),
20446 ..Default::default()
20447 },
20448 ])))
20449 });
20450 editor.update_in(cx, |editor, window, cx| {
20451 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20452 });
20453 cx.run_until_parked();
20454 completion_handle.next().await.unwrap();
20455 editor.update(cx, |editor, _| {
20456 assert!(
20457 editor.context_menu_visible(),
20458 "Completion menu should be visible"
20459 );
20460 });
20461 editor.update_in(cx, |editor, window, cx| {
20462 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20463 });
20464 cx.executor().run_until_parked();
20465 editor.update(cx, |editor, cx| {
20466 assert_eq!(editor.text(cx), "<head></head>");
20467 });
20468}
20469
20470#[gpui::test]
20471async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20472 init_test(cx, |_| {});
20473
20474 let fs = FakeFs::new(cx.executor());
20475 fs.insert_tree(
20476 path!("/root"),
20477 json!({
20478 "a": {
20479 "main.rs": "fn main() {}",
20480 },
20481 "foo": {
20482 "bar": {
20483 "external_file.rs": "pub mod external {}",
20484 }
20485 }
20486 }),
20487 )
20488 .await;
20489
20490 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20491 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20492 language_registry.add(rust_lang());
20493 let _fake_servers = language_registry.register_fake_lsp(
20494 "Rust",
20495 FakeLspAdapter {
20496 ..FakeLspAdapter::default()
20497 },
20498 );
20499 let (workspace, cx) =
20500 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20501 let worktree_id = workspace.update(cx, |workspace, cx| {
20502 workspace.project().update(cx, |project, cx| {
20503 project.worktrees(cx).next().unwrap().read(cx).id()
20504 })
20505 });
20506
20507 let assert_language_servers_count =
20508 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20509 project.update(cx, |project, cx| {
20510 let current = project
20511 .lsp_store()
20512 .read(cx)
20513 .as_local()
20514 .unwrap()
20515 .language_servers
20516 .len();
20517 assert_eq!(expected, current, "{context}");
20518 });
20519 };
20520
20521 assert_language_servers_count(
20522 0,
20523 "No servers should be running before any file is open",
20524 cx,
20525 );
20526 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20527 let main_editor = workspace
20528 .update_in(cx, |workspace, window, cx| {
20529 workspace.open_path(
20530 (worktree_id, "main.rs"),
20531 Some(pane.downgrade()),
20532 true,
20533 window,
20534 cx,
20535 )
20536 })
20537 .unwrap()
20538 .await
20539 .downcast::<Editor>()
20540 .unwrap();
20541 pane.update(cx, |pane, cx| {
20542 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20543 open_editor.update(cx, |editor, cx| {
20544 assert_eq!(
20545 editor.display_text(cx),
20546 "fn main() {}",
20547 "Original main.rs text on initial open",
20548 );
20549 });
20550 assert_eq!(open_editor, main_editor);
20551 });
20552 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20553
20554 let external_editor = workspace
20555 .update_in(cx, |workspace, window, cx| {
20556 workspace.open_abs_path(
20557 PathBuf::from("/root/foo/bar/external_file.rs"),
20558 OpenOptions::default(),
20559 window,
20560 cx,
20561 )
20562 })
20563 .await
20564 .expect("opening external file")
20565 .downcast::<Editor>()
20566 .expect("downcasted external file's open element to editor");
20567 pane.update(cx, |pane, cx| {
20568 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20569 open_editor.update(cx, |editor, cx| {
20570 assert_eq!(
20571 editor.display_text(cx),
20572 "pub mod external {}",
20573 "External file is open now",
20574 );
20575 });
20576 assert_eq!(open_editor, external_editor);
20577 });
20578 assert_language_servers_count(
20579 1,
20580 "Second, external, *.rs file should join the existing server",
20581 cx,
20582 );
20583
20584 pane.update_in(cx, |pane, window, cx| {
20585 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20586 })
20587 .unwrap()
20588 .await
20589 .unwrap();
20590 pane.update_in(cx, |pane, window, cx| {
20591 pane.navigate_backward(window, cx);
20592 });
20593 cx.run_until_parked();
20594 pane.update(cx, |pane, cx| {
20595 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20596 open_editor.update(cx, |editor, cx| {
20597 assert_eq!(
20598 editor.display_text(cx),
20599 "pub mod external {}",
20600 "External file is open now",
20601 );
20602 });
20603 });
20604 assert_language_servers_count(
20605 1,
20606 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20607 cx,
20608 );
20609
20610 cx.update(|_, cx| {
20611 workspace::reload(&workspace::Reload::default(), cx);
20612 });
20613 assert_language_servers_count(
20614 1,
20615 "After reloading the worktree with local and external files opened, only one project should be started",
20616 cx,
20617 );
20618}
20619
20620#[gpui::test]
20621async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20622 init_test(cx, |_| {});
20623
20624 let mut cx = EditorTestContext::new(cx).await;
20625 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20626 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20627
20628 // test cursor move to start of each line on tab
20629 // for `if`, `elif`, `else`, `while`, `with` and `for`
20630 cx.set_state(indoc! {"
20631 def main():
20632 ˇ for item in items:
20633 ˇ while item.active:
20634 ˇ if item.value > 10:
20635 ˇ continue
20636 ˇ elif item.value < 0:
20637 ˇ break
20638 ˇ else:
20639 ˇ with item.context() as ctx:
20640 ˇ yield count
20641 ˇ else:
20642 ˇ log('while else')
20643 ˇ else:
20644 ˇ log('for else')
20645 "});
20646 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20647 cx.assert_editor_state(indoc! {"
20648 def main():
20649 ˇfor item in items:
20650 ˇwhile item.active:
20651 ˇif item.value > 10:
20652 ˇcontinue
20653 ˇelif item.value < 0:
20654 ˇbreak
20655 ˇelse:
20656 ˇwith item.context() as ctx:
20657 ˇyield count
20658 ˇelse:
20659 ˇlog('while else')
20660 ˇelse:
20661 ˇlog('for else')
20662 "});
20663 // test relative indent is preserved when tab
20664 // for `if`, `elif`, `else`, `while`, `with` and `for`
20665 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20666 cx.assert_editor_state(indoc! {"
20667 def main():
20668 ˇfor item in items:
20669 ˇwhile item.active:
20670 ˇif item.value > 10:
20671 ˇcontinue
20672 ˇelif item.value < 0:
20673 ˇbreak
20674 ˇelse:
20675 ˇwith item.context() as ctx:
20676 ˇyield count
20677 ˇelse:
20678 ˇlog('while else')
20679 ˇelse:
20680 ˇlog('for else')
20681 "});
20682
20683 // test cursor move to start of each line on tab
20684 // for `try`, `except`, `else`, `finally`, `match` and `def`
20685 cx.set_state(indoc! {"
20686 def main():
20687 ˇ try:
20688 ˇ fetch()
20689 ˇ except ValueError:
20690 ˇ handle_error()
20691 ˇ else:
20692 ˇ match value:
20693 ˇ case _:
20694 ˇ finally:
20695 ˇ def status():
20696 ˇ return 0
20697 "});
20698 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20699 cx.assert_editor_state(indoc! {"
20700 def main():
20701 ˇtry:
20702 ˇfetch()
20703 ˇexcept ValueError:
20704 ˇhandle_error()
20705 ˇelse:
20706 ˇmatch value:
20707 ˇcase _:
20708 ˇfinally:
20709 ˇdef status():
20710 ˇreturn 0
20711 "});
20712 // test relative indent is preserved when tab
20713 // for `try`, `except`, `else`, `finally`, `match` and `def`
20714 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20715 cx.assert_editor_state(indoc! {"
20716 def main():
20717 ˇtry:
20718 ˇfetch()
20719 ˇexcept ValueError:
20720 ˇhandle_error()
20721 ˇelse:
20722 ˇmatch value:
20723 ˇcase _:
20724 ˇfinally:
20725 ˇdef status():
20726 ˇreturn 0
20727 "});
20728}
20729
20730#[gpui::test]
20731async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
20732 init_test(cx, |_| {});
20733
20734 let mut cx = EditorTestContext::new(cx).await;
20735 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20736 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20737
20738 // test `else` auto outdents when typed inside `if` block
20739 cx.set_state(indoc! {"
20740 def main():
20741 if i == 2:
20742 return
20743 ˇ
20744 "});
20745 cx.update_editor(|editor, window, cx| {
20746 editor.handle_input("else:", window, cx);
20747 });
20748 cx.assert_editor_state(indoc! {"
20749 def main():
20750 if i == 2:
20751 return
20752 else:ˇ
20753 "});
20754
20755 // test `except` auto outdents when typed inside `try` block
20756 cx.set_state(indoc! {"
20757 def main():
20758 try:
20759 i = 2
20760 ˇ
20761 "});
20762 cx.update_editor(|editor, window, cx| {
20763 editor.handle_input("except:", window, cx);
20764 });
20765 cx.assert_editor_state(indoc! {"
20766 def main():
20767 try:
20768 i = 2
20769 except:ˇ
20770 "});
20771
20772 // test `else` auto outdents when typed inside `except` block
20773 cx.set_state(indoc! {"
20774 def main():
20775 try:
20776 i = 2
20777 except:
20778 j = 2
20779 ˇ
20780 "});
20781 cx.update_editor(|editor, window, cx| {
20782 editor.handle_input("else:", window, cx);
20783 });
20784 cx.assert_editor_state(indoc! {"
20785 def main():
20786 try:
20787 i = 2
20788 except:
20789 j = 2
20790 else:ˇ
20791 "});
20792
20793 // test `finally` auto outdents when typed inside `else` block
20794 cx.set_state(indoc! {"
20795 def main():
20796 try:
20797 i = 2
20798 except:
20799 j = 2
20800 else:
20801 k = 2
20802 ˇ
20803 "});
20804 cx.update_editor(|editor, window, cx| {
20805 editor.handle_input("finally:", window, cx);
20806 });
20807 cx.assert_editor_state(indoc! {"
20808 def main():
20809 try:
20810 i = 2
20811 except:
20812 j = 2
20813 else:
20814 k = 2
20815 finally:ˇ
20816 "});
20817
20818 // TODO: test `except` auto outdents when typed inside `try` block right after for block
20819 // cx.set_state(indoc! {"
20820 // def main():
20821 // try:
20822 // for i in range(n):
20823 // pass
20824 // ˇ
20825 // "});
20826 // cx.update_editor(|editor, window, cx| {
20827 // editor.handle_input("except:", window, cx);
20828 // });
20829 // cx.assert_editor_state(indoc! {"
20830 // def main():
20831 // try:
20832 // for i in range(n):
20833 // pass
20834 // except:ˇ
20835 // "});
20836
20837 // TODO: test `else` auto outdents when typed inside `except` block right after for block
20838 // cx.set_state(indoc! {"
20839 // def main():
20840 // try:
20841 // i = 2
20842 // except:
20843 // for i in range(n):
20844 // pass
20845 // ˇ
20846 // "});
20847 // cx.update_editor(|editor, window, cx| {
20848 // editor.handle_input("else:", window, cx);
20849 // });
20850 // cx.assert_editor_state(indoc! {"
20851 // def main():
20852 // try:
20853 // i = 2
20854 // except:
20855 // for i in range(n):
20856 // pass
20857 // else:ˇ
20858 // "});
20859
20860 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
20861 // cx.set_state(indoc! {"
20862 // def main():
20863 // try:
20864 // i = 2
20865 // except:
20866 // j = 2
20867 // else:
20868 // for i in range(n):
20869 // pass
20870 // ˇ
20871 // "});
20872 // cx.update_editor(|editor, window, cx| {
20873 // editor.handle_input("finally:", window, cx);
20874 // });
20875 // cx.assert_editor_state(indoc! {"
20876 // def main():
20877 // try:
20878 // i = 2
20879 // except:
20880 // j = 2
20881 // else:
20882 // for i in range(n):
20883 // pass
20884 // finally:ˇ
20885 // "});
20886
20887 // test `else` stays at correct indent when typed after `for` block
20888 cx.set_state(indoc! {"
20889 def main():
20890 for i in range(10):
20891 if i == 3:
20892 break
20893 ˇ
20894 "});
20895 cx.update_editor(|editor, window, cx| {
20896 editor.handle_input("else:", window, cx);
20897 });
20898 cx.assert_editor_state(indoc! {"
20899 def main():
20900 for i in range(10):
20901 if i == 3:
20902 break
20903 else:ˇ
20904 "});
20905
20906 // test does not outdent on typing after line with square brackets
20907 cx.set_state(indoc! {"
20908 def f() -> list[str]:
20909 ˇ
20910 "});
20911 cx.update_editor(|editor, window, cx| {
20912 editor.handle_input("a", window, cx);
20913 });
20914 cx.assert_editor_state(indoc! {"
20915 def f() -> list[str]:
20916 aˇ
20917 "});
20918}
20919
20920#[gpui::test]
20921async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
20922 init_test(cx, |_| {});
20923 update_test_language_settings(cx, |settings| {
20924 settings.defaults.extend_comment_on_newline = Some(false);
20925 });
20926 let mut cx = EditorTestContext::new(cx).await;
20927 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20928 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20929
20930 // test correct indent after newline on comment
20931 cx.set_state(indoc! {"
20932 # COMMENT:ˇ
20933 "});
20934 cx.update_editor(|editor, window, cx| {
20935 editor.newline(&Newline, window, cx);
20936 });
20937 cx.assert_editor_state(indoc! {"
20938 # COMMENT:
20939 ˇ
20940 "});
20941
20942 // test correct indent after newline in brackets
20943 cx.set_state(indoc! {"
20944 {ˇ}
20945 "});
20946 cx.update_editor(|editor, window, cx| {
20947 editor.newline(&Newline, window, cx);
20948 });
20949 cx.run_until_parked();
20950 cx.assert_editor_state(indoc! {"
20951 {
20952 ˇ
20953 }
20954 "});
20955
20956 cx.set_state(indoc! {"
20957 (ˇ)
20958 "});
20959 cx.update_editor(|editor, window, cx| {
20960 editor.newline(&Newline, window, cx);
20961 });
20962 cx.run_until_parked();
20963 cx.assert_editor_state(indoc! {"
20964 (
20965 ˇ
20966 )
20967 "});
20968
20969 // do not indent after empty lists or dictionaries
20970 cx.set_state(indoc! {"
20971 a = []ˇ
20972 "});
20973 cx.update_editor(|editor, window, cx| {
20974 editor.newline(&Newline, window, cx);
20975 });
20976 cx.run_until_parked();
20977 cx.assert_editor_state(indoc! {"
20978 a = []
20979 ˇ
20980 "});
20981}
20982
20983fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20984 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20985 point..point
20986}
20987
20988fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20989 let (text, ranges) = marked_text_ranges(marked_text, true);
20990 assert_eq!(editor.text(cx), text);
20991 assert_eq!(
20992 editor.selections.ranges(cx),
20993 ranges,
20994 "Assert selections are {}",
20995 marked_text
20996 );
20997}
20998
20999pub fn handle_signature_help_request(
21000 cx: &mut EditorLspTestContext,
21001 mocked_response: lsp::SignatureHelp,
21002) -> impl Future<Output = ()> + use<> {
21003 let mut request =
21004 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
21005 let mocked_response = mocked_response.clone();
21006 async move { Ok(Some(mocked_response)) }
21007 });
21008
21009 async move {
21010 request.next().await;
21011 }
21012}
21013
21014/// Handle completion request passing a marked string specifying where the completion
21015/// should be triggered from using '|' character, what range should be replaced, and what completions
21016/// should be returned using '<' and '>' to delimit the range.
21017///
21018/// Also see `handle_completion_request_with_insert_and_replace`.
21019#[track_caller]
21020pub fn handle_completion_request(
21021 cx: &mut EditorLspTestContext,
21022 marked_string: &str,
21023 completions: Vec<&'static str>,
21024 counter: Arc<AtomicUsize>,
21025) -> impl Future<Output = ()> {
21026 let complete_from_marker: TextRangeMarker = '|'.into();
21027 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21028 let (_, mut marked_ranges) = marked_text_ranges_by(
21029 marked_string,
21030 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21031 );
21032
21033 let complete_from_position =
21034 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21035 let replace_range =
21036 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21037
21038 let mut request =
21039 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21040 let completions = completions.clone();
21041 counter.fetch_add(1, atomic::Ordering::Release);
21042 async move {
21043 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21044 assert_eq!(
21045 params.text_document_position.position,
21046 complete_from_position
21047 );
21048 Ok(Some(lsp::CompletionResponse::Array(
21049 completions
21050 .iter()
21051 .map(|completion_text| lsp::CompletionItem {
21052 label: completion_text.to_string(),
21053 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21054 range: replace_range,
21055 new_text: completion_text.to_string(),
21056 })),
21057 ..Default::default()
21058 })
21059 .collect(),
21060 )))
21061 }
21062 });
21063
21064 async move {
21065 request.next().await;
21066 }
21067}
21068
21069/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
21070/// given instead, which also contains an `insert` range.
21071///
21072/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
21073/// that is, `replace_range.start..cursor_pos`.
21074pub fn handle_completion_request_with_insert_and_replace(
21075 cx: &mut EditorLspTestContext,
21076 marked_string: &str,
21077 completions: Vec<&'static str>,
21078 counter: Arc<AtomicUsize>,
21079) -> impl Future<Output = ()> {
21080 let complete_from_marker: TextRangeMarker = '|'.into();
21081 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21082 let (_, mut marked_ranges) = marked_text_ranges_by(
21083 marked_string,
21084 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21085 );
21086
21087 let complete_from_position =
21088 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21089 let replace_range =
21090 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21091
21092 let mut request =
21093 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21094 let completions = completions.clone();
21095 counter.fetch_add(1, atomic::Ordering::Release);
21096 async move {
21097 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21098 assert_eq!(
21099 params.text_document_position.position, complete_from_position,
21100 "marker `|` position doesn't match",
21101 );
21102 Ok(Some(lsp::CompletionResponse::Array(
21103 completions
21104 .iter()
21105 .map(|completion_text| lsp::CompletionItem {
21106 label: completion_text.to_string(),
21107 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21108 lsp::InsertReplaceEdit {
21109 insert: lsp::Range {
21110 start: replace_range.start,
21111 end: complete_from_position,
21112 },
21113 replace: replace_range,
21114 new_text: completion_text.to_string(),
21115 },
21116 )),
21117 ..Default::default()
21118 })
21119 .collect(),
21120 )))
21121 }
21122 });
21123
21124 async move {
21125 request.next().await;
21126 }
21127}
21128
21129fn handle_resolve_completion_request(
21130 cx: &mut EditorLspTestContext,
21131 edits: Option<Vec<(&'static str, &'static str)>>,
21132) -> impl Future<Output = ()> {
21133 let edits = edits.map(|edits| {
21134 edits
21135 .iter()
21136 .map(|(marked_string, new_text)| {
21137 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21138 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21139 lsp::TextEdit::new(replace_range, new_text.to_string())
21140 })
21141 .collect::<Vec<_>>()
21142 });
21143
21144 let mut request =
21145 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21146 let edits = edits.clone();
21147 async move {
21148 Ok(lsp::CompletionItem {
21149 additional_text_edits: edits,
21150 ..Default::default()
21151 })
21152 }
21153 });
21154
21155 async move {
21156 request.next().await;
21157 }
21158}
21159
21160pub(crate) fn update_test_language_settings(
21161 cx: &mut TestAppContext,
21162 f: impl Fn(&mut AllLanguageSettingsContent),
21163) {
21164 cx.update(|cx| {
21165 SettingsStore::update_global(cx, |store, cx| {
21166 store.update_user_settings::<AllLanguageSettings>(cx, f);
21167 });
21168 });
21169}
21170
21171pub(crate) fn update_test_project_settings(
21172 cx: &mut TestAppContext,
21173 f: impl Fn(&mut ProjectSettings),
21174) {
21175 cx.update(|cx| {
21176 SettingsStore::update_global(cx, |store, cx| {
21177 store.update_user_settings::<ProjectSettings>(cx, f);
21178 });
21179 });
21180}
21181
21182pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
21183 cx.update(|cx| {
21184 assets::Assets.load_test_fonts(cx);
21185 let store = SettingsStore::test(cx);
21186 cx.set_global(store);
21187 theme::init(theme::LoadThemes::JustBase, cx);
21188 release_channel::init(SemanticVersion::default(), cx);
21189 client::init_settings(cx);
21190 language::init(cx);
21191 Project::init_settings(cx);
21192 workspace::init_settings(cx);
21193 crate::init(cx);
21194 });
21195
21196 update_test_language_settings(cx, f);
21197}
21198
21199#[track_caller]
21200fn assert_hunk_revert(
21201 not_reverted_text_with_selections: &str,
21202 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21203 expected_reverted_text_with_selections: &str,
21204 base_text: &str,
21205 cx: &mut EditorLspTestContext,
21206) {
21207 cx.set_state(not_reverted_text_with_selections);
21208 cx.set_head_text(base_text);
21209 cx.executor().run_until_parked();
21210
21211 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21212 let snapshot = editor.snapshot(window, cx);
21213 let reverted_hunk_statuses = snapshot
21214 .buffer_snapshot
21215 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21216 .map(|hunk| hunk.status().kind)
21217 .collect::<Vec<_>>();
21218
21219 editor.git_restore(&Default::default(), window, cx);
21220 reverted_hunk_statuses
21221 });
21222 cx.executor().run_until_parked();
21223 cx.assert_editor_state(expected_reverted_text_with_selections);
21224 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21225}