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(Language::new(
2867 LanguageConfig {
2868 documentation: Some(language::DocumentationConfig {
2869 start: "/**".into(),
2870 end: "*/".into(),
2871 prefix: "* ".into(),
2872 tab_size: NonZeroU32::new(1).unwrap(),
2873 }),
2874 ..LanguageConfig::default()
2875 },
2876 None,
2877 ));
2878 {
2879 let mut cx = EditorTestContext::new(cx).await;
2880 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2881 cx.set_state(indoc! {"
2882 /**ˇ
2883 "});
2884
2885 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2886 cx.assert_editor_state(indoc! {"
2887 /**
2888 * ˇ
2889 "});
2890 // Ensure that if cursor is before the comment start,
2891 // we do not actually insert a comment prefix.
2892 cx.set_state(indoc! {"
2893 ˇ/**
2894 "});
2895 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2896 cx.assert_editor_state(indoc! {"
2897
2898 ˇ/**
2899 "});
2900 // Ensure that if cursor is between it doesn't add comment prefix.
2901 cx.set_state(indoc! {"
2902 /*ˇ*
2903 "});
2904 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2905 cx.assert_editor_state(indoc! {"
2906 /*
2907 ˇ*
2908 "});
2909 // Ensure that if suffix exists on same line after cursor it adds new line.
2910 cx.set_state(indoc! {"
2911 /**ˇ*/
2912 "});
2913 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2914 cx.assert_editor_state(indoc! {"
2915 /**
2916 * ˇ
2917 */
2918 "});
2919 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2920 cx.set_state(indoc! {"
2921 /**ˇ */
2922 "});
2923 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2924 cx.assert_editor_state(indoc! {"
2925 /**
2926 * ˇ
2927 */
2928 "});
2929 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2930 cx.set_state(indoc! {"
2931 /** ˇ*/
2932 "});
2933 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2934 cx.assert_editor_state(
2935 indoc! {"
2936 /**s
2937 * ˇ
2938 */
2939 "}
2940 .replace("s", " ") // s is used as space placeholder to prevent format on save
2941 .as_str(),
2942 );
2943 // Ensure that delimiter space is preserved when newline on already
2944 // spaced delimiter.
2945 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2946 cx.assert_editor_state(
2947 indoc! {"
2948 /**s
2949 *s
2950 * ˇ
2951 */
2952 "}
2953 .replace("s", " ") // s is used as space placeholder to prevent format on save
2954 .as_str(),
2955 );
2956 // Ensure that delimiter space is preserved when space is not
2957 // on existing delimiter.
2958 cx.set_state(indoc! {"
2959 /**
2960 *ˇ
2961 */
2962 "});
2963 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2964 cx.assert_editor_state(indoc! {"
2965 /**
2966 *
2967 * ˇ
2968 */
2969 "});
2970 // Ensure that if suffix exists on same line after cursor it
2971 // doesn't add extra new line if prefix is not on same line.
2972 cx.set_state(indoc! {"
2973 /**
2974 ˇ*/
2975 "});
2976 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2977 cx.assert_editor_state(indoc! {"
2978 /**
2979
2980 ˇ*/
2981 "});
2982 // Ensure that it detects suffix after existing prefix.
2983 cx.set_state(indoc! {"
2984 /**ˇ/
2985 "});
2986 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2987 cx.assert_editor_state(indoc! {"
2988 /**
2989 ˇ/
2990 "});
2991 // Ensure that if suffix exists on same line before
2992 // cursor it does not add comment prefix.
2993 cx.set_state(indoc! {"
2994 /** */ˇ
2995 "});
2996 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2997 cx.assert_editor_state(indoc! {"
2998 /** */
2999 ˇ
3000 "});
3001 // Ensure that if suffix exists on same line before
3002 // cursor it does not add comment prefix.
3003 cx.set_state(indoc! {"
3004 /**
3005 *
3006 */ˇ
3007 "});
3008 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3009 cx.assert_editor_state(indoc! {"
3010 /**
3011 *
3012 */
3013 ˇ
3014 "});
3015
3016 // Ensure that inline comment followed by code
3017 // doesn't add comment prefix on newline
3018 cx.set_state(indoc! {"
3019 /** */ textˇ
3020 "});
3021 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3022 cx.assert_editor_state(indoc! {"
3023 /** */ text
3024 ˇ
3025 "});
3026
3027 // Ensure that text after comment end tag
3028 // doesn't add comment prefix on newline
3029 cx.set_state(indoc! {"
3030 /**
3031 *
3032 */ˇtext
3033 "});
3034 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3035 cx.assert_editor_state(indoc! {"
3036 /**
3037 *
3038 */
3039 ˇtext
3040 "});
3041 }
3042 // Ensure that comment continuations can be disabled.
3043 update_test_language_settings(cx, |settings| {
3044 settings.defaults.extend_comment_on_newline = Some(false);
3045 });
3046 let mut cx = EditorTestContext::new(cx).await;
3047 cx.set_state(indoc! {"
3048 /**ˇ
3049 "});
3050 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3051 cx.assert_editor_state(indoc! {"
3052 /**
3053 ˇ
3054 "});
3055}
3056
3057#[gpui::test]
3058fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3059 init_test(cx, |_| {});
3060
3061 let editor = cx.add_window(|window, cx| {
3062 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3063 let mut editor = build_editor(buffer.clone(), window, cx);
3064 editor.change_selections(None, window, cx, |s| {
3065 s.select_ranges([3..4, 11..12, 19..20])
3066 });
3067 editor
3068 });
3069
3070 _ = editor.update(cx, |editor, window, cx| {
3071 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3072 editor.buffer.update(cx, |buffer, cx| {
3073 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3074 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3075 });
3076 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3077
3078 editor.insert("Z", window, cx);
3079 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3080
3081 // The selections are moved after the inserted characters
3082 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3083 });
3084}
3085
3086#[gpui::test]
3087async fn test_tab(cx: &mut TestAppContext) {
3088 init_test(cx, |settings| {
3089 settings.defaults.tab_size = NonZeroU32::new(3)
3090 });
3091
3092 let mut cx = EditorTestContext::new(cx).await;
3093 cx.set_state(indoc! {"
3094 ˇabˇc
3095 ˇ🏀ˇ🏀ˇefg
3096 dˇ
3097 "});
3098 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3099 cx.assert_editor_state(indoc! {"
3100 ˇab ˇc
3101 ˇ🏀 ˇ🏀 ˇefg
3102 d ˇ
3103 "});
3104
3105 cx.set_state(indoc! {"
3106 a
3107 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3108 "});
3109 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3110 cx.assert_editor_state(indoc! {"
3111 a
3112 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3113 "});
3114}
3115
3116#[gpui::test]
3117async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3118 init_test(cx, |_| {});
3119
3120 let mut cx = EditorTestContext::new(cx).await;
3121 let language = Arc::new(
3122 Language::new(
3123 LanguageConfig::default(),
3124 Some(tree_sitter_rust::LANGUAGE.into()),
3125 )
3126 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3127 .unwrap(),
3128 );
3129 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3130
3131 // test when all cursors are not at suggested indent
3132 // then simply move to their suggested indent location
3133 cx.set_state(indoc! {"
3134 const a: B = (
3135 c(
3136 ˇ
3137 ˇ )
3138 );
3139 "});
3140 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3141 cx.assert_editor_state(indoc! {"
3142 const a: B = (
3143 c(
3144 ˇ
3145 ˇ)
3146 );
3147 "});
3148
3149 // test cursor already at suggested indent not moving when
3150 // other cursors are yet to reach their suggested indents
3151 cx.set_state(indoc! {"
3152 ˇ
3153 const a: B = (
3154 c(
3155 d(
3156 ˇ
3157 )
3158 ˇ
3159 ˇ )
3160 );
3161 "});
3162 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3163 cx.assert_editor_state(indoc! {"
3164 ˇ
3165 const a: B = (
3166 c(
3167 d(
3168 ˇ
3169 )
3170 ˇ
3171 ˇ)
3172 );
3173 "});
3174 // test when all cursors are at suggested indent then tab is inserted
3175 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3176 cx.assert_editor_state(indoc! {"
3177 ˇ
3178 const a: B = (
3179 c(
3180 d(
3181 ˇ
3182 )
3183 ˇ
3184 ˇ)
3185 );
3186 "});
3187
3188 // test when current indent is less than suggested indent,
3189 // we adjust line to match suggested indent and move cursor to it
3190 //
3191 // when no other cursor is at word boundary, all of them should move
3192 cx.set_state(indoc! {"
3193 const a: B = (
3194 c(
3195 d(
3196 ˇ
3197 ˇ )
3198 ˇ )
3199 );
3200 "});
3201 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3202 cx.assert_editor_state(indoc! {"
3203 const a: B = (
3204 c(
3205 d(
3206 ˇ
3207 ˇ)
3208 ˇ)
3209 );
3210 "});
3211
3212 // test when current indent is less than suggested indent,
3213 // we adjust line to match suggested indent and move cursor to it
3214 //
3215 // when some other cursor is at word boundary, it should not move
3216 cx.set_state(indoc! {"
3217 const a: B = (
3218 c(
3219 d(
3220 ˇ
3221 ˇ )
3222 ˇ)
3223 );
3224 "});
3225 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3226 cx.assert_editor_state(indoc! {"
3227 const a: B = (
3228 c(
3229 d(
3230 ˇ
3231 ˇ)
3232 ˇ)
3233 );
3234 "});
3235
3236 // test when current indent is more than suggested indent,
3237 // we just move cursor to current indent instead of suggested indent
3238 //
3239 // when no other cursor is at word boundary, all of them should move
3240 cx.set_state(indoc! {"
3241 const a: B = (
3242 c(
3243 d(
3244 ˇ
3245 ˇ )
3246 ˇ )
3247 );
3248 "});
3249 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3250 cx.assert_editor_state(indoc! {"
3251 const a: B = (
3252 c(
3253 d(
3254 ˇ
3255 ˇ)
3256 ˇ)
3257 );
3258 "});
3259 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3260 cx.assert_editor_state(indoc! {"
3261 const a: B = (
3262 c(
3263 d(
3264 ˇ
3265 ˇ)
3266 ˇ)
3267 );
3268 "});
3269
3270 // test when current indent is more than suggested indent,
3271 // we just move cursor to current indent instead of suggested indent
3272 //
3273 // when some other cursor is at word boundary, it doesn't move
3274 cx.set_state(indoc! {"
3275 const a: B = (
3276 c(
3277 d(
3278 ˇ
3279 ˇ )
3280 ˇ)
3281 );
3282 "});
3283 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3284 cx.assert_editor_state(indoc! {"
3285 const a: B = (
3286 c(
3287 d(
3288 ˇ
3289 ˇ)
3290 ˇ)
3291 );
3292 "});
3293
3294 // handle auto-indent when there are multiple cursors on the same line
3295 cx.set_state(indoc! {"
3296 const a: B = (
3297 c(
3298 ˇ ˇ
3299 ˇ )
3300 );
3301 "});
3302 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3303 cx.assert_editor_state(indoc! {"
3304 const a: B = (
3305 c(
3306 ˇ
3307 ˇ)
3308 );
3309 "});
3310}
3311
3312#[gpui::test]
3313async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3314 init_test(cx, |settings| {
3315 settings.defaults.tab_size = NonZeroU32::new(3)
3316 });
3317
3318 let mut cx = EditorTestContext::new(cx).await;
3319 cx.set_state(indoc! {"
3320 ˇ
3321 \t ˇ
3322 \t ˇ
3323 \t ˇ
3324 \t \t\t \t \t\t \t\t \t \t ˇ
3325 "});
3326
3327 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3328 cx.assert_editor_state(indoc! {"
3329 ˇ
3330 \t ˇ
3331 \t ˇ
3332 \t ˇ
3333 \t \t\t \t \t\t \t\t \t \t ˇ
3334 "});
3335}
3336
3337#[gpui::test]
3338async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3339 init_test(cx, |settings| {
3340 settings.defaults.tab_size = NonZeroU32::new(4)
3341 });
3342
3343 let language = Arc::new(
3344 Language::new(
3345 LanguageConfig::default(),
3346 Some(tree_sitter_rust::LANGUAGE.into()),
3347 )
3348 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3349 .unwrap(),
3350 );
3351
3352 let mut cx = EditorTestContext::new(cx).await;
3353 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3354 cx.set_state(indoc! {"
3355 fn a() {
3356 if b {
3357 \t ˇc
3358 }
3359 }
3360 "});
3361
3362 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3363 cx.assert_editor_state(indoc! {"
3364 fn a() {
3365 if b {
3366 ˇc
3367 }
3368 }
3369 "});
3370}
3371
3372#[gpui::test]
3373async fn test_indent_outdent(cx: &mut TestAppContext) {
3374 init_test(cx, |settings| {
3375 settings.defaults.tab_size = NonZeroU32::new(4);
3376 });
3377
3378 let mut cx = EditorTestContext::new(cx).await;
3379
3380 cx.set_state(indoc! {"
3381 «oneˇ» «twoˇ»
3382 three
3383 four
3384 "});
3385 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3386 cx.assert_editor_state(indoc! {"
3387 «oneˇ» «twoˇ»
3388 three
3389 four
3390 "});
3391
3392 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3393 cx.assert_editor_state(indoc! {"
3394 «oneˇ» «twoˇ»
3395 three
3396 four
3397 "});
3398
3399 // select across line ending
3400 cx.set_state(indoc! {"
3401 one two
3402 t«hree
3403 ˇ» four
3404 "});
3405 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3406 cx.assert_editor_state(indoc! {"
3407 one two
3408 t«hree
3409 ˇ» four
3410 "});
3411
3412 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3413 cx.assert_editor_state(indoc! {"
3414 one two
3415 t«hree
3416 ˇ» four
3417 "});
3418
3419 // Ensure that indenting/outdenting works when the cursor is at column 0.
3420 cx.set_state(indoc! {"
3421 one two
3422 ˇthree
3423 four
3424 "});
3425 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3426 cx.assert_editor_state(indoc! {"
3427 one two
3428 ˇthree
3429 four
3430 "});
3431
3432 cx.set_state(indoc! {"
3433 one two
3434 ˇ three
3435 four
3436 "});
3437 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3438 cx.assert_editor_state(indoc! {"
3439 one two
3440 ˇthree
3441 four
3442 "});
3443}
3444
3445#[gpui::test]
3446async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3447 init_test(cx, |settings| {
3448 settings.defaults.hard_tabs = Some(true);
3449 });
3450
3451 let mut cx = EditorTestContext::new(cx).await;
3452
3453 // select two ranges on one line
3454 cx.set_state(indoc! {"
3455 «oneˇ» «twoˇ»
3456 three
3457 four
3458 "});
3459 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3460 cx.assert_editor_state(indoc! {"
3461 \t«oneˇ» «twoˇ»
3462 three
3463 four
3464 "});
3465 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3466 cx.assert_editor_state(indoc! {"
3467 \t\t«oneˇ» «twoˇ»
3468 three
3469 four
3470 "});
3471 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3472 cx.assert_editor_state(indoc! {"
3473 \t«oneˇ» «twoˇ»
3474 three
3475 four
3476 "});
3477 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3478 cx.assert_editor_state(indoc! {"
3479 «oneˇ» «twoˇ»
3480 three
3481 four
3482 "});
3483
3484 // select across a line ending
3485 cx.set_state(indoc! {"
3486 one two
3487 t«hree
3488 ˇ»four
3489 "});
3490 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3491 cx.assert_editor_state(indoc! {"
3492 one two
3493 \tt«hree
3494 ˇ»four
3495 "});
3496 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3497 cx.assert_editor_state(indoc! {"
3498 one two
3499 \t\tt«hree
3500 ˇ»four
3501 "});
3502 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3503 cx.assert_editor_state(indoc! {"
3504 one two
3505 \tt«hree
3506 ˇ»four
3507 "});
3508 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3509 cx.assert_editor_state(indoc! {"
3510 one two
3511 t«hree
3512 ˇ»four
3513 "});
3514
3515 // Ensure that indenting/outdenting works when the cursor is at column 0.
3516 cx.set_state(indoc! {"
3517 one two
3518 ˇthree
3519 four
3520 "});
3521 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3522 cx.assert_editor_state(indoc! {"
3523 one two
3524 ˇthree
3525 four
3526 "});
3527 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3528 cx.assert_editor_state(indoc! {"
3529 one two
3530 \tˇthree
3531 four
3532 "});
3533 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3534 cx.assert_editor_state(indoc! {"
3535 one two
3536 ˇthree
3537 four
3538 "});
3539}
3540
3541#[gpui::test]
3542fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3543 init_test(cx, |settings| {
3544 settings.languages.extend([
3545 (
3546 "TOML".into(),
3547 LanguageSettingsContent {
3548 tab_size: NonZeroU32::new(2),
3549 ..Default::default()
3550 },
3551 ),
3552 (
3553 "Rust".into(),
3554 LanguageSettingsContent {
3555 tab_size: NonZeroU32::new(4),
3556 ..Default::default()
3557 },
3558 ),
3559 ]);
3560 });
3561
3562 let toml_language = Arc::new(Language::new(
3563 LanguageConfig {
3564 name: "TOML".into(),
3565 ..Default::default()
3566 },
3567 None,
3568 ));
3569 let rust_language = Arc::new(Language::new(
3570 LanguageConfig {
3571 name: "Rust".into(),
3572 ..Default::default()
3573 },
3574 None,
3575 ));
3576
3577 let toml_buffer =
3578 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3579 let rust_buffer =
3580 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3581 let multibuffer = cx.new(|cx| {
3582 let mut multibuffer = MultiBuffer::new(ReadWrite);
3583 multibuffer.push_excerpts(
3584 toml_buffer.clone(),
3585 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3586 cx,
3587 );
3588 multibuffer.push_excerpts(
3589 rust_buffer.clone(),
3590 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3591 cx,
3592 );
3593 multibuffer
3594 });
3595
3596 cx.add_window(|window, cx| {
3597 let mut editor = build_editor(multibuffer, window, cx);
3598
3599 assert_eq!(
3600 editor.text(cx),
3601 indoc! {"
3602 a = 1
3603 b = 2
3604
3605 const c: usize = 3;
3606 "}
3607 );
3608
3609 select_ranges(
3610 &mut editor,
3611 indoc! {"
3612 «aˇ» = 1
3613 b = 2
3614
3615 «const c:ˇ» usize = 3;
3616 "},
3617 window,
3618 cx,
3619 );
3620
3621 editor.tab(&Tab, window, cx);
3622 assert_text_with_selections(
3623 &mut editor,
3624 indoc! {"
3625 «aˇ» = 1
3626 b = 2
3627
3628 «const c:ˇ» usize = 3;
3629 "},
3630 cx,
3631 );
3632 editor.backtab(&Backtab, window, cx);
3633 assert_text_with_selections(
3634 &mut editor,
3635 indoc! {"
3636 «aˇ» = 1
3637 b = 2
3638
3639 «const c:ˇ» usize = 3;
3640 "},
3641 cx,
3642 );
3643
3644 editor
3645 });
3646}
3647
3648#[gpui::test]
3649async fn test_backspace(cx: &mut TestAppContext) {
3650 init_test(cx, |_| {});
3651
3652 let mut cx = EditorTestContext::new(cx).await;
3653
3654 // Basic backspace
3655 cx.set_state(indoc! {"
3656 onˇe two three
3657 fou«rˇ» five six
3658 seven «ˇeight nine
3659 »ten
3660 "});
3661 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3662 cx.assert_editor_state(indoc! {"
3663 oˇe two three
3664 fouˇ five six
3665 seven ˇten
3666 "});
3667
3668 // Test backspace inside and around indents
3669 cx.set_state(indoc! {"
3670 zero
3671 ˇone
3672 ˇtwo
3673 ˇ ˇ ˇ three
3674 ˇ ˇ four
3675 "});
3676 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3677 cx.assert_editor_state(indoc! {"
3678 zero
3679 ˇone
3680 ˇtwo
3681 ˇ threeˇ four
3682 "});
3683}
3684
3685#[gpui::test]
3686async fn test_delete(cx: &mut TestAppContext) {
3687 init_test(cx, |_| {});
3688
3689 let mut cx = EditorTestContext::new(cx).await;
3690 cx.set_state(indoc! {"
3691 onˇe two three
3692 fou«rˇ» five six
3693 seven «ˇeight nine
3694 »ten
3695 "});
3696 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3697 cx.assert_editor_state(indoc! {"
3698 onˇ two three
3699 fouˇ five six
3700 seven ˇten
3701 "});
3702}
3703
3704#[gpui::test]
3705fn test_delete_line(cx: &mut TestAppContext) {
3706 init_test(cx, |_| {});
3707
3708 let editor = cx.add_window(|window, cx| {
3709 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3710 build_editor(buffer, window, cx)
3711 });
3712 _ = editor.update(cx, |editor, window, cx| {
3713 editor.change_selections(None, window, cx, |s| {
3714 s.select_display_ranges([
3715 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3716 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3717 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3718 ])
3719 });
3720 editor.delete_line(&DeleteLine, window, cx);
3721 assert_eq!(editor.display_text(cx), "ghi");
3722 assert_eq!(
3723 editor.selections.display_ranges(cx),
3724 vec![
3725 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3726 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3727 ]
3728 );
3729 });
3730
3731 let editor = cx.add_window(|window, cx| {
3732 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3733 build_editor(buffer, window, cx)
3734 });
3735 _ = editor.update(cx, |editor, window, cx| {
3736 editor.change_selections(None, window, cx, |s| {
3737 s.select_display_ranges([
3738 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3739 ])
3740 });
3741 editor.delete_line(&DeleteLine, window, cx);
3742 assert_eq!(editor.display_text(cx), "ghi\n");
3743 assert_eq!(
3744 editor.selections.display_ranges(cx),
3745 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3746 );
3747 });
3748}
3749
3750#[gpui::test]
3751fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3752 init_test(cx, |_| {});
3753
3754 cx.add_window(|window, cx| {
3755 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3756 let mut editor = build_editor(buffer.clone(), window, cx);
3757 let buffer = buffer.read(cx).as_singleton().unwrap();
3758
3759 assert_eq!(
3760 editor.selections.ranges::<Point>(cx),
3761 &[Point::new(0, 0)..Point::new(0, 0)]
3762 );
3763
3764 // When on single line, replace newline at end by space
3765 editor.join_lines(&JoinLines, window, cx);
3766 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3767 assert_eq!(
3768 editor.selections.ranges::<Point>(cx),
3769 &[Point::new(0, 3)..Point::new(0, 3)]
3770 );
3771
3772 // When multiple lines are selected, remove newlines that are spanned by the selection
3773 editor.change_selections(None, window, cx, |s| {
3774 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3775 });
3776 editor.join_lines(&JoinLines, window, cx);
3777 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3778 assert_eq!(
3779 editor.selections.ranges::<Point>(cx),
3780 &[Point::new(0, 11)..Point::new(0, 11)]
3781 );
3782
3783 // Undo should be transactional
3784 editor.undo(&Undo, window, cx);
3785 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3786 assert_eq!(
3787 editor.selections.ranges::<Point>(cx),
3788 &[Point::new(0, 5)..Point::new(2, 2)]
3789 );
3790
3791 // When joining an empty line don't insert a space
3792 editor.change_selections(None, window, cx, |s| {
3793 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3794 });
3795 editor.join_lines(&JoinLines, window, cx);
3796 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3797 assert_eq!(
3798 editor.selections.ranges::<Point>(cx),
3799 [Point::new(2, 3)..Point::new(2, 3)]
3800 );
3801
3802 // We can remove trailing newlines
3803 editor.join_lines(&JoinLines, window, cx);
3804 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3805 assert_eq!(
3806 editor.selections.ranges::<Point>(cx),
3807 [Point::new(2, 3)..Point::new(2, 3)]
3808 );
3809
3810 // We don't blow up on the last line
3811 editor.join_lines(&JoinLines, window, cx);
3812 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3813 assert_eq!(
3814 editor.selections.ranges::<Point>(cx),
3815 [Point::new(2, 3)..Point::new(2, 3)]
3816 );
3817
3818 // reset to test indentation
3819 editor.buffer.update(cx, |buffer, cx| {
3820 buffer.edit(
3821 [
3822 (Point::new(1, 0)..Point::new(1, 2), " "),
3823 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3824 ],
3825 None,
3826 cx,
3827 )
3828 });
3829
3830 // We remove any leading spaces
3831 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3832 editor.change_selections(None, window, cx, |s| {
3833 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3834 });
3835 editor.join_lines(&JoinLines, window, cx);
3836 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3837
3838 // We don't insert a space for a line containing only spaces
3839 editor.join_lines(&JoinLines, window, cx);
3840 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3841
3842 // We ignore any leading tabs
3843 editor.join_lines(&JoinLines, window, cx);
3844 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3845
3846 editor
3847 });
3848}
3849
3850#[gpui::test]
3851fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3852 init_test(cx, |_| {});
3853
3854 cx.add_window(|window, cx| {
3855 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3856 let mut editor = build_editor(buffer.clone(), window, cx);
3857 let buffer = buffer.read(cx).as_singleton().unwrap();
3858
3859 editor.change_selections(None, window, cx, |s| {
3860 s.select_ranges([
3861 Point::new(0, 2)..Point::new(1, 1),
3862 Point::new(1, 2)..Point::new(1, 2),
3863 Point::new(3, 1)..Point::new(3, 2),
3864 ])
3865 });
3866
3867 editor.join_lines(&JoinLines, window, cx);
3868 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3869
3870 assert_eq!(
3871 editor.selections.ranges::<Point>(cx),
3872 [
3873 Point::new(0, 7)..Point::new(0, 7),
3874 Point::new(1, 3)..Point::new(1, 3)
3875 ]
3876 );
3877 editor
3878 });
3879}
3880
3881#[gpui::test]
3882async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3883 init_test(cx, |_| {});
3884
3885 let mut cx = EditorTestContext::new(cx).await;
3886
3887 let diff_base = r#"
3888 Line 0
3889 Line 1
3890 Line 2
3891 Line 3
3892 "#
3893 .unindent();
3894
3895 cx.set_state(
3896 &r#"
3897 ˇLine 0
3898 Line 1
3899 Line 2
3900 Line 3
3901 "#
3902 .unindent(),
3903 );
3904
3905 cx.set_head_text(&diff_base);
3906 executor.run_until_parked();
3907
3908 // Join lines
3909 cx.update_editor(|editor, window, cx| {
3910 editor.join_lines(&JoinLines, window, cx);
3911 });
3912 executor.run_until_parked();
3913
3914 cx.assert_editor_state(
3915 &r#"
3916 Line 0ˇ Line 1
3917 Line 2
3918 Line 3
3919 "#
3920 .unindent(),
3921 );
3922 // Join again
3923 cx.update_editor(|editor, window, cx| {
3924 editor.join_lines(&JoinLines, window, cx);
3925 });
3926 executor.run_until_parked();
3927
3928 cx.assert_editor_state(
3929 &r#"
3930 Line 0 Line 1ˇ Line 2
3931 Line 3
3932 "#
3933 .unindent(),
3934 );
3935}
3936
3937#[gpui::test]
3938async fn test_custom_newlines_cause_no_false_positive_diffs(
3939 executor: BackgroundExecutor,
3940 cx: &mut TestAppContext,
3941) {
3942 init_test(cx, |_| {});
3943 let mut cx = EditorTestContext::new(cx).await;
3944 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3945 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3946 executor.run_until_parked();
3947
3948 cx.update_editor(|editor, window, cx| {
3949 let snapshot = editor.snapshot(window, cx);
3950 assert_eq!(
3951 snapshot
3952 .buffer_snapshot
3953 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3954 .collect::<Vec<_>>(),
3955 Vec::new(),
3956 "Should not have any diffs for files with custom newlines"
3957 );
3958 });
3959}
3960
3961#[gpui::test]
3962async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3963 init_test(cx, |_| {});
3964
3965 let mut cx = EditorTestContext::new(cx).await;
3966
3967 // Test sort_lines_case_insensitive()
3968 cx.set_state(indoc! {"
3969 «z
3970 y
3971 x
3972 Z
3973 Y
3974 Xˇ»
3975 "});
3976 cx.update_editor(|e, window, cx| {
3977 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3978 });
3979 cx.assert_editor_state(indoc! {"
3980 «x
3981 X
3982 y
3983 Y
3984 z
3985 Zˇ»
3986 "});
3987
3988 // Test reverse_lines()
3989 cx.set_state(indoc! {"
3990 «5
3991 4
3992 3
3993 2
3994 1ˇ»
3995 "});
3996 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3997 cx.assert_editor_state(indoc! {"
3998 «1
3999 2
4000 3
4001 4
4002 5ˇ»
4003 "});
4004
4005 // Skip testing shuffle_line()
4006
4007 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
4008 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
4009
4010 // Don't manipulate when cursor is on single line, but expand the selection
4011 cx.set_state(indoc! {"
4012 ddˇdd
4013 ccc
4014 bb
4015 a
4016 "});
4017 cx.update_editor(|e, window, cx| {
4018 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4019 });
4020 cx.assert_editor_state(indoc! {"
4021 «ddddˇ»
4022 ccc
4023 bb
4024 a
4025 "});
4026
4027 // Basic manipulate case
4028 // Start selection moves to column 0
4029 // End of selection shrinks to fit shorter line
4030 cx.set_state(indoc! {"
4031 dd«d
4032 ccc
4033 bb
4034 aaaaaˇ»
4035 "});
4036 cx.update_editor(|e, window, cx| {
4037 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4038 });
4039 cx.assert_editor_state(indoc! {"
4040 «aaaaa
4041 bb
4042 ccc
4043 dddˇ»
4044 "});
4045
4046 // Manipulate case with newlines
4047 cx.set_state(indoc! {"
4048 dd«d
4049 ccc
4050
4051 bb
4052 aaaaa
4053
4054 ˇ»
4055 "});
4056 cx.update_editor(|e, window, cx| {
4057 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4058 });
4059 cx.assert_editor_state(indoc! {"
4060 «
4061
4062 aaaaa
4063 bb
4064 ccc
4065 dddˇ»
4066
4067 "});
4068
4069 // Adding new line
4070 cx.set_state(indoc! {"
4071 aa«a
4072 bbˇ»b
4073 "});
4074 cx.update_editor(|e, window, cx| {
4075 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4076 });
4077 cx.assert_editor_state(indoc! {"
4078 «aaa
4079 bbb
4080 added_lineˇ»
4081 "});
4082
4083 // Removing line
4084 cx.set_state(indoc! {"
4085 aa«a
4086 bbbˇ»
4087 "});
4088 cx.update_editor(|e, window, cx| {
4089 e.manipulate_lines(window, cx, |lines| {
4090 lines.pop();
4091 })
4092 });
4093 cx.assert_editor_state(indoc! {"
4094 «aaaˇ»
4095 "});
4096
4097 // Removing all lines
4098 cx.set_state(indoc! {"
4099 aa«a
4100 bbbˇ»
4101 "});
4102 cx.update_editor(|e, window, cx| {
4103 e.manipulate_lines(window, cx, |lines| {
4104 lines.drain(..);
4105 })
4106 });
4107 cx.assert_editor_state(indoc! {"
4108 ˇ
4109 "});
4110}
4111
4112#[gpui::test]
4113async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4114 init_test(cx, |_| {});
4115
4116 let mut cx = EditorTestContext::new(cx).await;
4117
4118 // Consider continuous selection as single selection
4119 cx.set_state(indoc! {"
4120 Aaa«aa
4121 cˇ»c«c
4122 bb
4123 aaaˇ»aa
4124 "});
4125 cx.update_editor(|e, window, cx| {
4126 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4127 });
4128 cx.assert_editor_state(indoc! {"
4129 «Aaaaa
4130 ccc
4131 bb
4132 aaaaaˇ»
4133 "});
4134
4135 cx.set_state(indoc! {"
4136 Aaa«aa
4137 cˇ»c«c
4138 bb
4139 aaaˇ»aa
4140 "});
4141 cx.update_editor(|e, window, cx| {
4142 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4143 });
4144 cx.assert_editor_state(indoc! {"
4145 «Aaaaa
4146 ccc
4147 bbˇ»
4148 "});
4149
4150 // Consider non continuous selection as distinct dedup operations
4151 cx.set_state(indoc! {"
4152 «aaaaa
4153 bb
4154 aaaaa
4155 aaaaaˇ»
4156
4157 aaa«aaˇ»
4158 "});
4159 cx.update_editor(|e, window, cx| {
4160 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4161 });
4162 cx.assert_editor_state(indoc! {"
4163 «aaaaa
4164 bbˇ»
4165
4166 «aaaaaˇ»
4167 "});
4168}
4169
4170#[gpui::test]
4171async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4172 init_test(cx, |_| {});
4173
4174 let mut cx = EditorTestContext::new(cx).await;
4175
4176 cx.set_state(indoc! {"
4177 «Aaa
4178 aAa
4179 Aaaˇ»
4180 "});
4181 cx.update_editor(|e, window, cx| {
4182 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4183 });
4184 cx.assert_editor_state(indoc! {"
4185 «Aaa
4186 aAaˇ»
4187 "});
4188
4189 cx.set_state(indoc! {"
4190 «Aaa
4191 aAa
4192 aaAˇ»
4193 "});
4194 cx.update_editor(|e, window, cx| {
4195 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4196 });
4197 cx.assert_editor_state(indoc! {"
4198 «Aaaˇ»
4199 "});
4200}
4201
4202#[gpui::test]
4203async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4204 init_test(cx, |_| {});
4205
4206 let mut cx = EditorTestContext::new(cx).await;
4207
4208 // Manipulate with multiple selections on a single line
4209 cx.set_state(indoc! {"
4210 dd«dd
4211 cˇ»c«c
4212 bb
4213 aaaˇ»aa
4214 "});
4215 cx.update_editor(|e, window, cx| {
4216 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4217 });
4218 cx.assert_editor_state(indoc! {"
4219 «aaaaa
4220 bb
4221 ccc
4222 ddddˇ»
4223 "});
4224
4225 // Manipulate with multiple disjoin selections
4226 cx.set_state(indoc! {"
4227 5«
4228 4
4229 3
4230 2
4231 1ˇ»
4232
4233 dd«dd
4234 ccc
4235 bb
4236 aaaˇ»aa
4237 "});
4238 cx.update_editor(|e, window, cx| {
4239 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4240 });
4241 cx.assert_editor_state(indoc! {"
4242 «1
4243 2
4244 3
4245 4
4246 5ˇ»
4247
4248 «aaaaa
4249 bb
4250 ccc
4251 ddddˇ»
4252 "});
4253
4254 // Adding lines on each selection
4255 cx.set_state(indoc! {"
4256 2«
4257 1ˇ»
4258
4259 bb«bb
4260 aaaˇ»aa
4261 "});
4262 cx.update_editor(|e, window, cx| {
4263 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4264 });
4265 cx.assert_editor_state(indoc! {"
4266 «2
4267 1
4268 added lineˇ»
4269
4270 «bbbb
4271 aaaaa
4272 added lineˇ»
4273 "});
4274
4275 // Removing lines on each selection
4276 cx.set_state(indoc! {"
4277 2«
4278 1ˇ»
4279
4280 bb«bb
4281 aaaˇ»aa
4282 "});
4283 cx.update_editor(|e, window, cx| {
4284 e.manipulate_lines(window, cx, |lines| {
4285 lines.pop();
4286 })
4287 });
4288 cx.assert_editor_state(indoc! {"
4289 «2ˇ»
4290
4291 «bbbbˇ»
4292 "});
4293}
4294
4295#[gpui::test]
4296async fn test_toggle_case(cx: &mut TestAppContext) {
4297 init_test(cx, |_| {});
4298
4299 let mut cx = EditorTestContext::new(cx).await;
4300
4301 // If all lower case -> upper case
4302 cx.set_state(indoc! {"
4303 «hello worldˇ»
4304 "});
4305 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4306 cx.assert_editor_state(indoc! {"
4307 «HELLO WORLDˇ»
4308 "});
4309
4310 // If all upper case -> lower case
4311 cx.set_state(indoc! {"
4312 «HELLO WORLDˇ»
4313 "});
4314 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4315 cx.assert_editor_state(indoc! {"
4316 «hello worldˇ»
4317 "});
4318
4319 // If any upper case characters are identified -> lower case
4320 // This matches JetBrains IDEs
4321 cx.set_state(indoc! {"
4322 «hEllo worldˇ»
4323 "});
4324 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4325 cx.assert_editor_state(indoc! {"
4326 «hello worldˇ»
4327 "});
4328}
4329
4330#[gpui::test]
4331async fn test_manipulate_text(cx: &mut TestAppContext) {
4332 init_test(cx, |_| {});
4333
4334 let mut cx = EditorTestContext::new(cx).await;
4335
4336 // Test convert_to_upper_case()
4337 cx.set_state(indoc! {"
4338 «hello worldˇ»
4339 "});
4340 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4341 cx.assert_editor_state(indoc! {"
4342 «HELLO WORLDˇ»
4343 "});
4344
4345 // Test convert_to_lower_case()
4346 cx.set_state(indoc! {"
4347 «HELLO WORLDˇ»
4348 "});
4349 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4350 cx.assert_editor_state(indoc! {"
4351 «hello worldˇ»
4352 "});
4353
4354 // Test multiple line, single selection case
4355 cx.set_state(indoc! {"
4356 «The quick brown
4357 fox jumps over
4358 the lazy dogˇ»
4359 "});
4360 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4361 cx.assert_editor_state(indoc! {"
4362 «The Quick Brown
4363 Fox Jumps Over
4364 The Lazy Dogˇ»
4365 "});
4366
4367 // Test multiple line, single selection case
4368 cx.set_state(indoc! {"
4369 «The quick brown
4370 fox jumps over
4371 the lazy dogˇ»
4372 "});
4373 cx.update_editor(|e, window, cx| {
4374 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4375 });
4376 cx.assert_editor_state(indoc! {"
4377 «TheQuickBrown
4378 FoxJumpsOver
4379 TheLazyDogˇ»
4380 "});
4381
4382 // From here on out, test more complex cases of manipulate_text()
4383
4384 // Test no selection case - should affect words cursors are in
4385 // Cursor at beginning, middle, and end of word
4386 cx.set_state(indoc! {"
4387 ˇhello big beauˇtiful worldˇ
4388 "});
4389 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4390 cx.assert_editor_state(indoc! {"
4391 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4392 "});
4393
4394 // Test multiple selections on a single line and across multiple lines
4395 cx.set_state(indoc! {"
4396 «Theˇ» quick «brown
4397 foxˇ» jumps «overˇ»
4398 the «lazyˇ» dog
4399 "});
4400 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4401 cx.assert_editor_state(indoc! {"
4402 «THEˇ» quick «BROWN
4403 FOXˇ» jumps «OVERˇ»
4404 the «LAZYˇ» dog
4405 "});
4406
4407 // Test case where text length grows
4408 cx.set_state(indoc! {"
4409 «tschüߡ»
4410 "});
4411 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4412 cx.assert_editor_state(indoc! {"
4413 «TSCHÜSSˇ»
4414 "});
4415
4416 // Test to make sure we don't crash when text shrinks
4417 cx.set_state(indoc! {"
4418 aaa_bbbˇ
4419 "});
4420 cx.update_editor(|e, window, cx| {
4421 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4422 });
4423 cx.assert_editor_state(indoc! {"
4424 «aaaBbbˇ»
4425 "});
4426
4427 // Test to make sure we all aware of the fact that each word can grow and shrink
4428 // Final selections should be aware of this fact
4429 cx.set_state(indoc! {"
4430 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4431 "});
4432 cx.update_editor(|e, window, cx| {
4433 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4434 });
4435 cx.assert_editor_state(indoc! {"
4436 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4437 "});
4438
4439 cx.set_state(indoc! {"
4440 «hElLo, WoRld!ˇ»
4441 "});
4442 cx.update_editor(|e, window, cx| {
4443 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4444 });
4445 cx.assert_editor_state(indoc! {"
4446 «HeLlO, wOrLD!ˇ»
4447 "});
4448}
4449
4450#[gpui::test]
4451fn test_duplicate_line(cx: &mut TestAppContext) {
4452 init_test(cx, |_| {});
4453
4454 let editor = cx.add_window(|window, cx| {
4455 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4456 build_editor(buffer, window, cx)
4457 });
4458 _ = editor.update(cx, |editor, window, cx| {
4459 editor.change_selections(None, window, cx, |s| {
4460 s.select_display_ranges([
4461 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4462 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4463 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4464 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4465 ])
4466 });
4467 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4468 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4469 assert_eq!(
4470 editor.selections.display_ranges(cx),
4471 vec![
4472 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4473 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4474 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4475 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4476 ]
4477 );
4478 });
4479
4480 let editor = cx.add_window(|window, cx| {
4481 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4482 build_editor(buffer, window, cx)
4483 });
4484 _ = editor.update(cx, |editor, window, cx| {
4485 editor.change_selections(None, window, cx, |s| {
4486 s.select_display_ranges([
4487 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4488 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4489 ])
4490 });
4491 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4492 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4493 assert_eq!(
4494 editor.selections.display_ranges(cx),
4495 vec![
4496 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4497 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4498 ]
4499 );
4500 });
4501
4502 // With `move_upwards` the selections stay in place, except for
4503 // the lines inserted above them
4504 let editor = cx.add_window(|window, cx| {
4505 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4506 build_editor(buffer, window, cx)
4507 });
4508 _ = editor.update(cx, |editor, window, cx| {
4509 editor.change_selections(None, window, cx, |s| {
4510 s.select_display_ranges([
4511 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4512 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4513 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4514 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4515 ])
4516 });
4517 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4518 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4519 assert_eq!(
4520 editor.selections.display_ranges(cx),
4521 vec![
4522 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4523 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4524 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4525 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4526 ]
4527 );
4528 });
4529
4530 let editor = cx.add_window(|window, cx| {
4531 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4532 build_editor(buffer, window, cx)
4533 });
4534 _ = editor.update(cx, |editor, window, cx| {
4535 editor.change_selections(None, window, cx, |s| {
4536 s.select_display_ranges([
4537 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4538 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4539 ])
4540 });
4541 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4542 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4543 assert_eq!(
4544 editor.selections.display_ranges(cx),
4545 vec![
4546 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4547 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4548 ]
4549 );
4550 });
4551
4552 let editor = cx.add_window(|window, cx| {
4553 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4554 build_editor(buffer, window, cx)
4555 });
4556 _ = editor.update(cx, |editor, window, cx| {
4557 editor.change_selections(None, window, cx, |s| {
4558 s.select_display_ranges([
4559 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4560 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4561 ])
4562 });
4563 editor.duplicate_selection(&DuplicateSelection, window, cx);
4564 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4565 assert_eq!(
4566 editor.selections.display_ranges(cx),
4567 vec![
4568 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4569 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4570 ]
4571 );
4572 });
4573}
4574
4575#[gpui::test]
4576fn test_move_line_up_down(cx: &mut TestAppContext) {
4577 init_test(cx, |_| {});
4578
4579 let editor = cx.add_window(|window, cx| {
4580 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4581 build_editor(buffer, window, cx)
4582 });
4583 _ = editor.update(cx, |editor, window, cx| {
4584 editor.fold_creases(
4585 vec![
4586 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4587 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4588 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4589 ],
4590 true,
4591 window,
4592 cx,
4593 );
4594 editor.change_selections(None, window, cx, |s| {
4595 s.select_display_ranges([
4596 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4597 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4598 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4599 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4600 ])
4601 });
4602 assert_eq!(
4603 editor.display_text(cx),
4604 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4605 );
4606
4607 editor.move_line_up(&MoveLineUp, window, cx);
4608 assert_eq!(
4609 editor.display_text(cx),
4610 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4611 );
4612 assert_eq!(
4613 editor.selections.display_ranges(cx),
4614 vec![
4615 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4616 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4617 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4618 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4619 ]
4620 );
4621 });
4622
4623 _ = editor.update(cx, |editor, window, cx| {
4624 editor.move_line_down(&MoveLineDown, window, cx);
4625 assert_eq!(
4626 editor.display_text(cx),
4627 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4628 );
4629 assert_eq!(
4630 editor.selections.display_ranges(cx),
4631 vec![
4632 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4633 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4634 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4635 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 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\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4645 );
4646 assert_eq!(
4647 editor.selections.display_ranges(cx),
4648 vec![
4649 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 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_up(&MoveLineUp, window, cx);
4659 assert_eq!(
4660 editor.display_text(cx),
4661 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4662 );
4663 assert_eq!(
4664 editor.selections.display_ranges(cx),
4665 vec![
4666 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4667 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4668 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4669 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4670 ]
4671 );
4672 });
4673}
4674
4675#[gpui::test]
4676fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4677 init_test(cx, |_| {});
4678
4679 let editor = cx.add_window(|window, cx| {
4680 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4681 build_editor(buffer, window, cx)
4682 });
4683 _ = editor.update(cx, |editor, window, cx| {
4684 let snapshot = editor.buffer.read(cx).snapshot(cx);
4685 editor.insert_blocks(
4686 [BlockProperties {
4687 style: BlockStyle::Fixed,
4688 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4689 height: Some(1),
4690 render: Arc::new(|_| div().into_any()),
4691 priority: 0,
4692 render_in_minimap: true,
4693 }],
4694 Some(Autoscroll::fit()),
4695 cx,
4696 );
4697 editor.change_selections(None, window, cx, |s| {
4698 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4699 });
4700 editor.move_line_down(&MoveLineDown, window, cx);
4701 });
4702}
4703
4704#[gpui::test]
4705async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4706 init_test(cx, |_| {});
4707
4708 let mut cx = EditorTestContext::new(cx).await;
4709 cx.set_state(
4710 &"
4711 ˇzero
4712 one
4713 two
4714 three
4715 four
4716 five
4717 "
4718 .unindent(),
4719 );
4720
4721 // Create a four-line block that replaces three lines of text.
4722 cx.update_editor(|editor, window, cx| {
4723 let snapshot = editor.snapshot(window, cx);
4724 let snapshot = &snapshot.buffer_snapshot;
4725 let placement = BlockPlacement::Replace(
4726 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4727 );
4728 editor.insert_blocks(
4729 [BlockProperties {
4730 placement,
4731 height: Some(4),
4732 style: BlockStyle::Sticky,
4733 render: Arc::new(|_| gpui::div().into_any_element()),
4734 priority: 0,
4735 render_in_minimap: true,
4736 }],
4737 None,
4738 cx,
4739 );
4740 });
4741
4742 // Move down so that the cursor touches the block.
4743 cx.update_editor(|editor, window, cx| {
4744 editor.move_down(&Default::default(), window, cx);
4745 });
4746 cx.assert_editor_state(
4747 &"
4748 zero
4749 «one
4750 two
4751 threeˇ»
4752 four
4753 five
4754 "
4755 .unindent(),
4756 );
4757
4758 // Move down past the block.
4759 cx.update_editor(|editor, window, cx| {
4760 editor.move_down(&Default::default(), window, cx);
4761 });
4762 cx.assert_editor_state(
4763 &"
4764 zero
4765 one
4766 two
4767 three
4768 ˇfour
4769 five
4770 "
4771 .unindent(),
4772 );
4773}
4774
4775#[gpui::test]
4776fn test_transpose(cx: &mut TestAppContext) {
4777 init_test(cx, |_| {});
4778
4779 _ = cx.add_window(|window, cx| {
4780 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4781 editor.set_style(EditorStyle::default(), window, cx);
4782 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4783 editor.transpose(&Default::default(), window, cx);
4784 assert_eq!(editor.text(cx), "bac");
4785 assert_eq!(editor.selections.ranges(cx), [2..2]);
4786
4787 editor.transpose(&Default::default(), window, cx);
4788 assert_eq!(editor.text(cx), "bca");
4789 assert_eq!(editor.selections.ranges(cx), [3..3]);
4790
4791 editor.transpose(&Default::default(), window, cx);
4792 assert_eq!(editor.text(cx), "bac");
4793 assert_eq!(editor.selections.ranges(cx), [3..3]);
4794
4795 editor
4796 });
4797
4798 _ = cx.add_window(|window, cx| {
4799 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4800 editor.set_style(EditorStyle::default(), window, cx);
4801 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4802 editor.transpose(&Default::default(), window, cx);
4803 assert_eq!(editor.text(cx), "acb\nde");
4804 assert_eq!(editor.selections.ranges(cx), [3..3]);
4805
4806 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4807 editor.transpose(&Default::default(), window, cx);
4808 assert_eq!(editor.text(cx), "acbd\ne");
4809 assert_eq!(editor.selections.ranges(cx), [5..5]);
4810
4811 editor.transpose(&Default::default(), window, cx);
4812 assert_eq!(editor.text(cx), "acbde\n");
4813 assert_eq!(editor.selections.ranges(cx), [6..6]);
4814
4815 editor.transpose(&Default::default(), window, cx);
4816 assert_eq!(editor.text(cx), "acbd\ne");
4817 assert_eq!(editor.selections.ranges(cx), [6..6]);
4818
4819 editor
4820 });
4821
4822 _ = cx.add_window(|window, cx| {
4823 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4824 editor.set_style(EditorStyle::default(), window, cx);
4825 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4826 editor.transpose(&Default::default(), window, cx);
4827 assert_eq!(editor.text(cx), "bacd\ne");
4828 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4829
4830 editor.transpose(&Default::default(), window, cx);
4831 assert_eq!(editor.text(cx), "bcade\n");
4832 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4833
4834 editor.transpose(&Default::default(), window, cx);
4835 assert_eq!(editor.text(cx), "bcda\ne");
4836 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4837
4838 editor.transpose(&Default::default(), window, cx);
4839 assert_eq!(editor.text(cx), "bcade\n");
4840 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4841
4842 editor.transpose(&Default::default(), window, cx);
4843 assert_eq!(editor.text(cx), "bcaed\n");
4844 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4845
4846 editor
4847 });
4848
4849 _ = cx.add_window(|window, cx| {
4850 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4851 editor.set_style(EditorStyle::default(), window, cx);
4852 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4853 editor.transpose(&Default::default(), window, cx);
4854 assert_eq!(editor.text(cx), "🏀🍐✋");
4855 assert_eq!(editor.selections.ranges(cx), [8..8]);
4856
4857 editor.transpose(&Default::default(), window, cx);
4858 assert_eq!(editor.text(cx), "🏀✋🍐");
4859 assert_eq!(editor.selections.ranges(cx), [11..11]);
4860
4861 editor.transpose(&Default::default(), window, cx);
4862 assert_eq!(editor.text(cx), "🏀🍐✋");
4863 assert_eq!(editor.selections.ranges(cx), [11..11]);
4864
4865 editor
4866 });
4867}
4868
4869#[gpui::test]
4870async fn test_rewrap(cx: &mut TestAppContext) {
4871 init_test(cx, |settings| {
4872 settings.languages.extend([
4873 (
4874 "Markdown".into(),
4875 LanguageSettingsContent {
4876 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4877 ..Default::default()
4878 },
4879 ),
4880 (
4881 "Plain Text".into(),
4882 LanguageSettingsContent {
4883 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4884 ..Default::default()
4885 },
4886 ),
4887 ])
4888 });
4889
4890 let mut cx = EditorTestContext::new(cx).await;
4891
4892 let language_with_c_comments = Arc::new(Language::new(
4893 LanguageConfig {
4894 line_comments: vec!["// ".into()],
4895 ..LanguageConfig::default()
4896 },
4897 None,
4898 ));
4899 let language_with_pound_comments = Arc::new(Language::new(
4900 LanguageConfig {
4901 line_comments: vec!["# ".into()],
4902 ..LanguageConfig::default()
4903 },
4904 None,
4905 ));
4906 let markdown_language = Arc::new(Language::new(
4907 LanguageConfig {
4908 name: "Markdown".into(),
4909 ..LanguageConfig::default()
4910 },
4911 None,
4912 ));
4913 let language_with_doc_comments = Arc::new(Language::new(
4914 LanguageConfig {
4915 line_comments: vec!["// ".into(), "/// ".into()],
4916 ..LanguageConfig::default()
4917 },
4918 Some(tree_sitter_rust::LANGUAGE.into()),
4919 ));
4920
4921 let plaintext_language = Arc::new(Language::new(
4922 LanguageConfig {
4923 name: "Plain Text".into(),
4924 ..LanguageConfig::default()
4925 },
4926 None,
4927 ));
4928
4929 assert_rewrap(
4930 indoc! {"
4931 // ˇ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.
4932 "},
4933 indoc! {"
4934 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4935 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4936 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4937 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4938 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4939 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4940 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4941 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4942 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4943 // porttitor id. Aliquam id accumsan eros.
4944 "},
4945 language_with_c_comments.clone(),
4946 &mut cx,
4947 );
4948
4949 // Test that rewrapping works inside of a selection
4950 assert_rewrap(
4951 indoc! {"
4952 «// 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.ˇ»
4953 "},
4954 indoc! {"
4955 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4956 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4957 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4958 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4959 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4960 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4961 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4962 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4963 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4964 // porttitor id. Aliquam id accumsan eros.ˇ»
4965 "},
4966 language_with_c_comments.clone(),
4967 &mut cx,
4968 );
4969
4970 // Test that cursors that expand to the same region are collapsed.
4971 assert_rewrap(
4972 indoc! {"
4973 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4974 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4975 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4976 // ˇ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.
4977 "},
4978 indoc! {"
4979 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4980 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4981 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4982 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4983 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4984 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4985 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4986 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4987 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4988 // porttitor id. Aliquam id accumsan eros.
4989 "},
4990 language_with_c_comments.clone(),
4991 &mut cx,
4992 );
4993
4994 // Test that non-contiguous selections are treated separately.
4995 assert_rewrap(
4996 indoc! {"
4997 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4998 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4999 //
5000 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5001 // ˇ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.
5002 "},
5003 indoc! {"
5004 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5005 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5006 // auctor, eu lacinia sapien scelerisque.
5007 //
5008 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5009 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5010 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5011 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5012 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5013 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5014 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5015 "},
5016 language_with_c_comments.clone(),
5017 &mut cx,
5018 );
5019
5020 // Test that different comment prefixes are supported.
5021 assert_rewrap(
5022 indoc! {"
5023 # ˇ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.
5024 "},
5025 indoc! {"
5026 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5027 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5028 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5029 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5030 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5031 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5032 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5033 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5034 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5035 # accumsan eros.
5036 "},
5037 language_with_pound_comments.clone(),
5038 &mut cx,
5039 );
5040
5041 // Test that rewrapping is ignored outside of comments in most languages.
5042 assert_rewrap(
5043 indoc! {"
5044 /// Adds two numbers.
5045 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5046 fn add(a: u32, b: u32) -> u32 {
5047 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ˇ
5048 }
5049 "},
5050 indoc! {"
5051 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5052 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5053 fn add(a: u32, b: u32) -> u32 {
5054 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ˇ
5055 }
5056 "},
5057 language_with_doc_comments.clone(),
5058 &mut cx,
5059 );
5060
5061 // Test that rewrapping works in Markdown and Plain Text languages.
5062 assert_rewrap(
5063 indoc! {"
5064 # Hello
5065
5066 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.
5067 "},
5068 indoc! {"
5069 # Hello
5070
5071 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5072 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5073 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5074 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5075 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5076 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5077 Integer sit amet scelerisque nisi.
5078 "},
5079 markdown_language,
5080 &mut cx,
5081 );
5082
5083 assert_rewrap(
5084 indoc! {"
5085 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.
5086 "},
5087 indoc! {"
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 plaintext_language,
5097 &mut cx,
5098 );
5099
5100 // Test rewrapping unaligned comments in a selection.
5101 assert_rewrap(
5102 indoc! {"
5103 fn foo() {
5104 if true {
5105 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5106 // Praesent semper egestas tellus id dignissim.ˇ»
5107 do_something();
5108 } else {
5109 //
5110 }
5111 }
5112 "},
5113 indoc! {"
5114 fn foo() {
5115 if true {
5116 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5117 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5118 // egestas tellus id dignissim.ˇ»
5119 do_something();
5120 } else {
5121 //
5122 }
5123 }
5124 "},
5125 language_with_doc_comments.clone(),
5126 &mut cx,
5127 );
5128
5129 assert_rewrap(
5130 indoc! {"
5131 fn foo() {
5132 if true {
5133 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5134 // Praesent semper egestas tellus id dignissim.»
5135 do_something();
5136 } else {
5137 //
5138 }
5139
5140 }
5141 "},
5142 indoc! {"
5143 fn foo() {
5144 if true {
5145 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5146 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5147 // egestas tellus id dignissim.»
5148 do_something();
5149 } else {
5150 //
5151 }
5152
5153 }
5154 "},
5155 language_with_doc_comments.clone(),
5156 &mut cx,
5157 );
5158
5159 #[track_caller]
5160 fn assert_rewrap(
5161 unwrapped_text: &str,
5162 wrapped_text: &str,
5163 language: Arc<Language>,
5164 cx: &mut EditorTestContext,
5165 ) {
5166 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5167 cx.set_state(unwrapped_text);
5168 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5169 cx.assert_editor_state(wrapped_text);
5170 }
5171}
5172
5173#[gpui::test]
5174async fn test_hard_wrap(cx: &mut TestAppContext) {
5175 init_test(cx, |_| {});
5176 let mut cx = EditorTestContext::new(cx).await;
5177
5178 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5179 cx.update_editor(|editor, _, cx| {
5180 editor.set_hard_wrap(Some(14), cx);
5181 });
5182
5183 cx.set_state(indoc!(
5184 "
5185 one two three ˇ
5186 "
5187 ));
5188 cx.simulate_input("four");
5189 cx.run_until_parked();
5190
5191 cx.assert_editor_state(indoc!(
5192 "
5193 one two three
5194 fourˇ
5195 "
5196 ));
5197
5198 cx.update_editor(|editor, window, cx| {
5199 editor.newline(&Default::default(), window, cx);
5200 });
5201 cx.run_until_parked();
5202 cx.assert_editor_state(indoc!(
5203 "
5204 one two three
5205 four
5206 ˇ
5207 "
5208 ));
5209
5210 cx.simulate_input("five");
5211 cx.run_until_parked();
5212 cx.assert_editor_state(indoc!(
5213 "
5214 one two three
5215 four
5216 fiveˇ
5217 "
5218 ));
5219
5220 cx.update_editor(|editor, window, cx| {
5221 editor.newline(&Default::default(), window, cx);
5222 });
5223 cx.run_until_parked();
5224 cx.simulate_input("# ");
5225 cx.run_until_parked();
5226 cx.assert_editor_state(indoc!(
5227 "
5228 one two three
5229 four
5230 five
5231 # ˇ
5232 "
5233 ));
5234
5235 cx.update_editor(|editor, window, cx| {
5236 editor.newline(&Default::default(), window, cx);
5237 });
5238 cx.run_until_parked();
5239 cx.assert_editor_state(indoc!(
5240 "
5241 one two three
5242 four
5243 five
5244 #\x20
5245 #ˇ
5246 "
5247 ));
5248
5249 cx.simulate_input(" 6");
5250 cx.run_until_parked();
5251 cx.assert_editor_state(indoc!(
5252 "
5253 one two three
5254 four
5255 five
5256 #
5257 # 6ˇ
5258 "
5259 ));
5260}
5261
5262#[gpui::test]
5263async fn test_clipboard(cx: &mut TestAppContext) {
5264 init_test(cx, |_| {});
5265
5266 let mut cx = EditorTestContext::new(cx).await;
5267
5268 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5269 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5270 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5271
5272 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5273 cx.set_state("two ˇfour ˇsix ˇ");
5274 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5275 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5276
5277 // Paste again but with only two cursors. Since the number of cursors doesn't
5278 // match the number of slices in the clipboard, the entire clipboard text
5279 // is pasted at each cursor.
5280 cx.set_state("ˇtwo one✅ four three six five ˇ");
5281 cx.update_editor(|e, window, cx| {
5282 e.handle_input("( ", window, cx);
5283 e.paste(&Paste, window, cx);
5284 e.handle_input(") ", window, cx);
5285 });
5286 cx.assert_editor_state(
5287 &([
5288 "( one✅ ",
5289 "three ",
5290 "five ) ˇtwo one✅ four three six five ( one✅ ",
5291 "three ",
5292 "five ) ˇ",
5293 ]
5294 .join("\n")),
5295 );
5296
5297 // Cut with three selections, one of which is full-line.
5298 cx.set_state(indoc! {"
5299 1«2ˇ»3
5300 4ˇ567
5301 «8ˇ»9"});
5302 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5303 cx.assert_editor_state(indoc! {"
5304 1ˇ3
5305 ˇ9"});
5306
5307 // Paste with three selections, noticing how the copied selection that was full-line
5308 // gets inserted before the second cursor.
5309 cx.set_state(indoc! {"
5310 1ˇ3
5311 9ˇ
5312 «oˇ»ne"});
5313 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5314 cx.assert_editor_state(indoc! {"
5315 12ˇ3
5316 4567
5317 9ˇ
5318 8ˇne"});
5319
5320 // Copy with a single cursor only, which writes the whole line into the clipboard.
5321 cx.set_state(indoc! {"
5322 The quick brown
5323 fox juˇmps over
5324 the lazy dog"});
5325 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5326 assert_eq!(
5327 cx.read_from_clipboard()
5328 .and_then(|item| item.text().as_deref().map(str::to_string)),
5329 Some("fox jumps over\n".to_string())
5330 );
5331
5332 // Paste with three selections, noticing how the copied full-line selection is inserted
5333 // before the empty selections but replaces the selection that is non-empty.
5334 cx.set_state(indoc! {"
5335 Tˇhe quick brown
5336 «foˇ»x jumps over
5337 tˇhe lazy dog"});
5338 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5339 cx.assert_editor_state(indoc! {"
5340 fox jumps over
5341 Tˇhe quick brown
5342 fox jumps over
5343 ˇx jumps over
5344 fox jumps over
5345 tˇhe lazy dog"});
5346}
5347
5348#[gpui::test]
5349async fn test_copy_trim(cx: &mut TestAppContext) {
5350 init_test(cx, |_| {});
5351
5352 let mut cx = EditorTestContext::new(cx).await;
5353 cx.set_state(
5354 r#" «for selection in selections.iter() {
5355 let mut start = selection.start;
5356 let mut end = selection.end;
5357 let is_entire_line = selection.is_empty();
5358 if is_entire_line {
5359 start = Point::new(start.row, 0);ˇ»
5360 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5361 }
5362 "#,
5363 );
5364 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5365 assert_eq!(
5366 cx.read_from_clipboard()
5367 .and_then(|item| item.text().as_deref().map(str::to_string)),
5368 Some(
5369 "for selection in selections.iter() {
5370 let mut start = selection.start;
5371 let mut end = selection.end;
5372 let is_entire_line = selection.is_empty();
5373 if is_entire_line {
5374 start = Point::new(start.row, 0);"
5375 .to_string()
5376 ),
5377 "Regular copying preserves all indentation selected",
5378 );
5379 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5380 assert_eq!(
5381 cx.read_from_clipboard()
5382 .and_then(|item| item.text().as_deref().map(str::to_string)),
5383 Some(
5384 "for selection in selections.iter() {
5385let mut start = selection.start;
5386let mut end = selection.end;
5387let is_entire_line = selection.is_empty();
5388if is_entire_line {
5389 start = Point::new(start.row, 0);"
5390 .to_string()
5391 ),
5392 "Copying with stripping should strip all leading whitespaces"
5393 );
5394
5395 cx.set_state(
5396 r#" « for selection in selections.iter() {
5397 let mut start = selection.start;
5398 let mut end = selection.end;
5399 let is_entire_line = selection.is_empty();
5400 if is_entire_line {
5401 start = Point::new(start.row, 0);ˇ»
5402 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5403 }
5404 "#,
5405 );
5406 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5407 assert_eq!(
5408 cx.read_from_clipboard()
5409 .and_then(|item| item.text().as_deref().map(str::to_string)),
5410 Some(
5411 " for selection in selections.iter() {
5412 let mut start = selection.start;
5413 let mut end = selection.end;
5414 let is_entire_line = selection.is_empty();
5415 if is_entire_line {
5416 start = Point::new(start.row, 0);"
5417 .to_string()
5418 ),
5419 "Regular copying preserves all indentation selected",
5420 );
5421 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5422 assert_eq!(
5423 cx.read_from_clipboard()
5424 .and_then(|item| item.text().as_deref().map(str::to_string)),
5425 Some(
5426 "for selection in selections.iter() {
5427let mut start = selection.start;
5428let mut end = selection.end;
5429let is_entire_line = selection.is_empty();
5430if is_entire_line {
5431 start = Point::new(start.row, 0);"
5432 .to_string()
5433 ),
5434 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5435 );
5436
5437 cx.set_state(
5438 r#" «ˇ for selection in selections.iter() {
5439 let mut start = selection.start;
5440 let mut end = selection.end;
5441 let is_entire_line = selection.is_empty();
5442 if is_entire_line {
5443 start = Point::new(start.row, 0);»
5444 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5445 }
5446 "#,
5447 );
5448 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5449 assert_eq!(
5450 cx.read_from_clipboard()
5451 .and_then(|item| item.text().as_deref().map(str::to_string)),
5452 Some(
5453 " for selection in selections.iter() {
5454 let mut start = selection.start;
5455 let mut end = selection.end;
5456 let is_entire_line = selection.is_empty();
5457 if is_entire_line {
5458 start = Point::new(start.row, 0);"
5459 .to_string()
5460 ),
5461 "Regular copying for reverse selection works the same",
5462 );
5463 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5464 assert_eq!(
5465 cx.read_from_clipboard()
5466 .and_then(|item| item.text().as_deref().map(str::to_string)),
5467 Some(
5468 "for selection in selections.iter() {
5469let mut start = selection.start;
5470let mut end = selection.end;
5471let is_entire_line = selection.is_empty();
5472if is_entire_line {
5473 start = Point::new(start.row, 0);"
5474 .to_string()
5475 ),
5476 "Copying with stripping for reverse selection works the same"
5477 );
5478
5479 cx.set_state(
5480 r#" for selection «in selections.iter() {
5481 let mut start = selection.start;
5482 let mut end = selection.end;
5483 let is_entire_line = selection.is_empty();
5484 if is_entire_line {
5485 start = Point::new(start.row, 0);ˇ»
5486 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5487 }
5488 "#,
5489 );
5490 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5491 assert_eq!(
5492 cx.read_from_clipboard()
5493 .and_then(|item| item.text().as_deref().map(str::to_string)),
5494 Some(
5495 "in selections.iter() {
5496 let mut start = selection.start;
5497 let mut end = selection.end;
5498 let is_entire_line = selection.is_empty();
5499 if is_entire_line {
5500 start = Point::new(start.row, 0);"
5501 .to_string()
5502 ),
5503 "When selecting past the indent, the copying works as usual",
5504 );
5505 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5506 assert_eq!(
5507 cx.read_from_clipboard()
5508 .and_then(|item| item.text().as_deref().map(str::to_string)),
5509 Some(
5510 "in selections.iter() {
5511 let mut start = selection.start;
5512 let mut end = selection.end;
5513 let is_entire_line = selection.is_empty();
5514 if is_entire_line {
5515 start = Point::new(start.row, 0);"
5516 .to_string()
5517 ),
5518 "When selecting past the indent, nothing is trimmed"
5519 );
5520
5521 cx.set_state(
5522 r#" «for selection in selections.iter() {
5523 let mut start = selection.start;
5524
5525 let mut end = selection.end;
5526 let is_entire_line = selection.is_empty();
5527 if is_entire_line {
5528 start = Point::new(start.row, 0);
5529ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5530 }
5531 "#,
5532 );
5533 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5534 assert_eq!(
5535 cx.read_from_clipboard()
5536 .and_then(|item| item.text().as_deref().map(str::to_string)),
5537 Some(
5538 "for selection in selections.iter() {
5539let mut start = selection.start;
5540
5541let mut end = selection.end;
5542let is_entire_line = selection.is_empty();
5543if is_entire_line {
5544 start = Point::new(start.row, 0);
5545"
5546 .to_string()
5547 ),
5548 "Copying with stripping should ignore empty lines"
5549 );
5550}
5551
5552#[gpui::test]
5553async fn test_paste_multiline(cx: &mut TestAppContext) {
5554 init_test(cx, |_| {});
5555
5556 let mut cx = EditorTestContext::new(cx).await;
5557 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5558
5559 // Cut an indented block, without the leading whitespace.
5560 cx.set_state(indoc! {"
5561 const a: B = (
5562 c(),
5563 «d(
5564 e,
5565 f
5566 )ˇ»
5567 );
5568 "});
5569 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5570 cx.assert_editor_state(indoc! {"
5571 const a: B = (
5572 c(),
5573 ˇ
5574 );
5575 "});
5576
5577 // Paste it at the same position.
5578 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5579 cx.assert_editor_state(indoc! {"
5580 const a: B = (
5581 c(),
5582 d(
5583 e,
5584 f
5585 )ˇ
5586 );
5587 "});
5588
5589 // Paste it at a line with a lower indent level.
5590 cx.set_state(indoc! {"
5591 ˇ
5592 const a: B = (
5593 c(),
5594 );
5595 "});
5596 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5597 cx.assert_editor_state(indoc! {"
5598 d(
5599 e,
5600 f
5601 )ˇ
5602 const a: B = (
5603 c(),
5604 );
5605 "});
5606
5607 // Cut an indented block, with the leading whitespace.
5608 cx.set_state(indoc! {"
5609 const a: B = (
5610 c(),
5611 « d(
5612 e,
5613 f
5614 )
5615 ˇ»);
5616 "});
5617 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5618 cx.assert_editor_state(indoc! {"
5619 const a: B = (
5620 c(),
5621 ˇ);
5622 "});
5623
5624 // Paste it at the same position.
5625 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5626 cx.assert_editor_state(indoc! {"
5627 const a: B = (
5628 c(),
5629 d(
5630 e,
5631 f
5632 )
5633 ˇ);
5634 "});
5635
5636 // Paste it at a line with a higher indent level.
5637 cx.set_state(indoc! {"
5638 const a: B = (
5639 c(),
5640 d(
5641 e,
5642 fˇ
5643 )
5644 );
5645 "});
5646 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5647 cx.assert_editor_state(indoc! {"
5648 const a: B = (
5649 c(),
5650 d(
5651 e,
5652 f d(
5653 e,
5654 f
5655 )
5656 ˇ
5657 )
5658 );
5659 "});
5660
5661 // Copy an indented block, starting mid-line
5662 cx.set_state(indoc! {"
5663 const a: B = (
5664 c(),
5665 somethin«g(
5666 e,
5667 f
5668 )ˇ»
5669 );
5670 "});
5671 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5672
5673 // Paste it on a line with a lower indent level
5674 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5675 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5676 cx.assert_editor_state(indoc! {"
5677 const a: B = (
5678 c(),
5679 something(
5680 e,
5681 f
5682 )
5683 );
5684 g(
5685 e,
5686 f
5687 )ˇ"});
5688}
5689
5690#[gpui::test]
5691async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5692 init_test(cx, |_| {});
5693
5694 cx.write_to_clipboard(ClipboardItem::new_string(
5695 " d(\n e\n );\n".into(),
5696 ));
5697
5698 let mut cx = EditorTestContext::new(cx).await;
5699 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5700
5701 cx.set_state(indoc! {"
5702 fn a() {
5703 b();
5704 if c() {
5705 ˇ
5706 }
5707 }
5708 "});
5709
5710 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5711 cx.assert_editor_state(indoc! {"
5712 fn a() {
5713 b();
5714 if c() {
5715 d(
5716 e
5717 );
5718 ˇ
5719 }
5720 }
5721 "});
5722
5723 cx.set_state(indoc! {"
5724 fn a() {
5725 b();
5726 ˇ
5727 }
5728 "});
5729
5730 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5731 cx.assert_editor_state(indoc! {"
5732 fn a() {
5733 b();
5734 d(
5735 e
5736 );
5737 ˇ
5738 }
5739 "});
5740}
5741
5742#[gpui::test]
5743fn test_select_all(cx: &mut TestAppContext) {
5744 init_test(cx, |_| {});
5745
5746 let editor = cx.add_window(|window, cx| {
5747 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5748 build_editor(buffer, window, cx)
5749 });
5750 _ = editor.update(cx, |editor, window, cx| {
5751 editor.select_all(&SelectAll, window, cx);
5752 assert_eq!(
5753 editor.selections.display_ranges(cx),
5754 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5755 );
5756 });
5757}
5758
5759#[gpui::test]
5760fn test_select_line(cx: &mut TestAppContext) {
5761 init_test(cx, |_| {});
5762
5763 let editor = cx.add_window(|window, cx| {
5764 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5765 build_editor(buffer, window, cx)
5766 });
5767 _ = editor.update(cx, |editor, window, cx| {
5768 editor.change_selections(None, window, cx, |s| {
5769 s.select_display_ranges([
5770 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5771 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5772 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5773 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5774 ])
5775 });
5776 editor.select_line(&SelectLine, window, cx);
5777 assert_eq!(
5778 editor.selections.display_ranges(cx),
5779 vec![
5780 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5781 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5782 ]
5783 );
5784 });
5785
5786 _ = editor.update(cx, |editor, window, cx| {
5787 editor.select_line(&SelectLine, window, cx);
5788 assert_eq!(
5789 editor.selections.display_ranges(cx),
5790 vec![
5791 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5792 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5793 ]
5794 );
5795 });
5796
5797 _ = editor.update(cx, |editor, window, cx| {
5798 editor.select_line(&SelectLine, window, cx);
5799 assert_eq!(
5800 editor.selections.display_ranges(cx),
5801 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5802 );
5803 });
5804}
5805
5806#[gpui::test]
5807async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5808 init_test(cx, |_| {});
5809 let mut cx = EditorTestContext::new(cx).await;
5810
5811 #[track_caller]
5812 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5813 cx.set_state(initial_state);
5814 cx.update_editor(|e, window, cx| {
5815 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5816 });
5817 cx.assert_editor_state(expected_state);
5818 }
5819
5820 // Selection starts and ends at the middle of lines, left-to-right
5821 test(
5822 &mut cx,
5823 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5824 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5825 );
5826 // Same thing, right-to-left
5827 test(
5828 &mut cx,
5829 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5830 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5831 );
5832
5833 // Whole buffer, left-to-right, last line *doesn't* end with newline
5834 test(
5835 &mut cx,
5836 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5837 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5838 );
5839 // Same thing, right-to-left
5840 test(
5841 &mut cx,
5842 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5843 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5844 );
5845
5846 // Whole buffer, left-to-right, last line ends with newline
5847 test(
5848 &mut cx,
5849 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5850 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5851 );
5852 // Same thing, right-to-left
5853 test(
5854 &mut cx,
5855 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5856 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5857 );
5858
5859 // Starts at the end of a line, ends at the start of another
5860 test(
5861 &mut cx,
5862 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5863 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5864 );
5865}
5866
5867#[gpui::test]
5868async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5869 init_test(cx, |_| {});
5870
5871 let editor = cx.add_window(|window, cx| {
5872 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5873 build_editor(buffer, window, cx)
5874 });
5875
5876 // setup
5877 _ = editor.update(cx, |editor, window, cx| {
5878 editor.fold_creases(
5879 vec![
5880 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5881 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5882 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5883 ],
5884 true,
5885 window,
5886 cx,
5887 );
5888 assert_eq!(
5889 editor.display_text(cx),
5890 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5891 );
5892 });
5893
5894 _ = editor.update(cx, |editor, window, cx| {
5895 editor.change_selections(None, window, cx, |s| {
5896 s.select_display_ranges([
5897 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5898 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5899 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5900 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5901 ])
5902 });
5903 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5904 assert_eq!(
5905 editor.display_text(cx),
5906 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5907 );
5908 });
5909 EditorTestContext::for_editor(editor, cx)
5910 .await
5911 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5912
5913 _ = editor.update(cx, |editor, window, cx| {
5914 editor.change_selections(None, window, cx, |s| {
5915 s.select_display_ranges([
5916 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5917 ])
5918 });
5919 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5920 assert_eq!(
5921 editor.display_text(cx),
5922 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5923 );
5924 assert_eq!(
5925 editor.selections.display_ranges(cx),
5926 [
5927 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5928 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5929 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5930 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5931 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5932 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5933 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5934 ]
5935 );
5936 });
5937 EditorTestContext::for_editor(editor, cx)
5938 .await
5939 .assert_editor_state(
5940 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5941 );
5942}
5943
5944#[gpui::test]
5945async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5946 init_test(cx, |_| {});
5947
5948 let mut cx = EditorTestContext::new(cx).await;
5949
5950 cx.set_state(indoc!(
5951 r#"abc
5952 defˇghi
5953
5954 jk
5955 nlmo
5956 "#
5957 ));
5958
5959 cx.update_editor(|editor, window, cx| {
5960 editor.add_selection_above(&Default::default(), window, cx);
5961 });
5962
5963 cx.assert_editor_state(indoc!(
5964 r#"abcˇ
5965 defˇghi
5966
5967 jk
5968 nlmo
5969 "#
5970 ));
5971
5972 cx.update_editor(|editor, window, cx| {
5973 editor.add_selection_above(&Default::default(), window, cx);
5974 });
5975
5976 cx.assert_editor_state(indoc!(
5977 r#"abcˇ
5978 defˇghi
5979
5980 jk
5981 nlmo
5982 "#
5983 ));
5984
5985 cx.update_editor(|editor, window, cx| {
5986 editor.add_selection_below(&Default::default(), window, cx);
5987 });
5988
5989 cx.assert_editor_state(indoc!(
5990 r#"abc
5991 defˇghi
5992
5993 jk
5994 nlmo
5995 "#
5996 ));
5997
5998 cx.update_editor(|editor, window, cx| {
5999 editor.undo_selection(&Default::default(), window, cx);
6000 });
6001
6002 cx.assert_editor_state(indoc!(
6003 r#"abcˇ
6004 defˇghi
6005
6006 jk
6007 nlmo
6008 "#
6009 ));
6010
6011 cx.update_editor(|editor, window, cx| {
6012 editor.redo_selection(&Default::default(), window, cx);
6013 });
6014
6015 cx.assert_editor_state(indoc!(
6016 r#"abc
6017 defˇghi
6018
6019 jk
6020 nlmo
6021 "#
6022 ));
6023
6024 cx.update_editor(|editor, window, cx| {
6025 editor.add_selection_below(&Default::default(), window, cx);
6026 });
6027
6028 cx.assert_editor_state(indoc!(
6029 r#"abc
6030 defˇghi
6031 ˇ
6032 jk
6033 nlmo
6034 "#
6035 ));
6036
6037 cx.update_editor(|editor, window, cx| {
6038 editor.add_selection_below(&Default::default(), window, cx);
6039 });
6040
6041 cx.assert_editor_state(indoc!(
6042 r#"abc
6043 defˇghi
6044 ˇ
6045 jkˇ
6046 nlmo
6047 "#
6048 ));
6049
6050 cx.update_editor(|editor, window, cx| {
6051 editor.add_selection_below(&Default::default(), window, cx);
6052 });
6053
6054 cx.assert_editor_state(indoc!(
6055 r#"abc
6056 defˇghi
6057 ˇ
6058 jkˇ
6059 nlmˇo
6060 "#
6061 ));
6062
6063 cx.update_editor(|editor, window, cx| {
6064 editor.add_selection_below(&Default::default(), window, cx);
6065 });
6066
6067 cx.assert_editor_state(indoc!(
6068 r#"abc
6069 defˇghi
6070 ˇ
6071 jkˇ
6072 nlmˇo
6073 ˇ"#
6074 ));
6075
6076 // change selections
6077 cx.set_state(indoc!(
6078 r#"abc
6079 def«ˇg»hi
6080
6081 jk
6082 nlmo
6083 "#
6084 ));
6085
6086 cx.update_editor(|editor, window, cx| {
6087 editor.add_selection_below(&Default::default(), window, cx);
6088 });
6089
6090 cx.assert_editor_state(indoc!(
6091 r#"abc
6092 def«ˇg»hi
6093
6094 jk
6095 nlm«ˇo»
6096 "#
6097 ));
6098
6099 cx.update_editor(|editor, window, cx| {
6100 editor.add_selection_below(&Default::default(), window, cx);
6101 });
6102
6103 cx.assert_editor_state(indoc!(
6104 r#"abc
6105 def«ˇg»hi
6106
6107 jk
6108 nlm«ˇo»
6109 "#
6110 ));
6111
6112 cx.update_editor(|editor, window, cx| {
6113 editor.add_selection_above(&Default::default(), window, cx);
6114 });
6115
6116 cx.assert_editor_state(indoc!(
6117 r#"abc
6118 def«ˇg»hi
6119
6120 jk
6121 nlmo
6122 "#
6123 ));
6124
6125 cx.update_editor(|editor, window, cx| {
6126 editor.add_selection_above(&Default::default(), window, cx);
6127 });
6128
6129 cx.assert_editor_state(indoc!(
6130 r#"abc
6131 def«ˇg»hi
6132
6133 jk
6134 nlmo
6135 "#
6136 ));
6137
6138 // Change selections again
6139 cx.set_state(indoc!(
6140 r#"a«bc
6141 defgˇ»hi
6142
6143 jk
6144 nlmo
6145 "#
6146 ));
6147
6148 cx.update_editor(|editor, window, cx| {
6149 editor.add_selection_below(&Default::default(), window, cx);
6150 });
6151
6152 cx.assert_editor_state(indoc!(
6153 r#"a«bcˇ»
6154 d«efgˇ»hi
6155
6156 j«kˇ»
6157 nlmo
6158 "#
6159 ));
6160
6161 cx.update_editor(|editor, window, cx| {
6162 editor.add_selection_below(&Default::default(), window, cx);
6163 });
6164 cx.assert_editor_state(indoc!(
6165 r#"a«bcˇ»
6166 d«efgˇ»hi
6167
6168 j«kˇ»
6169 n«lmoˇ»
6170 "#
6171 ));
6172 cx.update_editor(|editor, window, cx| {
6173 editor.add_selection_above(&Default::default(), window, cx);
6174 });
6175
6176 cx.assert_editor_state(indoc!(
6177 r#"a«bcˇ»
6178 d«efgˇ»hi
6179
6180 j«kˇ»
6181 nlmo
6182 "#
6183 ));
6184
6185 // Change selections again
6186 cx.set_state(indoc!(
6187 r#"abc
6188 d«ˇefghi
6189
6190 jk
6191 nlm»o
6192 "#
6193 ));
6194
6195 cx.update_editor(|editor, window, cx| {
6196 editor.add_selection_above(&Default::default(), window, cx);
6197 });
6198
6199 cx.assert_editor_state(indoc!(
6200 r#"a«ˇbc»
6201 d«ˇef»ghi
6202
6203 j«ˇk»
6204 n«ˇlm»o
6205 "#
6206 ));
6207
6208 cx.update_editor(|editor, window, cx| {
6209 editor.add_selection_below(&Default::default(), window, cx);
6210 });
6211
6212 cx.assert_editor_state(indoc!(
6213 r#"abc
6214 d«ˇef»ghi
6215
6216 j«ˇk»
6217 n«ˇlm»o
6218 "#
6219 ));
6220}
6221
6222#[gpui::test]
6223async fn test_select_next(cx: &mut TestAppContext) {
6224 init_test(cx, |_| {});
6225
6226 let mut cx = EditorTestContext::new(cx).await;
6227 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6228
6229 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6230 .unwrap();
6231 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6232
6233 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6234 .unwrap();
6235 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6236
6237 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6238 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6239
6240 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6241 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6242
6243 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6244 .unwrap();
6245 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6246
6247 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6248 .unwrap();
6249 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6250
6251 // Test selection direction should be preserved
6252 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6253
6254 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6255 .unwrap();
6256 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6257}
6258
6259#[gpui::test]
6260async fn test_select_all_matches(cx: &mut TestAppContext) {
6261 init_test(cx, |_| {});
6262
6263 let mut cx = EditorTestContext::new(cx).await;
6264
6265 // Test caret-only selections
6266 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6267 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6268 .unwrap();
6269 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6270
6271 // Test left-to-right selections
6272 cx.set_state("abc\n«abcˇ»\nabc");
6273 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6274 .unwrap();
6275 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6276
6277 // Test right-to-left selections
6278 cx.set_state("abc\n«ˇabc»\nabc");
6279 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6280 .unwrap();
6281 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6282
6283 // Test selecting whitespace with caret selection
6284 cx.set_state("abc\nˇ abc\nabc");
6285 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6286 .unwrap();
6287 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6288
6289 // Test selecting whitespace with left-to-right selection
6290 cx.set_state("abc\n«ˇ »abc\nabc");
6291 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6292 .unwrap();
6293 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6294
6295 // Test no matches with right-to-left selection
6296 cx.set_state("abc\n« ˇ»abc\nabc");
6297 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6298 .unwrap();
6299 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6300}
6301
6302#[gpui::test]
6303async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6304 init_test(cx, |_| {});
6305
6306 let mut cx = EditorTestContext::new(cx).await;
6307
6308 let large_body_1 = "\nd".repeat(200);
6309 let large_body_2 = "\ne".repeat(200);
6310
6311 cx.set_state(&format!(
6312 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6313 ));
6314 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6315 let scroll_position = editor.scroll_position(cx);
6316 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6317 scroll_position
6318 });
6319
6320 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6321 .unwrap();
6322 cx.assert_editor_state(&format!(
6323 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6324 ));
6325 let scroll_position_after_selection =
6326 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6327 assert_eq!(
6328 initial_scroll_position, scroll_position_after_selection,
6329 "Scroll position should not change after selecting all matches"
6330 );
6331}
6332
6333#[gpui::test]
6334async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6335 init_test(cx, |_| {});
6336
6337 let mut cx = EditorLspTestContext::new_rust(
6338 lsp::ServerCapabilities {
6339 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6340 ..Default::default()
6341 },
6342 cx,
6343 )
6344 .await;
6345
6346 cx.set_state(indoc! {"
6347 line 1
6348 line 2
6349 linˇe 3
6350 line 4
6351 line 5
6352 "});
6353
6354 // Make an edit
6355 cx.update_editor(|editor, window, cx| {
6356 editor.handle_input("X", window, cx);
6357 });
6358
6359 // Move cursor to a different position
6360 cx.update_editor(|editor, window, cx| {
6361 editor.change_selections(None, window, cx, |s| {
6362 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6363 });
6364 });
6365
6366 cx.assert_editor_state(indoc! {"
6367 line 1
6368 line 2
6369 linXe 3
6370 line 4
6371 liˇne 5
6372 "});
6373
6374 cx.lsp
6375 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6376 Ok(Some(vec![lsp::TextEdit::new(
6377 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6378 "PREFIX ".to_string(),
6379 )]))
6380 });
6381
6382 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6383 .unwrap()
6384 .await
6385 .unwrap();
6386
6387 cx.assert_editor_state(indoc! {"
6388 PREFIX line 1
6389 line 2
6390 linXe 3
6391 line 4
6392 liˇne 5
6393 "});
6394
6395 // Undo formatting
6396 cx.update_editor(|editor, window, cx| {
6397 editor.undo(&Default::default(), window, cx);
6398 });
6399
6400 // Verify cursor moved back to position after edit
6401 cx.assert_editor_state(indoc! {"
6402 line 1
6403 line 2
6404 linXˇe 3
6405 line 4
6406 line 5
6407 "});
6408}
6409
6410#[gpui::test]
6411async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
6412 init_test(cx, |_| {});
6413
6414 let mut cx = EditorTestContext::new(cx).await;
6415
6416 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
6417 cx.update_editor(|editor, window, cx| {
6418 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
6419 });
6420
6421 cx.set_state(indoc! {"
6422 line 1
6423 line 2
6424 linˇe 3
6425 line 4
6426 line 5
6427 line 6
6428 line 7
6429 line 8
6430 line 9
6431 line 10
6432 "});
6433
6434 let snapshot = cx.buffer_snapshot();
6435 let edit_position = snapshot.anchor_after(Point::new(2, 4));
6436
6437 cx.update(|_, cx| {
6438 provider.update(cx, |provider, _| {
6439 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
6440 id: None,
6441 edits: vec![(edit_position..edit_position, "X".into())],
6442 edit_preview: None,
6443 }))
6444 })
6445 });
6446
6447 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
6448 cx.update_editor(|editor, window, cx| {
6449 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
6450 });
6451
6452 cx.assert_editor_state(indoc! {"
6453 line 1
6454 line 2
6455 lineXˇ 3
6456 line 4
6457 line 5
6458 line 6
6459 line 7
6460 line 8
6461 line 9
6462 line 10
6463 "});
6464
6465 cx.update_editor(|editor, window, cx| {
6466 editor.change_selections(None, window, cx, |s| {
6467 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
6468 });
6469 });
6470
6471 cx.assert_editor_state(indoc! {"
6472 line 1
6473 line 2
6474 lineX 3
6475 line 4
6476 line 5
6477 line 6
6478 line 7
6479 line 8
6480 line 9
6481 liˇne 10
6482 "});
6483
6484 cx.update_editor(|editor, window, cx| {
6485 editor.undo(&Default::default(), window, cx);
6486 });
6487
6488 cx.assert_editor_state(indoc! {"
6489 line 1
6490 line 2
6491 lineˇ 3
6492 line 4
6493 line 5
6494 line 6
6495 line 7
6496 line 8
6497 line 9
6498 line 10
6499 "});
6500}
6501
6502#[gpui::test]
6503async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6504 init_test(cx, |_| {});
6505
6506 let mut cx = EditorTestContext::new(cx).await;
6507 cx.set_state(
6508 r#"let foo = 2;
6509lˇet foo = 2;
6510let fooˇ = 2;
6511let foo = 2;
6512let foo = ˇ2;"#,
6513 );
6514
6515 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6516 .unwrap();
6517 cx.assert_editor_state(
6518 r#"let foo = 2;
6519«letˇ» foo = 2;
6520let «fooˇ» = 2;
6521let foo = 2;
6522let foo = «2ˇ»;"#,
6523 );
6524
6525 // noop for multiple selections with different contents
6526 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6527 .unwrap();
6528 cx.assert_editor_state(
6529 r#"let foo = 2;
6530«letˇ» foo = 2;
6531let «fooˇ» = 2;
6532let foo = 2;
6533let foo = «2ˇ»;"#,
6534 );
6535
6536 // Test last selection direction should be preserved
6537 cx.set_state(
6538 r#"let foo = 2;
6539let foo = 2;
6540let «fooˇ» = 2;
6541let «ˇfoo» = 2;
6542let foo = 2;"#,
6543 );
6544
6545 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6546 .unwrap();
6547 cx.assert_editor_state(
6548 r#"let foo = 2;
6549let foo = 2;
6550let «fooˇ» = 2;
6551let «ˇfoo» = 2;
6552let «ˇfoo» = 2;"#,
6553 );
6554}
6555
6556#[gpui::test]
6557async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6558 init_test(cx, |_| {});
6559
6560 let mut cx =
6561 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6562
6563 cx.assert_editor_state(indoc! {"
6564 ˇbbb
6565 ccc
6566
6567 bbb
6568 ccc
6569 "});
6570 cx.dispatch_action(SelectPrevious::default());
6571 cx.assert_editor_state(indoc! {"
6572 «bbbˇ»
6573 ccc
6574
6575 bbb
6576 ccc
6577 "});
6578 cx.dispatch_action(SelectPrevious::default());
6579 cx.assert_editor_state(indoc! {"
6580 «bbbˇ»
6581 ccc
6582
6583 «bbbˇ»
6584 ccc
6585 "});
6586}
6587
6588#[gpui::test]
6589async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6590 init_test(cx, |_| {});
6591
6592 let mut cx = EditorTestContext::new(cx).await;
6593 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6594
6595 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6596 .unwrap();
6597 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6598
6599 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6600 .unwrap();
6601 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6602
6603 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6604 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6605
6606 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6607 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6608
6609 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6610 .unwrap();
6611 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6612
6613 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6614 .unwrap();
6615 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6616}
6617
6618#[gpui::test]
6619async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6620 init_test(cx, |_| {});
6621
6622 let mut cx = EditorTestContext::new(cx).await;
6623 cx.set_state("aˇ");
6624
6625 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6626 .unwrap();
6627 cx.assert_editor_state("«aˇ»");
6628 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6629 .unwrap();
6630 cx.assert_editor_state("«aˇ»");
6631}
6632
6633#[gpui::test]
6634async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6635 init_test(cx, |_| {});
6636
6637 let mut cx = EditorTestContext::new(cx).await;
6638 cx.set_state(
6639 r#"let foo = 2;
6640lˇet foo = 2;
6641let fooˇ = 2;
6642let foo = 2;
6643let foo = ˇ2;"#,
6644 );
6645
6646 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6647 .unwrap();
6648 cx.assert_editor_state(
6649 r#"let foo = 2;
6650«letˇ» foo = 2;
6651let «fooˇ» = 2;
6652let foo = 2;
6653let foo = «2ˇ»;"#,
6654 );
6655
6656 // noop for multiple selections with different contents
6657 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6658 .unwrap();
6659 cx.assert_editor_state(
6660 r#"let foo = 2;
6661«letˇ» foo = 2;
6662let «fooˇ» = 2;
6663let foo = 2;
6664let foo = «2ˇ»;"#,
6665 );
6666}
6667
6668#[gpui::test]
6669async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6670 init_test(cx, |_| {});
6671
6672 let mut cx = EditorTestContext::new(cx).await;
6673 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6674
6675 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6676 .unwrap();
6677 // selection direction is preserved
6678 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6679
6680 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6681 .unwrap();
6682 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6683
6684 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6685 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6686
6687 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6688 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6689
6690 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6691 .unwrap();
6692 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6693
6694 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6695 .unwrap();
6696 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6697}
6698
6699#[gpui::test]
6700async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6701 init_test(cx, |_| {});
6702
6703 let language = Arc::new(Language::new(
6704 LanguageConfig::default(),
6705 Some(tree_sitter_rust::LANGUAGE.into()),
6706 ));
6707
6708 let text = r#"
6709 use mod1::mod2::{mod3, mod4};
6710
6711 fn fn_1(param1: bool, param2: &str) {
6712 let var1 = "text";
6713 }
6714 "#
6715 .unindent();
6716
6717 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6718 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6719 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6720
6721 editor
6722 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6723 .await;
6724
6725 editor.update_in(cx, |editor, window, cx| {
6726 editor.change_selections(None, window, cx, |s| {
6727 s.select_display_ranges([
6728 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6729 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6730 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6731 ]);
6732 });
6733 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6734 });
6735 editor.update(cx, |editor, cx| {
6736 assert_text_with_selections(
6737 editor,
6738 indoc! {r#"
6739 use mod1::mod2::{mod3, «mod4ˇ»};
6740
6741 fn fn_1«ˇ(param1: bool, param2: &str)» {
6742 let var1 = "«ˇtext»";
6743 }
6744 "#},
6745 cx,
6746 );
6747 });
6748
6749 editor.update_in(cx, |editor, window, cx| {
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 assert_eq!(
6770 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6771 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6772 );
6773
6774 // Trying to expand the selected syntax node one more time has no effect.
6775 editor.update_in(cx, |editor, window, cx| {
6776 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6777 });
6778 assert_eq!(
6779 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6780 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6781 );
6782
6783 editor.update_in(cx, |editor, window, cx| {
6784 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6785 });
6786 editor.update(cx, |editor, cx| {
6787 assert_text_with_selections(
6788 editor,
6789 indoc! {r#"
6790 use mod1::mod2::«{mod3, mod4}ˇ»;
6791
6792 «ˇfn fn_1(param1: bool, param2: &str) {
6793 let var1 = "text";
6794 }»
6795 "#},
6796 cx,
6797 );
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, mo«ˇ»d4};
6825
6826 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6827 let var1 = "te«ˇ»xt";
6828 }
6829 "#},
6830 cx,
6831 );
6832 });
6833
6834 // Trying to shrink the selected syntax node one more time has no effect.
6835 editor.update_in(cx, |editor, window, cx| {
6836 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6837 });
6838 editor.update_in(cx, |editor, _, cx| {
6839 assert_text_with_selections(
6840 editor,
6841 indoc! {r#"
6842 use mod1::mod2::{mod3, mo«ˇ»d4};
6843
6844 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6845 let var1 = "te«ˇ»xt";
6846 }
6847 "#},
6848 cx,
6849 );
6850 });
6851
6852 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6853 // a fold.
6854 editor.update_in(cx, |editor, window, cx| {
6855 editor.fold_creases(
6856 vec![
6857 Crease::simple(
6858 Point::new(0, 21)..Point::new(0, 24),
6859 FoldPlaceholder::test(),
6860 ),
6861 Crease::simple(
6862 Point::new(3, 20)..Point::new(3, 22),
6863 FoldPlaceholder::test(),
6864 ),
6865 ],
6866 true,
6867 window,
6868 cx,
6869 );
6870 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6871 });
6872 editor.update(cx, |editor, cx| {
6873 assert_text_with_selections(
6874 editor,
6875 indoc! {r#"
6876 use mod1::mod2::«{mod3, mod4}ˇ»;
6877
6878 fn fn_1«ˇ(param1: bool, param2: &str)» {
6879 let var1 = "«ˇtext»";
6880 }
6881 "#},
6882 cx,
6883 );
6884 });
6885}
6886
6887#[gpui::test]
6888async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6889 init_test(cx, |_| {});
6890
6891 let language = Arc::new(Language::new(
6892 LanguageConfig::default(),
6893 Some(tree_sitter_rust::LANGUAGE.into()),
6894 ));
6895
6896 let text = "let a = 2;";
6897
6898 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6899 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6900 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6901
6902 editor
6903 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6904 .await;
6905
6906 // Test case 1: Cursor at end of word
6907 editor.update_in(cx, |editor, window, cx| {
6908 editor.change_selections(None, window, cx, |s| {
6909 s.select_display_ranges([
6910 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6911 ]);
6912 });
6913 });
6914 editor.update(cx, |editor, cx| {
6915 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6916 });
6917 editor.update_in(cx, |editor, window, cx| {
6918 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6919 });
6920 editor.update(cx, |editor, cx| {
6921 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6922 });
6923 editor.update_in(cx, |editor, window, cx| {
6924 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6925 });
6926 editor.update(cx, |editor, cx| {
6927 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6928 });
6929
6930 // Test case 2: Cursor at end of statement
6931 editor.update_in(cx, |editor, window, cx| {
6932 editor.change_selections(None, window, cx, |s| {
6933 s.select_display_ranges([
6934 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6935 ]);
6936 });
6937 });
6938 editor.update(cx, |editor, cx| {
6939 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6940 });
6941 editor.update_in(cx, |editor, window, cx| {
6942 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6943 });
6944 editor.update(cx, |editor, cx| {
6945 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6946 });
6947}
6948
6949#[gpui::test]
6950async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6951 init_test(cx, |_| {});
6952
6953 let language = Arc::new(Language::new(
6954 LanguageConfig::default(),
6955 Some(tree_sitter_rust::LANGUAGE.into()),
6956 ));
6957
6958 let text = r#"
6959 use mod1::mod2::{mod3, mod4};
6960
6961 fn fn_1(param1: bool, param2: &str) {
6962 let var1 = "hello world";
6963 }
6964 "#
6965 .unindent();
6966
6967 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6968 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6969 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6970
6971 editor
6972 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6973 .await;
6974
6975 // Test 1: Cursor on a letter of a string word
6976 editor.update_in(cx, |editor, window, cx| {
6977 editor.change_selections(None, window, cx, |s| {
6978 s.select_display_ranges([
6979 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6980 ]);
6981 });
6982 });
6983 editor.update_in(cx, |editor, window, cx| {
6984 assert_text_with_selections(
6985 editor,
6986 indoc! {r#"
6987 use mod1::mod2::{mod3, mod4};
6988
6989 fn fn_1(param1: bool, param2: &str) {
6990 let var1 = "hˇello world";
6991 }
6992 "#},
6993 cx,
6994 );
6995 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6996 assert_text_with_selections(
6997 editor,
6998 indoc! {r#"
6999 use mod1::mod2::{mod3, mod4};
7000
7001 fn fn_1(param1: bool, param2: &str) {
7002 let var1 = "«ˇhello» world";
7003 }
7004 "#},
7005 cx,
7006 );
7007 });
7008
7009 // Test 2: Partial selection within a word
7010 editor.update_in(cx, |editor, window, cx| {
7011 editor.change_selections(None, window, cx, |s| {
7012 s.select_display_ranges([
7013 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7014 ]);
7015 });
7016 });
7017 editor.update_in(cx, |editor, window, cx| {
7018 assert_text_with_selections(
7019 editor,
7020 indoc! {r#"
7021 use mod1::mod2::{mod3, mod4};
7022
7023 fn fn_1(param1: bool, param2: &str) {
7024 let var1 = "h«elˇ»lo world";
7025 }
7026 "#},
7027 cx,
7028 );
7029 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7030 assert_text_with_selections(
7031 editor,
7032 indoc! {r#"
7033 use mod1::mod2::{mod3, mod4};
7034
7035 fn fn_1(param1: bool, param2: &str) {
7036 let var1 = "«ˇhello» world";
7037 }
7038 "#},
7039 cx,
7040 );
7041 });
7042
7043 // Test 3: Complete word already selected
7044 editor.update_in(cx, |editor, window, cx| {
7045 editor.change_selections(None, window, cx, |s| {
7046 s.select_display_ranges([
7047 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7048 ]);
7049 });
7050 });
7051 editor.update_in(cx, |editor, window, cx| {
7052 assert_text_with_selections(
7053 editor,
7054 indoc! {r#"
7055 use mod1::mod2::{mod3, mod4};
7056
7057 fn fn_1(param1: bool, param2: &str) {
7058 let var1 = "«helloˇ» world";
7059 }
7060 "#},
7061 cx,
7062 );
7063 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7064 assert_text_with_selections(
7065 editor,
7066 indoc! {r#"
7067 use mod1::mod2::{mod3, mod4};
7068
7069 fn fn_1(param1: bool, param2: &str) {
7070 let var1 = "«hello worldˇ»";
7071 }
7072 "#},
7073 cx,
7074 );
7075 });
7076
7077 // Test 4: Selection spanning across words
7078 editor.update_in(cx, |editor, window, cx| {
7079 editor.change_selections(None, window, cx, |s| {
7080 s.select_display_ranges([
7081 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7082 ]);
7083 });
7084 });
7085 editor.update_in(cx, |editor, window, cx| {
7086 assert_text_with_selections(
7087 editor,
7088 indoc! {r#"
7089 use mod1::mod2::{mod3, mod4};
7090
7091 fn fn_1(param1: bool, param2: &str) {
7092 let var1 = "hel«lo woˇ»rld";
7093 }
7094 "#},
7095 cx,
7096 );
7097 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7098 assert_text_with_selections(
7099 editor,
7100 indoc! {r#"
7101 use mod1::mod2::{mod3, mod4};
7102
7103 fn fn_1(param1: bool, param2: &str) {
7104 let var1 = "«ˇhello world»";
7105 }
7106 "#},
7107 cx,
7108 );
7109 });
7110
7111 // Test 5: Expansion beyond string
7112 editor.update_in(cx, |editor, window, cx| {
7113 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
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
7129#[gpui::test]
7130async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7131 init_test(cx, |_| {});
7132
7133 let base_text = r#"
7134 impl A {
7135 // this is an uncommitted comment
7136
7137 fn b() {
7138 c();
7139 }
7140
7141 // this is another uncommitted comment
7142
7143 fn d() {
7144 // e
7145 // f
7146 }
7147 }
7148
7149 fn g() {
7150 // h
7151 }
7152 "#
7153 .unindent();
7154
7155 let text = r#"
7156 ˇimpl A {
7157
7158 fn b() {
7159 c();
7160 }
7161
7162 fn d() {
7163 // e
7164 // f
7165 }
7166 }
7167
7168 fn g() {
7169 // h
7170 }
7171 "#
7172 .unindent();
7173
7174 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7175 cx.set_state(&text);
7176 cx.set_head_text(&base_text);
7177 cx.update_editor(|editor, window, cx| {
7178 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7179 });
7180
7181 cx.assert_state_with_diff(
7182 "
7183 ˇimpl A {
7184 - // this is an uncommitted comment
7185
7186 fn b() {
7187 c();
7188 }
7189
7190 - // this is another uncommitted comment
7191 -
7192 fn d() {
7193 // e
7194 // f
7195 }
7196 }
7197
7198 fn g() {
7199 // h
7200 }
7201 "
7202 .unindent(),
7203 );
7204
7205 let expected_display_text = "
7206 impl A {
7207 // this is an uncommitted comment
7208
7209 fn b() {
7210 ⋯
7211 }
7212
7213 // this is another uncommitted comment
7214
7215 fn d() {
7216 ⋯
7217 }
7218 }
7219
7220 fn g() {
7221 ⋯
7222 }
7223 "
7224 .unindent();
7225
7226 cx.update_editor(|editor, window, cx| {
7227 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7228 assert_eq!(editor.display_text(cx), expected_display_text);
7229 });
7230}
7231
7232#[gpui::test]
7233async fn test_autoindent(cx: &mut TestAppContext) {
7234 init_test(cx, |_| {});
7235
7236 let language = Arc::new(
7237 Language::new(
7238 LanguageConfig {
7239 brackets: BracketPairConfig {
7240 pairs: vec![
7241 BracketPair {
7242 start: "{".to_string(),
7243 end: "}".to_string(),
7244 close: false,
7245 surround: false,
7246 newline: true,
7247 },
7248 BracketPair {
7249 start: "(".to_string(),
7250 end: ")".to_string(),
7251 close: false,
7252 surround: false,
7253 newline: true,
7254 },
7255 ],
7256 ..Default::default()
7257 },
7258 ..Default::default()
7259 },
7260 Some(tree_sitter_rust::LANGUAGE.into()),
7261 )
7262 .with_indents_query(
7263 r#"
7264 (_ "(" ")" @end) @indent
7265 (_ "{" "}" @end) @indent
7266 "#,
7267 )
7268 .unwrap(),
7269 );
7270
7271 let text = "fn a() {}";
7272
7273 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7274 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7275 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7276 editor
7277 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7278 .await;
7279
7280 editor.update_in(cx, |editor, window, cx| {
7281 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7282 editor.newline(&Newline, window, cx);
7283 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7284 assert_eq!(
7285 editor.selections.ranges(cx),
7286 &[
7287 Point::new(1, 4)..Point::new(1, 4),
7288 Point::new(3, 4)..Point::new(3, 4),
7289 Point::new(5, 0)..Point::new(5, 0)
7290 ]
7291 );
7292 });
7293}
7294
7295#[gpui::test]
7296async fn test_autoindent_selections(cx: &mut TestAppContext) {
7297 init_test(cx, |_| {});
7298
7299 {
7300 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7301 cx.set_state(indoc! {"
7302 impl A {
7303
7304 fn b() {}
7305
7306 «fn c() {
7307
7308 }ˇ»
7309 }
7310 "});
7311
7312 cx.update_editor(|editor, window, cx| {
7313 editor.autoindent(&Default::default(), window, cx);
7314 });
7315
7316 cx.assert_editor_state(indoc! {"
7317 impl A {
7318
7319 fn b() {}
7320
7321 «fn c() {
7322
7323 }ˇ»
7324 }
7325 "});
7326 }
7327
7328 {
7329 let mut cx = EditorTestContext::new_multibuffer(
7330 cx,
7331 [indoc! { "
7332 impl A {
7333 «
7334 // a
7335 fn b(){}
7336 »
7337 «
7338 }
7339 fn c(){}
7340 »
7341 "}],
7342 );
7343
7344 let buffer = cx.update_editor(|editor, _, cx| {
7345 let buffer = editor.buffer().update(cx, |buffer, _| {
7346 buffer.all_buffers().iter().next().unwrap().clone()
7347 });
7348 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7349 buffer
7350 });
7351
7352 cx.run_until_parked();
7353 cx.update_editor(|editor, window, cx| {
7354 editor.select_all(&Default::default(), window, cx);
7355 editor.autoindent(&Default::default(), window, cx)
7356 });
7357 cx.run_until_parked();
7358
7359 cx.update(|_, cx| {
7360 assert_eq!(
7361 buffer.read(cx).text(),
7362 indoc! { "
7363 impl A {
7364
7365 // a
7366 fn b(){}
7367
7368
7369 }
7370 fn c(){}
7371
7372 " }
7373 )
7374 });
7375 }
7376}
7377
7378#[gpui::test]
7379async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7380 init_test(cx, |_| {});
7381
7382 let mut cx = EditorTestContext::new(cx).await;
7383
7384 let language = Arc::new(Language::new(
7385 LanguageConfig {
7386 brackets: BracketPairConfig {
7387 pairs: vec![
7388 BracketPair {
7389 start: "{".to_string(),
7390 end: "}".to_string(),
7391 close: true,
7392 surround: true,
7393 newline: true,
7394 },
7395 BracketPair {
7396 start: "(".to_string(),
7397 end: ")".to_string(),
7398 close: true,
7399 surround: true,
7400 newline: true,
7401 },
7402 BracketPair {
7403 start: "/*".to_string(),
7404 end: " */".to_string(),
7405 close: true,
7406 surround: true,
7407 newline: true,
7408 },
7409 BracketPair {
7410 start: "[".to_string(),
7411 end: "]".to_string(),
7412 close: false,
7413 surround: false,
7414 newline: true,
7415 },
7416 BracketPair {
7417 start: "\"".to_string(),
7418 end: "\"".to_string(),
7419 close: true,
7420 surround: true,
7421 newline: false,
7422 },
7423 BracketPair {
7424 start: "<".to_string(),
7425 end: ">".to_string(),
7426 close: false,
7427 surround: true,
7428 newline: true,
7429 },
7430 ],
7431 ..Default::default()
7432 },
7433 autoclose_before: "})]".to_string(),
7434 ..Default::default()
7435 },
7436 Some(tree_sitter_rust::LANGUAGE.into()),
7437 ));
7438
7439 cx.language_registry().add(language.clone());
7440 cx.update_buffer(|buffer, cx| {
7441 buffer.set_language(Some(language), cx);
7442 });
7443
7444 cx.set_state(
7445 &r#"
7446 🏀ˇ
7447 εˇ
7448 ❤️ˇ
7449 "#
7450 .unindent(),
7451 );
7452
7453 // autoclose multiple nested brackets at multiple cursors
7454 cx.update_editor(|editor, window, cx| {
7455 editor.handle_input("{", window, cx);
7456 editor.handle_input("{", window, cx);
7457 editor.handle_input("{", window, cx);
7458 });
7459 cx.assert_editor_state(
7460 &"
7461 🏀{{{ˇ}}}
7462 ε{{{ˇ}}}
7463 ❤️{{{ˇ}}}
7464 "
7465 .unindent(),
7466 );
7467
7468 // insert a different closing bracket
7469 cx.update_editor(|editor, window, cx| {
7470 editor.handle_input(")", window, cx);
7471 });
7472 cx.assert_editor_state(
7473 &"
7474 🏀{{{)ˇ}}}
7475 ε{{{)ˇ}}}
7476 ❤️{{{)ˇ}}}
7477 "
7478 .unindent(),
7479 );
7480
7481 // skip over the auto-closed brackets when typing a closing bracket
7482 cx.update_editor(|editor, window, cx| {
7483 editor.move_right(&MoveRight, window, cx);
7484 editor.handle_input("}", window, cx);
7485 editor.handle_input("}", window, cx);
7486 editor.handle_input("}", window, cx);
7487 });
7488 cx.assert_editor_state(
7489 &"
7490 🏀{{{)}}}}ˇ
7491 ε{{{)}}}}ˇ
7492 ❤️{{{)}}}}ˇ
7493 "
7494 .unindent(),
7495 );
7496
7497 // autoclose multi-character pairs
7498 cx.set_state(
7499 &"
7500 ˇ
7501 ˇ
7502 "
7503 .unindent(),
7504 );
7505 cx.update_editor(|editor, window, cx| {
7506 editor.handle_input("/", window, cx);
7507 editor.handle_input("*", window, cx);
7508 });
7509 cx.assert_editor_state(
7510 &"
7511 /*ˇ */
7512 /*ˇ */
7513 "
7514 .unindent(),
7515 );
7516
7517 // one cursor autocloses a multi-character pair, one cursor
7518 // does not autoclose.
7519 cx.set_state(
7520 &"
7521 /ˇ
7522 ˇ
7523 "
7524 .unindent(),
7525 );
7526 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7527 cx.assert_editor_state(
7528 &"
7529 /*ˇ */
7530 *ˇ
7531 "
7532 .unindent(),
7533 );
7534
7535 // Don't autoclose if the next character isn't whitespace and isn't
7536 // listed in the language's "autoclose_before" section.
7537 cx.set_state("ˇa b");
7538 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7539 cx.assert_editor_state("{ˇa b");
7540
7541 // Don't autoclose if `close` is false for the bracket pair
7542 cx.set_state("ˇ");
7543 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7544 cx.assert_editor_state("[ˇ");
7545
7546 // Surround with brackets if text is selected
7547 cx.set_state("«aˇ» b");
7548 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7549 cx.assert_editor_state("{«aˇ»} b");
7550
7551 // Autoclose when not immediately after a word character
7552 cx.set_state("a ˇ");
7553 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7554 cx.assert_editor_state("a \"ˇ\"");
7555
7556 // Autoclose pair where the start and end characters are the same
7557 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7558 cx.assert_editor_state("a \"\"ˇ");
7559
7560 // Don't autoclose when immediately after a word character
7561 cx.set_state("aˇ");
7562 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7563 cx.assert_editor_state("a\"ˇ");
7564
7565 // Do autoclose when after a non-word character
7566 cx.set_state("{ˇ");
7567 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7568 cx.assert_editor_state("{\"ˇ\"");
7569
7570 // Non identical pairs autoclose regardless of preceding character
7571 cx.set_state("aˇ");
7572 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7573 cx.assert_editor_state("a{ˇ}");
7574
7575 // Don't autoclose pair if autoclose is disabled
7576 cx.set_state("ˇ");
7577 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7578 cx.assert_editor_state("<ˇ");
7579
7580 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7581 cx.set_state("«aˇ» b");
7582 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7583 cx.assert_editor_state("<«aˇ»> b");
7584}
7585
7586#[gpui::test]
7587async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7588 init_test(cx, |settings| {
7589 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7590 });
7591
7592 let mut cx = EditorTestContext::new(cx).await;
7593
7594 let language = Arc::new(Language::new(
7595 LanguageConfig {
7596 brackets: BracketPairConfig {
7597 pairs: vec![
7598 BracketPair {
7599 start: "{".to_string(),
7600 end: "}".to_string(),
7601 close: true,
7602 surround: true,
7603 newline: true,
7604 },
7605 BracketPair {
7606 start: "(".to_string(),
7607 end: ")".to_string(),
7608 close: true,
7609 surround: true,
7610 newline: true,
7611 },
7612 BracketPair {
7613 start: "[".to_string(),
7614 end: "]".to_string(),
7615 close: false,
7616 surround: false,
7617 newline: true,
7618 },
7619 ],
7620 ..Default::default()
7621 },
7622 autoclose_before: "})]".to_string(),
7623 ..Default::default()
7624 },
7625 Some(tree_sitter_rust::LANGUAGE.into()),
7626 ));
7627
7628 cx.language_registry().add(language.clone());
7629 cx.update_buffer(|buffer, cx| {
7630 buffer.set_language(Some(language), cx);
7631 });
7632
7633 cx.set_state(
7634 &"
7635 ˇ
7636 ˇ
7637 ˇ
7638 "
7639 .unindent(),
7640 );
7641
7642 // ensure only matching closing brackets are skipped over
7643 cx.update_editor(|editor, window, cx| {
7644 editor.handle_input("}", window, cx);
7645 editor.move_left(&MoveLeft, window, cx);
7646 editor.handle_input(")", window, cx);
7647 editor.move_left(&MoveLeft, window, cx);
7648 });
7649 cx.assert_editor_state(
7650 &"
7651 ˇ)}
7652 ˇ)}
7653 ˇ)}
7654 "
7655 .unindent(),
7656 );
7657
7658 // skip-over closing brackets at multiple cursors
7659 cx.update_editor(|editor, window, cx| {
7660 editor.handle_input(")", window, cx);
7661 editor.handle_input("}", window, cx);
7662 });
7663 cx.assert_editor_state(
7664 &"
7665 )}ˇ
7666 )}ˇ
7667 )}ˇ
7668 "
7669 .unindent(),
7670 );
7671
7672 // ignore non-close brackets
7673 cx.update_editor(|editor, window, cx| {
7674 editor.handle_input("]", window, cx);
7675 editor.move_left(&MoveLeft, window, cx);
7676 editor.handle_input("]", window, cx);
7677 });
7678 cx.assert_editor_state(
7679 &"
7680 )}]ˇ]
7681 )}]ˇ]
7682 )}]ˇ]
7683 "
7684 .unindent(),
7685 );
7686}
7687
7688#[gpui::test]
7689async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7690 init_test(cx, |_| {});
7691
7692 let mut cx = EditorTestContext::new(cx).await;
7693
7694 let html_language = Arc::new(
7695 Language::new(
7696 LanguageConfig {
7697 name: "HTML".into(),
7698 brackets: BracketPairConfig {
7699 pairs: vec![
7700 BracketPair {
7701 start: "<".into(),
7702 end: ">".into(),
7703 close: true,
7704 ..Default::default()
7705 },
7706 BracketPair {
7707 start: "{".into(),
7708 end: "}".into(),
7709 close: true,
7710 ..Default::default()
7711 },
7712 BracketPair {
7713 start: "(".into(),
7714 end: ")".into(),
7715 close: true,
7716 ..Default::default()
7717 },
7718 ],
7719 ..Default::default()
7720 },
7721 autoclose_before: "})]>".into(),
7722 ..Default::default()
7723 },
7724 Some(tree_sitter_html::LANGUAGE.into()),
7725 )
7726 .with_injection_query(
7727 r#"
7728 (script_element
7729 (raw_text) @injection.content
7730 (#set! injection.language "javascript"))
7731 "#,
7732 )
7733 .unwrap(),
7734 );
7735
7736 let javascript_language = Arc::new(Language::new(
7737 LanguageConfig {
7738 name: "JavaScript".into(),
7739 brackets: BracketPairConfig {
7740 pairs: vec![
7741 BracketPair {
7742 start: "/*".into(),
7743 end: " */".into(),
7744 close: true,
7745 ..Default::default()
7746 },
7747 BracketPair {
7748 start: "{".into(),
7749 end: "}".into(),
7750 close: true,
7751 ..Default::default()
7752 },
7753 BracketPair {
7754 start: "(".into(),
7755 end: ")".into(),
7756 close: true,
7757 ..Default::default()
7758 },
7759 ],
7760 ..Default::default()
7761 },
7762 autoclose_before: "})]>".into(),
7763 ..Default::default()
7764 },
7765 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7766 ));
7767
7768 cx.language_registry().add(html_language.clone());
7769 cx.language_registry().add(javascript_language.clone());
7770
7771 cx.update_buffer(|buffer, cx| {
7772 buffer.set_language(Some(html_language), cx);
7773 });
7774
7775 cx.set_state(
7776 &r#"
7777 <body>ˇ
7778 <script>
7779 var x = 1;ˇ
7780 </script>
7781 </body>ˇ
7782 "#
7783 .unindent(),
7784 );
7785
7786 // Precondition: different languages are active at different locations.
7787 cx.update_editor(|editor, window, cx| {
7788 let snapshot = editor.snapshot(window, cx);
7789 let cursors = editor.selections.ranges::<usize>(cx);
7790 let languages = cursors
7791 .iter()
7792 .map(|c| snapshot.language_at(c.start).unwrap().name())
7793 .collect::<Vec<_>>();
7794 assert_eq!(
7795 languages,
7796 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7797 );
7798 });
7799
7800 // Angle brackets autoclose in HTML, but not JavaScript.
7801 cx.update_editor(|editor, window, cx| {
7802 editor.handle_input("<", window, cx);
7803 editor.handle_input("a", window, cx);
7804 });
7805 cx.assert_editor_state(
7806 &r#"
7807 <body><aˇ>
7808 <script>
7809 var x = 1;<aˇ
7810 </script>
7811 </body><aˇ>
7812 "#
7813 .unindent(),
7814 );
7815
7816 // Curly braces and parens autoclose in both HTML and JavaScript.
7817 cx.update_editor(|editor, window, cx| {
7818 editor.handle_input(" b=", window, cx);
7819 editor.handle_input("{", window, cx);
7820 editor.handle_input("c", window, cx);
7821 editor.handle_input("(", window, cx);
7822 });
7823 cx.assert_editor_state(
7824 &r#"
7825 <body><a b={c(ˇ)}>
7826 <script>
7827 var x = 1;<a b={c(ˇ)}
7828 </script>
7829 </body><a b={c(ˇ)}>
7830 "#
7831 .unindent(),
7832 );
7833
7834 // Brackets that were already autoclosed are skipped.
7835 cx.update_editor(|editor, window, cx| {
7836 editor.handle_input(")", window, cx);
7837 editor.handle_input("d", window, cx);
7838 editor.handle_input("}", window, cx);
7839 });
7840 cx.assert_editor_state(
7841 &r#"
7842 <body><a b={c()d}ˇ>
7843 <script>
7844 var x = 1;<a b={c()d}ˇ
7845 </script>
7846 </body><a b={c()d}ˇ>
7847 "#
7848 .unindent(),
7849 );
7850 cx.update_editor(|editor, window, cx| {
7851 editor.handle_input(">", window, cx);
7852 });
7853 cx.assert_editor_state(
7854 &r#"
7855 <body><a b={c()d}>ˇ
7856 <script>
7857 var x = 1;<a b={c()d}>ˇ
7858 </script>
7859 </body><a b={c()d}>ˇ
7860 "#
7861 .unindent(),
7862 );
7863
7864 // Reset
7865 cx.set_state(
7866 &r#"
7867 <body>ˇ
7868 <script>
7869 var x = 1;ˇ
7870 </script>
7871 </body>ˇ
7872 "#
7873 .unindent(),
7874 );
7875
7876 cx.update_editor(|editor, window, cx| {
7877 editor.handle_input("<", window, cx);
7878 });
7879 cx.assert_editor_state(
7880 &r#"
7881 <body><ˇ>
7882 <script>
7883 var x = 1;<ˇ
7884 </script>
7885 </body><ˇ>
7886 "#
7887 .unindent(),
7888 );
7889
7890 // When backspacing, the closing angle brackets are removed.
7891 cx.update_editor(|editor, window, cx| {
7892 editor.backspace(&Backspace, window, cx);
7893 });
7894 cx.assert_editor_state(
7895 &r#"
7896 <body>ˇ
7897 <script>
7898 var x = 1;ˇ
7899 </script>
7900 </body>ˇ
7901 "#
7902 .unindent(),
7903 );
7904
7905 // Block comments autoclose in JavaScript, but not HTML.
7906 cx.update_editor(|editor, window, cx| {
7907 editor.handle_input("/", window, cx);
7908 editor.handle_input("*", window, cx);
7909 });
7910 cx.assert_editor_state(
7911 &r#"
7912 <body>/*ˇ
7913 <script>
7914 var x = 1;/*ˇ */
7915 </script>
7916 </body>/*ˇ
7917 "#
7918 .unindent(),
7919 );
7920}
7921
7922#[gpui::test]
7923async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7924 init_test(cx, |_| {});
7925
7926 let mut cx = EditorTestContext::new(cx).await;
7927
7928 let rust_language = Arc::new(
7929 Language::new(
7930 LanguageConfig {
7931 name: "Rust".into(),
7932 brackets: serde_json::from_value(json!([
7933 { "start": "{", "end": "}", "close": true, "newline": true },
7934 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7935 ]))
7936 .unwrap(),
7937 autoclose_before: "})]>".into(),
7938 ..Default::default()
7939 },
7940 Some(tree_sitter_rust::LANGUAGE.into()),
7941 )
7942 .with_override_query("(string_literal) @string")
7943 .unwrap(),
7944 );
7945
7946 cx.language_registry().add(rust_language.clone());
7947 cx.update_buffer(|buffer, cx| {
7948 buffer.set_language(Some(rust_language), cx);
7949 });
7950
7951 cx.set_state(
7952 &r#"
7953 let x = ˇ
7954 "#
7955 .unindent(),
7956 );
7957
7958 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7959 cx.update_editor(|editor, window, cx| {
7960 editor.handle_input("\"", window, cx);
7961 });
7962 cx.assert_editor_state(
7963 &r#"
7964 let x = "ˇ"
7965 "#
7966 .unindent(),
7967 );
7968
7969 // Inserting another quotation mark. The cursor moves across the existing
7970 // automatically-inserted quotation mark.
7971 cx.update_editor(|editor, window, cx| {
7972 editor.handle_input("\"", window, cx);
7973 });
7974 cx.assert_editor_state(
7975 &r#"
7976 let x = ""ˇ
7977 "#
7978 .unindent(),
7979 );
7980
7981 // Reset
7982 cx.set_state(
7983 &r#"
7984 let x = ˇ
7985 "#
7986 .unindent(),
7987 );
7988
7989 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7990 cx.update_editor(|editor, window, cx| {
7991 editor.handle_input("\"", window, cx);
7992 editor.handle_input(" ", window, cx);
7993 editor.move_left(&Default::default(), window, cx);
7994 editor.handle_input("\\", window, cx);
7995 editor.handle_input("\"", window, cx);
7996 });
7997 cx.assert_editor_state(
7998 &r#"
7999 let x = "\"ˇ "
8000 "#
8001 .unindent(),
8002 );
8003
8004 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8005 // mark. Nothing is inserted.
8006 cx.update_editor(|editor, window, cx| {
8007 editor.move_right(&Default::default(), window, cx);
8008 editor.handle_input("\"", window, cx);
8009 });
8010 cx.assert_editor_state(
8011 &r#"
8012 let x = "\" "ˇ
8013 "#
8014 .unindent(),
8015 );
8016}
8017
8018#[gpui::test]
8019async fn test_surround_with_pair(cx: &mut TestAppContext) {
8020 init_test(cx, |_| {});
8021
8022 let language = Arc::new(Language::new(
8023 LanguageConfig {
8024 brackets: BracketPairConfig {
8025 pairs: vec![
8026 BracketPair {
8027 start: "{".to_string(),
8028 end: "}".to_string(),
8029 close: true,
8030 surround: true,
8031 newline: true,
8032 },
8033 BracketPair {
8034 start: "/* ".to_string(),
8035 end: "*/".to_string(),
8036 close: true,
8037 surround: true,
8038 ..Default::default()
8039 },
8040 ],
8041 ..Default::default()
8042 },
8043 ..Default::default()
8044 },
8045 Some(tree_sitter_rust::LANGUAGE.into()),
8046 ));
8047
8048 let text = r#"
8049 a
8050 b
8051 c
8052 "#
8053 .unindent();
8054
8055 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8056 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8057 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8058 editor
8059 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8060 .await;
8061
8062 editor.update_in(cx, |editor, window, cx| {
8063 editor.change_selections(None, window, cx, |s| {
8064 s.select_display_ranges([
8065 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8066 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8067 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8068 ])
8069 });
8070
8071 editor.handle_input("{", window, cx);
8072 editor.handle_input("{", window, cx);
8073 editor.handle_input("{", window, cx);
8074 assert_eq!(
8075 editor.text(cx),
8076 "
8077 {{{a}}}
8078 {{{b}}}
8079 {{{c}}}
8080 "
8081 .unindent()
8082 );
8083 assert_eq!(
8084 editor.selections.display_ranges(cx),
8085 [
8086 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8087 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8088 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8089 ]
8090 );
8091
8092 editor.undo(&Undo, window, cx);
8093 editor.undo(&Undo, window, cx);
8094 editor.undo(&Undo, window, cx);
8095 assert_eq!(
8096 editor.text(cx),
8097 "
8098 a
8099 b
8100 c
8101 "
8102 .unindent()
8103 );
8104 assert_eq!(
8105 editor.selections.display_ranges(cx),
8106 [
8107 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8108 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8109 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8110 ]
8111 );
8112
8113 // Ensure inserting the first character of a multi-byte bracket pair
8114 // doesn't surround the selections with the bracket.
8115 editor.handle_input("/", window, cx);
8116 assert_eq!(
8117 editor.text(cx),
8118 "
8119 /
8120 /
8121 /
8122 "
8123 .unindent()
8124 );
8125 assert_eq!(
8126 editor.selections.display_ranges(cx),
8127 [
8128 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8129 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8130 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8131 ]
8132 );
8133
8134 editor.undo(&Undo, window, cx);
8135 assert_eq!(
8136 editor.text(cx),
8137 "
8138 a
8139 b
8140 c
8141 "
8142 .unindent()
8143 );
8144 assert_eq!(
8145 editor.selections.display_ranges(cx),
8146 [
8147 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8148 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8149 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8150 ]
8151 );
8152
8153 // Ensure inserting the last character of a multi-byte bracket pair
8154 // doesn't surround the selections with the bracket.
8155 editor.handle_input("*", window, cx);
8156 assert_eq!(
8157 editor.text(cx),
8158 "
8159 *
8160 *
8161 *
8162 "
8163 .unindent()
8164 );
8165 assert_eq!(
8166 editor.selections.display_ranges(cx),
8167 [
8168 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8169 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8170 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8171 ]
8172 );
8173 });
8174}
8175
8176#[gpui::test]
8177async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8178 init_test(cx, |_| {});
8179
8180 let language = Arc::new(Language::new(
8181 LanguageConfig {
8182 brackets: BracketPairConfig {
8183 pairs: vec![BracketPair {
8184 start: "{".to_string(),
8185 end: "}".to_string(),
8186 close: true,
8187 surround: true,
8188 newline: true,
8189 }],
8190 ..Default::default()
8191 },
8192 autoclose_before: "}".to_string(),
8193 ..Default::default()
8194 },
8195 Some(tree_sitter_rust::LANGUAGE.into()),
8196 ));
8197
8198 let text = r#"
8199 a
8200 b
8201 c
8202 "#
8203 .unindent();
8204
8205 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8206 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8207 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8208 editor
8209 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8210 .await;
8211
8212 editor.update_in(cx, |editor, window, cx| {
8213 editor.change_selections(None, window, cx, |s| {
8214 s.select_ranges([
8215 Point::new(0, 1)..Point::new(0, 1),
8216 Point::new(1, 1)..Point::new(1, 1),
8217 Point::new(2, 1)..Point::new(2, 1),
8218 ])
8219 });
8220
8221 editor.handle_input("{", window, cx);
8222 editor.handle_input("{", window, cx);
8223 editor.handle_input("_", window, cx);
8224 assert_eq!(
8225 editor.text(cx),
8226 "
8227 a{{_}}
8228 b{{_}}
8229 c{{_}}
8230 "
8231 .unindent()
8232 );
8233 assert_eq!(
8234 editor.selections.ranges::<Point>(cx),
8235 [
8236 Point::new(0, 4)..Point::new(0, 4),
8237 Point::new(1, 4)..Point::new(1, 4),
8238 Point::new(2, 4)..Point::new(2, 4)
8239 ]
8240 );
8241
8242 editor.backspace(&Default::default(), window, cx);
8243 editor.backspace(&Default::default(), window, cx);
8244 assert_eq!(
8245 editor.text(cx),
8246 "
8247 a{}
8248 b{}
8249 c{}
8250 "
8251 .unindent()
8252 );
8253 assert_eq!(
8254 editor.selections.ranges::<Point>(cx),
8255 [
8256 Point::new(0, 2)..Point::new(0, 2),
8257 Point::new(1, 2)..Point::new(1, 2),
8258 Point::new(2, 2)..Point::new(2, 2)
8259 ]
8260 );
8261
8262 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8263 assert_eq!(
8264 editor.text(cx),
8265 "
8266 a
8267 b
8268 c
8269 "
8270 .unindent()
8271 );
8272 assert_eq!(
8273 editor.selections.ranges::<Point>(cx),
8274 [
8275 Point::new(0, 1)..Point::new(0, 1),
8276 Point::new(1, 1)..Point::new(1, 1),
8277 Point::new(2, 1)..Point::new(2, 1)
8278 ]
8279 );
8280 });
8281}
8282
8283#[gpui::test]
8284async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8285 init_test(cx, |settings| {
8286 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8287 });
8288
8289 let mut cx = EditorTestContext::new(cx).await;
8290
8291 let language = Arc::new(Language::new(
8292 LanguageConfig {
8293 brackets: BracketPairConfig {
8294 pairs: vec![
8295 BracketPair {
8296 start: "{".to_string(),
8297 end: "}".to_string(),
8298 close: true,
8299 surround: true,
8300 newline: true,
8301 },
8302 BracketPair {
8303 start: "(".to_string(),
8304 end: ")".to_string(),
8305 close: true,
8306 surround: true,
8307 newline: true,
8308 },
8309 BracketPair {
8310 start: "[".to_string(),
8311 end: "]".to_string(),
8312 close: false,
8313 surround: true,
8314 newline: true,
8315 },
8316 ],
8317 ..Default::default()
8318 },
8319 autoclose_before: "})]".to_string(),
8320 ..Default::default()
8321 },
8322 Some(tree_sitter_rust::LANGUAGE.into()),
8323 ));
8324
8325 cx.language_registry().add(language.clone());
8326 cx.update_buffer(|buffer, cx| {
8327 buffer.set_language(Some(language), cx);
8328 });
8329
8330 cx.set_state(
8331 &"
8332 {(ˇ)}
8333 [[ˇ]]
8334 {(ˇ)}
8335 "
8336 .unindent(),
8337 );
8338
8339 cx.update_editor(|editor, window, cx| {
8340 editor.backspace(&Default::default(), window, cx);
8341 editor.backspace(&Default::default(), window, cx);
8342 });
8343
8344 cx.assert_editor_state(
8345 &"
8346 ˇ
8347 ˇ]]
8348 ˇ
8349 "
8350 .unindent(),
8351 );
8352
8353 cx.update_editor(|editor, window, cx| {
8354 editor.handle_input("{", window, cx);
8355 editor.handle_input("{", window, cx);
8356 editor.move_right(&MoveRight, window, cx);
8357 editor.move_right(&MoveRight, window, cx);
8358 editor.move_left(&MoveLeft, window, cx);
8359 editor.move_left(&MoveLeft, window, cx);
8360 editor.backspace(&Default::default(), window, cx);
8361 });
8362
8363 cx.assert_editor_state(
8364 &"
8365 {ˇ}
8366 {ˇ}]]
8367 {ˇ}
8368 "
8369 .unindent(),
8370 );
8371
8372 cx.update_editor(|editor, window, cx| {
8373 editor.backspace(&Default::default(), window, cx);
8374 });
8375
8376 cx.assert_editor_state(
8377 &"
8378 ˇ
8379 ˇ]]
8380 ˇ
8381 "
8382 .unindent(),
8383 );
8384}
8385
8386#[gpui::test]
8387async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8388 init_test(cx, |_| {});
8389
8390 let language = Arc::new(Language::new(
8391 LanguageConfig::default(),
8392 Some(tree_sitter_rust::LANGUAGE.into()),
8393 ));
8394
8395 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8396 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8397 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8398 editor
8399 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8400 .await;
8401
8402 editor.update_in(cx, |editor, window, cx| {
8403 editor.set_auto_replace_emoji_shortcode(true);
8404
8405 editor.handle_input("Hello ", window, cx);
8406 editor.handle_input(":wave", window, cx);
8407 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8408
8409 editor.handle_input(":", window, cx);
8410 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8411
8412 editor.handle_input(" :smile", window, cx);
8413 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8414
8415 editor.handle_input(":", window, cx);
8416 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8417
8418 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8419 editor.handle_input(":wave", window, cx);
8420 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8421
8422 editor.handle_input(":", window, cx);
8423 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8424
8425 editor.handle_input(":1", window, cx);
8426 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8427
8428 editor.handle_input(":", window, cx);
8429 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8430
8431 // Ensure shortcode does not get replaced when it is part of a word
8432 editor.handle_input(" Test:wave", window, cx);
8433 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8434
8435 editor.handle_input(":", window, cx);
8436 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8437
8438 editor.set_auto_replace_emoji_shortcode(false);
8439
8440 // Ensure shortcode does not get replaced when auto replace is off
8441 editor.handle_input(" :wave", window, cx);
8442 assert_eq!(
8443 editor.text(cx),
8444 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8445 );
8446
8447 editor.handle_input(":", window, cx);
8448 assert_eq!(
8449 editor.text(cx),
8450 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8451 );
8452 });
8453}
8454
8455#[gpui::test]
8456async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8457 init_test(cx, |_| {});
8458
8459 let (text, insertion_ranges) = marked_text_ranges(
8460 indoc! {"
8461 ˇ
8462 "},
8463 false,
8464 );
8465
8466 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8467 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8468
8469 _ = editor.update_in(cx, |editor, window, cx| {
8470 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8471
8472 editor
8473 .insert_snippet(&insertion_ranges, snippet, window, cx)
8474 .unwrap();
8475
8476 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8477 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8478 assert_eq!(editor.text(cx), expected_text);
8479 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8480 }
8481
8482 assert(
8483 editor,
8484 cx,
8485 indoc! {"
8486 type «» =•
8487 "},
8488 );
8489
8490 assert!(editor.context_menu_visible(), "There should be a matches");
8491 });
8492}
8493
8494#[gpui::test]
8495async fn test_snippets(cx: &mut TestAppContext) {
8496 init_test(cx, |_| {});
8497
8498 let (text, insertion_ranges) = marked_text_ranges(
8499 indoc! {"
8500 a.ˇ b
8501 a.ˇ b
8502 a.ˇ b
8503 "},
8504 false,
8505 );
8506
8507 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8508 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8509
8510 editor.update_in(cx, |editor, window, cx| {
8511 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8512
8513 editor
8514 .insert_snippet(&insertion_ranges, snippet, window, cx)
8515 .unwrap();
8516
8517 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8518 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8519 assert_eq!(editor.text(cx), expected_text);
8520 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8521 }
8522
8523 assert(
8524 editor,
8525 cx,
8526 indoc! {"
8527 a.f(«one», two, «three») b
8528 a.f(«one», two, «three») b
8529 a.f(«one», two, «three») b
8530 "},
8531 );
8532
8533 // Can't move earlier than the first tab stop
8534 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8535 assert(
8536 editor,
8537 cx,
8538 indoc! {"
8539 a.f(«one», two, «three») b
8540 a.f(«one», two, «three») b
8541 a.f(«one», two, «three») b
8542 "},
8543 );
8544
8545 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8546 assert(
8547 editor,
8548 cx,
8549 indoc! {"
8550 a.f(one, «two», three) b
8551 a.f(one, «two», three) b
8552 a.f(one, «two», three) b
8553 "},
8554 );
8555
8556 editor.move_to_prev_snippet_tabstop(window, cx);
8557 assert(
8558 editor,
8559 cx,
8560 indoc! {"
8561 a.f(«one», two, «three») b
8562 a.f(«one», two, «three») b
8563 a.f(«one», two, «three») b
8564 "},
8565 );
8566
8567 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8568 assert(
8569 editor,
8570 cx,
8571 indoc! {"
8572 a.f(one, «two», three) b
8573 a.f(one, «two», three) b
8574 a.f(one, «two», three) b
8575 "},
8576 );
8577 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8578 assert(
8579 editor,
8580 cx,
8581 indoc! {"
8582 a.f(one, two, three)ˇ b
8583 a.f(one, two, three)ˇ b
8584 a.f(one, two, three)ˇ b
8585 "},
8586 );
8587
8588 // As soon as the last tab stop is reached, snippet state is gone
8589 editor.move_to_prev_snippet_tabstop(window, cx);
8590 assert(
8591 editor,
8592 cx,
8593 indoc! {"
8594 a.f(one, two, three)ˇ b
8595 a.f(one, two, three)ˇ b
8596 a.f(one, two, three)ˇ b
8597 "},
8598 );
8599 });
8600}
8601
8602#[gpui::test]
8603async fn test_document_format_during_save(cx: &mut TestAppContext) {
8604 init_test(cx, |_| {});
8605
8606 let fs = FakeFs::new(cx.executor());
8607 fs.insert_file(path!("/file.rs"), Default::default()).await;
8608
8609 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8610
8611 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8612 language_registry.add(rust_lang());
8613 let mut fake_servers = language_registry.register_fake_lsp(
8614 "Rust",
8615 FakeLspAdapter {
8616 capabilities: lsp::ServerCapabilities {
8617 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8618 ..Default::default()
8619 },
8620 ..Default::default()
8621 },
8622 );
8623
8624 let buffer = project
8625 .update(cx, |project, cx| {
8626 project.open_local_buffer(path!("/file.rs"), cx)
8627 })
8628 .await
8629 .unwrap();
8630
8631 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8632 let (editor, cx) = cx.add_window_view(|window, cx| {
8633 build_editor_with_project(project.clone(), buffer, window, cx)
8634 });
8635 editor.update_in(cx, |editor, window, cx| {
8636 editor.set_text("one\ntwo\nthree\n", window, cx)
8637 });
8638 assert!(cx.read(|cx| editor.is_dirty(cx)));
8639
8640 cx.executor().start_waiting();
8641 let fake_server = fake_servers.next().await.unwrap();
8642
8643 {
8644 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8645 move |params, _| async move {
8646 assert_eq!(
8647 params.text_document.uri,
8648 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8649 );
8650 assert_eq!(params.options.tab_size, 4);
8651 Ok(Some(vec![lsp::TextEdit::new(
8652 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8653 ", ".to_string(),
8654 )]))
8655 },
8656 );
8657 let save = editor
8658 .update_in(cx, |editor, window, cx| {
8659 editor.save(true, project.clone(), window, cx)
8660 })
8661 .unwrap();
8662 cx.executor().start_waiting();
8663 save.await;
8664
8665 assert_eq!(
8666 editor.update(cx, |editor, cx| editor.text(cx)),
8667 "one, two\nthree\n"
8668 );
8669 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8670 }
8671
8672 {
8673 editor.update_in(cx, |editor, window, cx| {
8674 editor.set_text("one\ntwo\nthree\n", window, cx)
8675 });
8676 assert!(cx.read(|cx| editor.is_dirty(cx)));
8677
8678 // Ensure we can still save even if formatting hangs.
8679 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8680 move |params, _| async move {
8681 assert_eq!(
8682 params.text_document.uri,
8683 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8684 );
8685 futures::future::pending::<()>().await;
8686 unreachable!()
8687 },
8688 );
8689 let save = editor
8690 .update_in(cx, |editor, window, cx| {
8691 editor.save(true, project.clone(), window, cx)
8692 })
8693 .unwrap();
8694 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8695 cx.executor().start_waiting();
8696 save.await;
8697 assert_eq!(
8698 editor.update(cx, |editor, cx| editor.text(cx)),
8699 "one\ntwo\nthree\n"
8700 );
8701 }
8702
8703 // For non-dirty buffer, no formatting request should be sent
8704 {
8705 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8706
8707 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8708 panic!("Should not be invoked on non-dirty buffer");
8709 });
8710 let save = editor
8711 .update_in(cx, |editor, window, cx| {
8712 editor.save(true, project.clone(), window, cx)
8713 })
8714 .unwrap();
8715 cx.executor().start_waiting();
8716 save.await;
8717 }
8718
8719 // Set rust language override and assert overridden tabsize is sent to language server
8720 update_test_language_settings(cx, |settings| {
8721 settings.languages.insert(
8722 "Rust".into(),
8723 LanguageSettingsContent {
8724 tab_size: NonZeroU32::new(8),
8725 ..Default::default()
8726 },
8727 );
8728 });
8729
8730 {
8731 editor.update_in(cx, |editor, window, cx| {
8732 editor.set_text("somehting_new\n", window, cx)
8733 });
8734 assert!(cx.read(|cx| editor.is_dirty(cx)));
8735 let _formatting_request_signal = fake_server
8736 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8737 assert_eq!(
8738 params.text_document.uri,
8739 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8740 );
8741 assert_eq!(params.options.tab_size, 8);
8742 Ok(Some(vec![]))
8743 });
8744 let save = editor
8745 .update_in(cx, |editor, window, cx| {
8746 editor.save(true, project.clone(), window, cx)
8747 })
8748 .unwrap();
8749 cx.executor().start_waiting();
8750 save.await;
8751 }
8752}
8753
8754#[gpui::test]
8755async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8756 init_test(cx, |_| {});
8757
8758 let cols = 4;
8759 let rows = 10;
8760 let sample_text_1 = sample_text(rows, cols, 'a');
8761 assert_eq!(
8762 sample_text_1,
8763 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8764 );
8765 let sample_text_2 = sample_text(rows, cols, 'l');
8766 assert_eq!(
8767 sample_text_2,
8768 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8769 );
8770 let sample_text_3 = sample_text(rows, cols, 'v');
8771 assert_eq!(
8772 sample_text_3,
8773 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8774 );
8775
8776 let fs = FakeFs::new(cx.executor());
8777 fs.insert_tree(
8778 path!("/a"),
8779 json!({
8780 "main.rs": sample_text_1,
8781 "other.rs": sample_text_2,
8782 "lib.rs": sample_text_3,
8783 }),
8784 )
8785 .await;
8786
8787 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8788 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8789 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8790
8791 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8792 language_registry.add(rust_lang());
8793 let mut fake_servers = language_registry.register_fake_lsp(
8794 "Rust",
8795 FakeLspAdapter {
8796 capabilities: lsp::ServerCapabilities {
8797 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8798 ..Default::default()
8799 },
8800 ..Default::default()
8801 },
8802 );
8803
8804 let worktree = project.update(cx, |project, cx| {
8805 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8806 assert_eq!(worktrees.len(), 1);
8807 worktrees.pop().unwrap()
8808 });
8809 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8810
8811 let buffer_1 = project
8812 .update(cx, |project, cx| {
8813 project.open_buffer((worktree_id, "main.rs"), cx)
8814 })
8815 .await
8816 .unwrap();
8817 let buffer_2 = project
8818 .update(cx, |project, cx| {
8819 project.open_buffer((worktree_id, "other.rs"), cx)
8820 })
8821 .await
8822 .unwrap();
8823 let buffer_3 = project
8824 .update(cx, |project, cx| {
8825 project.open_buffer((worktree_id, "lib.rs"), cx)
8826 })
8827 .await
8828 .unwrap();
8829
8830 let multi_buffer = cx.new(|cx| {
8831 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8832 multi_buffer.push_excerpts(
8833 buffer_1.clone(),
8834 [
8835 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8836 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8837 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8838 ],
8839 cx,
8840 );
8841 multi_buffer.push_excerpts(
8842 buffer_2.clone(),
8843 [
8844 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8845 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8846 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8847 ],
8848 cx,
8849 );
8850 multi_buffer.push_excerpts(
8851 buffer_3.clone(),
8852 [
8853 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8854 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8855 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8856 ],
8857 cx,
8858 );
8859 multi_buffer
8860 });
8861 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8862 Editor::new(
8863 EditorMode::full(),
8864 multi_buffer,
8865 Some(project.clone()),
8866 window,
8867 cx,
8868 )
8869 });
8870
8871 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8872 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8873 s.select_ranges(Some(1..2))
8874 });
8875 editor.insert("|one|two|three|", window, cx);
8876 });
8877 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8878 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8879 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8880 s.select_ranges(Some(60..70))
8881 });
8882 editor.insert("|four|five|six|", window, cx);
8883 });
8884 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8885
8886 // First two buffers should be edited, but not the third one.
8887 assert_eq!(
8888 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8889 "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}",
8890 );
8891 buffer_1.update(cx, |buffer, _| {
8892 assert!(buffer.is_dirty());
8893 assert_eq!(
8894 buffer.text(),
8895 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8896 )
8897 });
8898 buffer_2.update(cx, |buffer, _| {
8899 assert!(buffer.is_dirty());
8900 assert_eq!(
8901 buffer.text(),
8902 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8903 )
8904 });
8905 buffer_3.update(cx, |buffer, _| {
8906 assert!(!buffer.is_dirty());
8907 assert_eq!(buffer.text(), sample_text_3,)
8908 });
8909 cx.executor().run_until_parked();
8910
8911 cx.executor().start_waiting();
8912 let save = multi_buffer_editor
8913 .update_in(cx, |editor, window, cx| {
8914 editor.save(true, project.clone(), window, cx)
8915 })
8916 .unwrap();
8917
8918 let fake_server = fake_servers.next().await.unwrap();
8919 fake_server
8920 .server
8921 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8922 Ok(Some(vec![lsp::TextEdit::new(
8923 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8924 format!("[{} formatted]", params.text_document.uri),
8925 )]))
8926 })
8927 .detach();
8928 save.await;
8929
8930 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8931 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8932 assert_eq!(
8933 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8934 uri!(
8935 "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}"
8936 ),
8937 );
8938 buffer_1.update(cx, |buffer, _| {
8939 assert!(!buffer.is_dirty());
8940 assert_eq!(
8941 buffer.text(),
8942 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8943 )
8944 });
8945 buffer_2.update(cx, |buffer, _| {
8946 assert!(!buffer.is_dirty());
8947 assert_eq!(
8948 buffer.text(),
8949 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8950 )
8951 });
8952 buffer_3.update(cx, |buffer, _| {
8953 assert!(!buffer.is_dirty());
8954 assert_eq!(buffer.text(), sample_text_3,)
8955 });
8956}
8957
8958#[gpui::test]
8959async fn test_range_format_during_save(cx: &mut TestAppContext) {
8960 init_test(cx, |_| {});
8961
8962 let fs = FakeFs::new(cx.executor());
8963 fs.insert_file(path!("/file.rs"), Default::default()).await;
8964
8965 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8966
8967 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8968 language_registry.add(rust_lang());
8969 let mut fake_servers = language_registry.register_fake_lsp(
8970 "Rust",
8971 FakeLspAdapter {
8972 capabilities: lsp::ServerCapabilities {
8973 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8974 ..Default::default()
8975 },
8976 ..Default::default()
8977 },
8978 );
8979
8980 let buffer = project
8981 .update(cx, |project, cx| {
8982 project.open_local_buffer(path!("/file.rs"), cx)
8983 })
8984 .await
8985 .unwrap();
8986
8987 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8988 let (editor, cx) = cx.add_window_view(|window, cx| {
8989 build_editor_with_project(project.clone(), buffer, window, cx)
8990 });
8991 editor.update_in(cx, |editor, window, cx| {
8992 editor.set_text("one\ntwo\nthree\n", window, cx)
8993 });
8994 assert!(cx.read(|cx| editor.is_dirty(cx)));
8995
8996 cx.executor().start_waiting();
8997 let fake_server = fake_servers.next().await.unwrap();
8998
8999 let save = editor
9000 .update_in(cx, |editor, window, cx| {
9001 editor.save(true, project.clone(), window, cx)
9002 })
9003 .unwrap();
9004 fake_server
9005 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9006 assert_eq!(
9007 params.text_document.uri,
9008 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9009 );
9010 assert_eq!(params.options.tab_size, 4);
9011 Ok(Some(vec![lsp::TextEdit::new(
9012 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9013 ", ".to_string(),
9014 )]))
9015 })
9016 .next()
9017 .await;
9018 cx.executor().start_waiting();
9019 save.await;
9020 assert_eq!(
9021 editor.update(cx, |editor, cx| editor.text(cx)),
9022 "one, two\nthree\n"
9023 );
9024 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9025
9026 editor.update_in(cx, |editor, window, cx| {
9027 editor.set_text("one\ntwo\nthree\n", window, cx)
9028 });
9029 assert!(cx.read(|cx| editor.is_dirty(cx)));
9030
9031 // Ensure we can still save even if formatting hangs.
9032 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9033 move |params, _| async move {
9034 assert_eq!(
9035 params.text_document.uri,
9036 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9037 );
9038 futures::future::pending::<()>().await;
9039 unreachable!()
9040 },
9041 );
9042 let save = editor
9043 .update_in(cx, |editor, window, cx| {
9044 editor.save(true, project.clone(), window, cx)
9045 })
9046 .unwrap();
9047 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9048 cx.executor().start_waiting();
9049 save.await;
9050 assert_eq!(
9051 editor.update(cx, |editor, cx| editor.text(cx)),
9052 "one\ntwo\nthree\n"
9053 );
9054 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9055
9056 // For non-dirty buffer, no formatting request should be sent
9057 let save = editor
9058 .update_in(cx, |editor, window, cx| {
9059 editor.save(true, project.clone(), window, cx)
9060 })
9061 .unwrap();
9062 let _pending_format_request = fake_server
9063 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9064 panic!("Should not be invoked on non-dirty buffer");
9065 })
9066 .next();
9067 cx.executor().start_waiting();
9068 save.await;
9069
9070 // Set Rust language override and assert overridden tabsize is sent to language server
9071 update_test_language_settings(cx, |settings| {
9072 settings.languages.insert(
9073 "Rust".into(),
9074 LanguageSettingsContent {
9075 tab_size: NonZeroU32::new(8),
9076 ..Default::default()
9077 },
9078 );
9079 });
9080
9081 editor.update_in(cx, |editor, window, cx| {
9082 editor.set_text("somehting_new\n", window, cx)
9083 });
9084 assert!(cx.read(|cx| editor.is_dirty(cx)));
9085 let save = editor
9086 .update_in(cx, |editor, window, cx| {
9087 editor.save(true, project.clone(), window, cx)
9088 })
9089 .unwrap();
9090 fake_server
9091 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9092 assert_eq!(
9093 params.text_document.uri,
9094 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9095 );
9096 assert_eq!(params.options.tab_size, 8);
9097 Ok(Some(vec![]))
9098 })
9099 .next()
9100 .await;
9101 cx.executor().start_waiting();
9102 save.await;
9103}
9104
9105#[gpui::test]
9106async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9107 init_test(cx, |settings| {
9108 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9109 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9110 ))
9111 });
9112
9113 let fs = FakeFs::new(cx.executor());
9114 fs.insert_file(path!("/file.rs"), Default::default()).await;
9115
9116 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9117
9118 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9119 language_registry.add(Arc::new(Language::new(
9120 LanguageConfig {
9121 name: "Rust".into(),
9122 matcher: LanguageMatcher {
9123 path_suffixes: vec!["rs".to_string()],
9124 ..Default::default()
9125 },
9126 ..LanguageConfig::default()
9127 },
9128 Some(tree_sitter_rust::LANGUAGE.into()),
9129 )));
9130 update_test_language_settings(cx, |settings| {
9131 // Enable Prettier formatting for the same buffer, and ensure
9132 // LSP is called instead of Prettier.
9133 settings.defaults.prettier = Some(PrettierSettings {
9134 allowed: true,
9135 ..PrettierSettings::default()
9136 });
9137 });
9138 let mut fake_servers = language_registry.register_fake_lsp(
9139 "Rust",
9140 FakeLspAdapter {
9141 capabilities: lsp::ServerCapabilities {
9142 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9143 ..Default::default()
9144 },
9145 ..Default::default()
9146 },
9147 );
9148
9149 let buffer = project
9150 .update(cx, |project, cx| {
9151 project.open_local_buffer(path!("/file.rs"), cx)
9152 })
9153 .await
9154 .unwrap();
9155
9156 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9157 let (editor, cx) = cx.add_window_view(|window, cx| {
9158 build_editor_with_project(project.clone(), buffer, window, cx)
9159 });
9160 editor.update_in(cx, |editor, window, cx| {
9161 editor.set_text("one\ntwo\nthree\n", window, cx)
9162 });
9163
9164 cx.executor().start_waiting();
9165 let fake_server = fake_servers.next().await.unwrap();
9166
9167 let format = editor
9168 .update_in(cx, |editor, window, cx| {
9169 editor.perform_format(
9170 project.clone(),
9171 FormatTrigger::Manual,
9172 FormatTarget::Buffers,
9173 window,
9174 cx,
9175 )
9176 })
9177 .unwrap();
9178 fake_server
9179 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9180 assert_eq!(
9181 params.text_document.uri,
9182 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9183 );
9184 assert_eq!(params.options.tab_size, 4);
9185 Ok(Some(vec![lsp::TextEdit::new(
9186 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9187 ", ".to_string(),
9188 )]))
9189 })
9190 .next()
9191 .await;
9192 cx.executor().start_waiting();
9193 format.await;
9194 assert_eq!(
9195 editor.update(cx, |editor, cx| editor.text(cx)),
9196 "one, two\nthree\n"
9197 );
9198
9199 editor.update_in(cx, |editor, window, cx| {
9200 editor.set_text("one\ntwo\nthree\n", window, cx)
9201 });
9202 // Ensure we don't lock if formatting hangs.
9203 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9204 move |params, _| async move {
9205 assert_eq!(
9206 params.text_document.uri,
9207 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9208 );
9209 futures::future::pending::<()>().await;
9210 unreachable!()
9211 },
9212 );
9213 let format = editor
9214 .update_in(cx, |editor, window, cx| {
9215 editor.perform_format(
9216 project,
9217 FormatTrigger::Manual,
9218 FormatTarget::Buffers,
9219 window,
9220 cx,
9221 )
9222 })
9223 .unwrap();
9224 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9225 cx.executor().start_waiting();
9226 format.await;
9227 assert_eq!(
9228 editor.update(cx, |editor, cx| editor.text(cx)),
9229 "one\ntwo\nthree\n"
9230 );
9231}
9232
9233#[gpui::test]
9234async fn test_multiple_formatters(cx: &mut TestAppContext) {
9235 init_test(cx, |settings| {
9236 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9237 settings.defaults.formatter =
9238 Some(language_settings::SelectedFormatter::List(FormatterList(
9239 vec![
9240 Formatter::LanguageServer { name: None },
9241 Formatter::CodeActions(
9242 [
9243 ("code-action-1".into(), true),
9244 ("code-action-2".into(), true),
9245 ]
9246 .into_iter()
9247 .collect(),
9248 ),
9249 ]
9250 .into(),
9251 )))
9252 });
9253
9254 let fs = FakeFs::new(cx.executor());
9255 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9256 .await;
9257
9258 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9259 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9260 language_registry.add(rust_lang());
9261
9262 let mut fake_servers = language_registry.register_fake_lsp(
9263 "Rust",
9264 FakeLspAdapter {
9265 capabilities: lsp::ServerCapabilities {
9266 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9267 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9268 commands: vec!["the-command-for-code-action-1".into()],
9269 ..Default::default()
9270 }),
9271 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9272 ..Default::default()
9273 },
9274 ..Default::default()
9275 },
9276 );
9277
9278 let buffer = project
9279 .update(cx, |project, cx| {
9280 project.open_local_buffer(path!("/file.rs"), cx)
9281 })
9282 .await
9283 .unwrap();
9284
9285 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9286 let (editor, cx) = cx.add_window_view(|window, cx| {
9287 build_editor_with_project(project.clone(), buffer, window, cx)
9288 });
9289
9290 cx.executor().start_waiting();
9291
9292 let fake_server = fake_servers.next().await.unwrap();
9293 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9294 move |_params, _| async move {
9295 Ok(Some(vec![lsp::TextEdit::new(
9296 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9297 "applied-formatting\n".to_string(),
9298 )]))
9299 },
9300 );
9301 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9302 move |params, _| async move {
9303 assert_eq!(
9304 params.context.only,
9305 Some(vec!["code-action-1".into(), "code-action-2".into()])
9306 );
9307 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9308 Ok(Some(vec![
9309 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9310 kind: Some("code-action-1".into()),
9311 edit: Some(lsp::WorkspaceEdit::new(
9312 [(
9313 uri.clone(),
9314 vec![lsp::TextEdit::new(
9315 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9316 "applied-code-action-1-edit\n".to_string(),
9317 )],
9318 )]
9319 .into_iter()
9320 .collect(),
9321 )),
9322 command: Some(lsp::Command {
9323 command: "the-command-for-code-action-1".into(),
9324 ..Default::default()
9325 }),
9326 ..Default::default()
9327 }),
9328 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9329 kind: Some("code-action-2".into()),
9330 edit: Some(lsp::WorkspaceEdit::new(
9331 [(
9332 uri.clone(),
9333 vec![lsp::TextEdit::new(
9334 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9335 "applied-code-action-2-edit\n".to_string(),
9336 )],
9337 )]
9338 .into_iter()
9339 .collect(),
9340 )),
9341 ..Default::default()
9342 }),
9343 ]))
9344 },
9345 );
9346
9347 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9348 move |params, _| async move { Ok(params) }
9349 });
9350
9351 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9352 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9353 let fake = fake_server.clone();
9354 let lock = command_lock.clone();
9355 move |params, _| {
9356 assert_eq!(params.command, "the-command-for-code-action-1");
9357 let fake = fake.clone();
9358 let lock = lock.clone();
9359 async move {
9360 lock.lock().await;
9361 fake.server
9362 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9363 label: None,
9364 edit: lsp::WorkspaceEdit {
9365 changes: Some(
9366 [(
9367 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9368 vec![lsp::TextEdit {
9369 range: lsp::Range::new(
9370 lsp::Position::new(0, 0),
9371 lsp::Position::new(0, 0),
9372 ),
9373 new_text: "applied-code-action-1-command\n".into(),
9374 }],
9375 )]
9376 .into_iter()
9377 .collect(),
9378 ),
9379 ..Default::default()
9380 },
9381 })
9382 .await
9383 .into_response()
9384 .unwrap();
9385 Ok(Some(json!(null)))
9386 }
9387 }
9388 });
9389
9390 cx.executor().start_waiting();
9391 editor
9392 .update_in(cx, |editor, window, cx| {
9393 editor.perform_format(
9394 project.clone(),
9395 FormatTrigger::Manual,
9396 FormatTarget::Buffers,
9397 window,
9398 cx,
9399 )
9400 })
9401 .unwrap()
9402 .await;
9403 editor.update(cx, |editor, cx| {
9404 assert_eq!(
9405 editor.text(cx),
9406 r#"
9407 applied-code-action-2-edit
9408 applied-code-action-1-command
9409 applied-code-action-1-edit
9410 applied-formatting
9411 one
9412 two
9413 three
9414 "#
9415 .unindent()
9416 );
9417 });
9418
9419 editor.update_in(cx, |editor, window, cx| {
9420 editor.undo(&Default::default(), window, cx);
9421 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9422 });
9423
9424 // Perform a manual edit while waiting for an LSP command
9425 // that's being run as part of a formatting code action.
9426 let lock_guard = command_lock.lock().await;
9427 let format = editor
9428 .update_in(cx, |editor, window, cx| {
9429 editor.perform_format(
9430 project.clone(),
9431 FormatTrigger::Manual,
9432 FormatTarget::Buffers,
9433 window,
9434 cx,
9435 )
9436 })
9437 .unwrap();
9438 cx.run_until_parked();
9439 editor.update(cx, |editor, cx| {
9440 assert_eq!(
9441 editor.text(cx),
9442 r#"
9443 applied-code-action-1-edit
9444 applied-formatting
9445 one
9446 two
9447 three
9448 "#
9449 .unindent()
9450 );
9451
9452 editor.buffer.update(cx, |buffer, cx| {
9453 let ix = buffer.len(cx);
9454 buffer.edit([(ix..ix, "edited\n")], None, cx);
9455 });
9456 });
9457
9458 // Allow the LSP command to proceed. Because the buffer was edited,
9459 // the second code action will not be run.
9460 drop(lock_guard);
9461 format.await;
9462 editor.update_in(cx, |editor, window, cx| {
9463 assert_eq!(
9464 editor.text(cx),
9465 r#"
9466 applied-code-action-1-command
9467 applied-code-action-1-edit
9468 applied-formatting
9469 one
9470 two
9471 three
9472 edited
9473 "#
9474 .unindent()
9475 );
9476
9477 // The manual edit is undone first, because it is the last thing the user did
9478 // (even though the command completed afterwards).
9479 editor.undo(&Default::default(), 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 "#
9490 .unindent()
9491 );
9492
9493 // All the formatting (including the command, which completed after the manual edit)
9494 // is undone together.
9495 editor.undo(&Default::default(), window, cx);
9496 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9497 });
9498}
9499
9500#[gpui::test]
9501async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9502 init_test(cx, |settings| {
9503 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9504 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9505 ))
9506 });
9507
9508 let fs = FakeFs::new(cx.executor());
9509 fs.insert_file(path!("/file.ts"), Default::default()).await;
9510
9511 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9512
9513 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9514 language_registry.add(Arc::new(Language::new(
9515 LanguageConfig {
9516 name: "TypeScript".into(),
9517 matcher: LanguageMatcher {
9518 path_suffixes: vec!["ts".to_string()],
9519 ..Default::default()
9520 },
9521 ..LanguageConfig::default()
9522 },
9523 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9524 )));
9525 update_test_language_settings(cx, |settings| {
9526 settings.defaults.prettier = Some(PrettierSettings {
9527 allowed: true,
9528 ..PrettierSettings::default()
9529 });
9530 });
9531 let mut fake_servers = language_registry.register_fake_lsp(
9532 "TypeScript",
9533 FakeLspAdapter {
9534 capabilities: lsp::ServerCapabilities {
9535 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9536 ..Default::default()
9537 },
9538 ..Default::default()
9539 },
9540 );
9541
9542 let buffer = project
9543 .update(cx, |project, cx| {
9544 project.open_local_buffer(path!("/file.ts"), cx)
9545 })
9546 .await
9547 .unwrap();
9548
9549 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9550 let (editor, cx) = cx.add_window_view(|window, cx| {
9551 build_editor_with_project(project.clone(), buffer, window, cx)
9552 });
9553 editor.update_in(cx, |editor, window, cx| {
9554 editor.set_text(
9555 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9556 window,
9557 cx,
9558 )
9559 });
9560
9561 cx.executor().start_waiting();
9562 let fake_server = fake_servers.next().await.unwrap();
9563
9564 let format = editor
9565 .update_in(cx, |editor, window, cx| {
9566 editor.perform_code_action_kind(
9567 project.clone(),
9568 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9569 window,
9570 cx,
9571 )
9572 })
9573 .unwrap();
9574 fake_server
9575 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9576 assert_eq!(
9577 params.text_document.uri,
9578 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9579 );
9580 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9581 lsp::CodeAction {
9582 title: "Organize Imports".to_string(),
9583 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9584 edit: Some(lsp::WorkspaceEdit {
9585 changes: Some(
9586 [(
9587 params.text_document.uri.clone(),
9588 vec![lsp::TextEdit::new(
9589 lsp::Range::new(
9590 lsp::Position::new(1, 0),
9591 lsp::Position::new(2, 0),
9592 ),
9593 "".to_string(),
9594 )],
9595 )]
9596 .into_iter()
9597 .collect(),
9598 ),
9599 ..Default::default()
9600 }),
9601 ..Default::default()
9602 },
9603 )]))
9604 })
9605 .next()
9606 .await;
9607 cx.executor().start_waiting();
9608 format.await;
9609 assert_eq!(
9610 editor.update(cx, |editor, cx| editor.text(cx)),
9611 "import { a } from 'module';\n\nconst x = a;\n"
9612 );
9613
9614 editor.update_in(cx, |editor, window, cx| {
9615 editor.set_text(
9616 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9617 window,
9618 cx,
9619 )
9620 });
9621 // Ensure we don't lock if code action hangs.
9622 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9623 move |params, _| async move {
9624 assert_eq!(
9625 params.text_document.uri,
9626 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9627 );
9628 futures::future::pending::<()>().await;
9629 unreachable!()
9630 },
9631 );
9632 let format = editor
9633 .update_in(cx, |editor, window, cx| {
9634 editor.perform_code_action_kind(
9635 project,
9636 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9637 window,
9638 cx,
9639 )
9640 })
9641 .unwrap();
9642 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9643 cx.executor().start_waiting();
9644 format.await;
9645 assert_eq!(
9646 editor.update(cx, |editor, cx| editor.text(cx)),
9647 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9648 );
9649}
9650
9651#[gpui::test]
9652async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9653 init_test(cx, |_| {});
9654
9655 let mut cx = EditorLspTestContext::new_rust(
9656 lsp::ServerCapabilities {
9657 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9658 ..Default::default()
9659 },
9660 cx,
9661 )
9662 .await;
9663
9664 cx.set_state(indoc! {"
9665 one.twoˇ
9666 "});
9667
9668 // The format request takes a long time. When it completes, it inserts
9669 // a newline and an indent before the `.`
9670 cx.lsp
9671 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9672 let executor = cx.background_executor().clone();
9673 async move {
9674 executor.timer(Duration::from_millis(100)).await;
9675 Ok(Some(vec![lsp::TextEdit {
9676 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9677 new_text: "\n ".into(),
9678 }]))
9679 }
9680 });
9681
9682 // Submit a format request.
9683 let format_1 = cx
9684 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9685 .unwrap();
9686 cx.executor().run_until_parked();
9687
9688 // Submit a second format request.
9689 let format_2 = cx
9690 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9691 .unwrap();
9692 cx.executor().run_until_parked();
9693
9694 // Wait for both format requests to complete
9695 cx.executor().advance_clock(Duration::from_millis(200));
9696 cx.executor().start_waiting();
9697 format_1.await.unwrap();
9698 cx.executor().start_waiting();
9699 format_2.await.unwrap();
9700
9701 // The formatting edits only happens once.
9702 cx.assert_editor_state(indoc! {"
9703 one
9704 .twoˇ
9705 "});
9706}
9707
9708#[gpui::test]
9709async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9710 init_test(cx, |settings| {
9711 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9712 });
9713
9714 let mut cx = EditorLspTestContext::new_rust(
9715 lsp::ServerCapabilities {
9716 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9717 ..Default::default()
9718 },
9719 cx,
9720 )
9721 .await;
9722
9723 // Set up a buffer white some trailing whitespace and no trailing newline.
9724 cx.set_state(
9725 &[
9726 "one ", //
9727 "twoˇ", //
9728 "three ", //
9729 "four", //
9730 ]
9731 .join("\n"),
9732 );
9733
9734 // Submit a format request.
9735 let format = cx
9736 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9737 .unwrap();
9738
9739 // Record which buffer changes have been sent to the language server
9740 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9741 cx.lsp
9742 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9743 let buffer_changes = buffer_changes.clone();
9744 move |params, _| {
9745 buffer_changes.lock().extend(
9746 params
9747 .content_changes
9748 .into_iter()
9749 .map(|e| (e.range.unwrap(), e.text)),
9750 );
9751 }
9752 });
9753
9754 // Handle formatting requests to the language server.
9755 cx.lsp
9756 .set_request_handler::<lsp::request::Formatting, _, _>({
9757 let buffer_changes = buffer_changes.clone();
9758 move |_, _| {
9759 // When formatting is requested, trailing whitespace has already been stripped,
9760 // and the trailing newline has already been added.
9761 assert_eq!(
9762 &buffer_changes.lock()[1..],
9763 &[
9764 (
9765 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9766 "".into()
9767 ),
9768 (
9769 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9770 "".into()
9771 ),
9772 (
9773 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9774 "\n".into()
9775 ),
9776 ]
9777 );
9778
9779 // Insert blank lines between each line of the buffer.
9780 async move {
9781 Ok(Some(vec![
9782 lsp::TextEdit {
9783 range: lsp::Range::new(
9784 lsp::Position::new(1, 0),
9785 lsp::Position::new(1, 0),
9786 ),
9787 new_text: "\n".into(),
9788 },
9789 lsp::TextEdit {
9790 range: lsp::Range::new(
9791 lsp::Position::new(2, 0),
9792 lsp::Position::new(2, 0),
9793 ),
9794 new_text: "\n".into(),
9795 },
9796 ]))
9797 }
9798 }
9799 });
9800
9801 // After formatting the buffer, the trailing whitespace is stripped,
9802 // a newline is appended, and the edits provided by the language server
9803 // have been applied.
9804 format.await.unwrap();
9805 cx.assert_editor_state(
9806 &[
9807 "one", //
9808 "", //
9809 "twoˇ", //
9810 "", //
9811 "three", //
9812 "four", //
9813 "", //
9814 ]
9815 .join("\n"),
9816 );
9817
9818 // Undoing the formatting undoes the trailing whitespace removal, the
9819 // trailing newline, and the LSP edits.
9820 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9821 cx.assert_editor_state(
9822 &[
9823 "one ", //
9824 "twoˇ", //
9825 "three ", //
9826 "four", //
9827 ]
9828 .join("\n"),
9829 );
9830}
9831
9832#[gpui::test]
9833async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9834 cx: &mut TestAppContext,
9835) {
9836 init_test(cx, |_| {});
9837
9838 cx.update(|cx| {
9839 cx.update_global::<SettingsStore, _>(|settings, cx| {
9840 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9841 settings.auto_signature_help = Some(true);
9842 });
9843 });
9844 });
9845
9846 let mut cx = EditorLspTestContext::new_rust(
9847 lsp::ServerCapabilities {
9848 signature_help_provider: Some(lsp::SignatureHelpOptions {
9849 ..Default::default()
9850 }),
9851 ..Default::default()
9852 },
9853 cx,
9854 )
9855 .await;
9856
9857 let language = Language::new(
9858 LanguageConfig {
9859 name: "Rust".into(),
9860 brackets: BracketPairConfig {
9861 pairs: vec![
9862 BracketPair {
9863 start: "{".to_string(),
9864 end: "}".to_string(),
9865 close: true,
9866 surround: true,
9867 newline: true,
9868 },
9869 BracketPair {
9870 start: "(".to_string(),
9871 end: ")".to_string(),
9872 close: true,
9873 surround: true,
9874 newline: true,
9875 },
9876 BracketPair {
9877 start: "/*".to_string(),
9878 end: " */".to_string(),
9879 close: true,
9880 surround: true,
9881 newline: true,
9882 },
9883 BracketPair {
9884 start: "[".to_string(),
9885 end: "]".to_string(),
9886 close: false,
9887 surround: false,
9888 newline: true,
9889 },
9890 BracketPair {
9891 start: "\"".to_string(),
9892 end: "\"".to_string(),
9893 close: true,
9894 surround: true,
9895 newline: false,
9896 },
9897 BracketPair {
9898 start: "<".to_string(),
9899 end: ">".to_string(),
9900 close: false,
9901 surround: true,
9902 newline: true,
9903 },
9904 ],
9905 ..Default::default()
9906 },
9907 autoclose_before: "})]".to_string(),
9908 ..Default::default()
9909 },
9910 Some(tree_sitter_rust::LANGUAGE.into()),
9911 );
9912 let language = Arc::new(language);
9913
9914 cx.language_registry().add(language.clone());
9915 cx.update_buffer(|buffer, cx| {
9916 buffer.set_language(Some(language), cx);
9917 });
9918
9919 cx.set_state(
9920 &r#"
9921 fn main() {
9922 sampleˇ
9923 }
9924 "#
9925 .unindent(),
9926 );
9927
9928 cx.update_editor(|editor, window, cx| {
9929 editor.handle_input("(", window, cx);
9930 });
9931 cx.assert_editor_state(
9932 &"
9933 fn main() {
9934 sample(ˇ)
9935 }
9936 "
9937 .unindent(),
9938 );
9939
9940 let mocked_response = lsp::SignatureHelp {
9941 signatures: vec![lsp::SignatureInformation {
9942 label: "fn sample(param1: u8, param2: u8)".to_string(),
9943 documentation: None,
9944 parameters: Some(vec![
9945 lsp::ParameterInformation {
9946 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9947 documentation: None,
9948 },
9949 lsp::ParameterInformation {
9950 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9951 documentation: None,
9952 },
9953 ]),
9954 active_parameter: None,
9955 }],
9956 active_signature: Some(0),
9957 active_parameter: Some(0),
9958 };
9959 handle_signature_help_request(&mut cx, mocked_response).await;
9960
9961 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9962 .await;
9963
9964 cx.editor(|editor, _, _| {
9965 let signature_help_state = editor.signature_help_state.popover().cloned();
9966 assert_eq!(
9967 signature_help_state.unwrap().label,
9968 "param1: u8, param2: u8"
9969 );
9970 });
9971}
9972
9973#[gpui::test]
9974async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9975 init_test(cx, |_| {});
9976
9977 cx.update(|cx| {
9978 cx.update_global::<SettingsStore, _>(|settings, cx| {
9979 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9980 settings.auto_signature_help = Some(false);
9981 settings.show_signature_help_after_edits = Some(false);
9982 });
9983 });
9984 });
9985
9986 let mut cx = EditorLspTestContext::new_rust(
9987 lsp::ServerCapabilities {
9988 signature_help_provider: Some(lsp::SignatureHelpOptions {
9989 ..Default::default()
9990 }),
9991 ..Default::default()
9992 },
9993 cx,
9994 )
9995 .await;
9996
9997 let language = Language::new(
9998 LanguageConfig {
9999 name: "Rust".into(),
10000 brackets: BracketPairConfig {
10001 pairs: vec![
10002 BracketPair {
10003 start: "{".to_string(),
10004 end: "}".to_string(),
10005 close: true,
10006 surround: true,
10007 newline: true,
10008 },
10009 BracketPair {
10010 start: "(".to_string(),
10011 end: ")".to_string(),
10012 close: true,
10013 surround: true,
10014 newline: true,
10015 },
10016 BracketPair {
10017 start: "/*".to_string(),
10018 end: " */".to_string(),
10019 close: true,
10020 surround: true,
10021 newline: true,
10022 },
10023 BracketPair {
10024 start: "[".to_string(),
10025 end: "]".to_string(),
10026 close: false,
10027 surround: false,
10028 newline: true,
10029 },
10030 BracketPair {
10031 start: "\"".to_string(),
10032 end: "\"".to_string(),
10033 close: true,
10034 surround: true,
10035 newline: false,
10036 },
10037 BracketPair {
10038 start: "<".to_string(),
10039 end: ">".to_string(),
10040 close: false,
10041 surround: true,
10042 newline: true,
10043 },
10044 ],
10045 ..Default::default()
10046 },
10047 autoclose_before: "})]".to_string(),
10048 ..Default::default()
10049 },
10050 Some(tree_sitter_rust::LANGUAGE.into()),
10051 );
10052 let language = Arc::new(language);
10053
10054 cx.language_registry().add(language.clone());
10055 cx.update_buffer(|buffer, cx| {
10056 buffer.set_language(Some(language), cx);
10057 });
10058
10059 // Ensure that signature_help is not called when no signature help is enabled.
10060 cx.set_state(
10061 &r#"
10062 fn main() {
10063 sampleˇ
10064 }
10065 "#
10066 .unindent(),
10067 );
10068 cx.update_editor(|editor, window, cx| {
10069 editor.handle_input("(", window, cx);
10070 });
10071 cx.assert_editor_state(
10072 &"
10073 fn main() {
10074 sample(ˇ)
10075 }
10076 "
10077 .unindent(),
10078 );
10079 cx.editor(|editor, _, _| {
10080 assert!(editor.signature_help_state.task().is_none());
10081 });
10082
10083 let mocked_response = lsp::SignatureHelp {
10084 signatures: vec![lsp::SignatureInformation {
10085 label: "fn sample(param1: u8, param2: u8)".to_string(),
10086 documentation: None,
10087 parameters: Some(vec![
10088 lsp::ParameterInformation {
10089 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10090 documentation: None,
10091 },
10092 lsp::ParameterInformation {
10093 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10094 documentation: None,
10095 },
10096 ]),
10097 active_parameter: None,
10098 }],
10099 active_signature: Some(0),
10100 active_parameter: Some(0),
10101 };
10102
10103 // Ensure that signature_help is called when enabled afte edits
10104 cx.update(|_, cx| {
10105 cx.update_global::<SettingsStore, _>(|settings, cx| {
10106 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10107 settings.auto_signature_help = Some(false);
10108 settings.show_signature_help_after_edits = Some(true);
10109 });
10110 });
10111 });
10112 cx.set_state(
10113 &r#"
10114 fn main() {
10115 sampleˇ
10116 }
10117 "#
10118 .unindent(),
10119 );
10120 cx.update_editor(|editor, window, cx| {
10121 editor.handle_input("(", window, cx);
10122 });
10123 cx.assert_editor_state(
10124 &"
10125 fn main() {
10126 sample(ˇ)
10127 }
10128 "
10129 .unindent(),
10130 );
10131 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10132 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10133 .await;
10134 cx.update_editor(|editor, _, _| {
10135 let signature_help_state = editor.signature_help_state.popover().cloned();
10136 assert!(signature_help_state.is_some());
10137 assert_eq!(
10138 signature_help_state.unwrap().label,
10139 "param1: u8, param2: u8"
10140 );
10141 editor.signature_help_state = SignatureHelpState::default();
10142 });
10143
10144 // Ensure that signature_help is called when auto signature help override is enabled
10145 cx.update(|_, cx| {
10146 cx.update_global::<SettingsStore, _>(|settings, cx| {
10147 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10148 settings.auto_signature_help = Some(true);
10149 settings.show_signature_help_after_edits = Some(false);
10150 });
10151 });
10152 });
10153 cx.set_state(
10154 &r#"
10155 fn main() {
10156 sampleˇ
10157 }
10158 "#
10159 .unindent(),
10160 );
10161 cx.update_editor(|editor, window, cx| {
10162 editor.handle_input("(", window, cx);
10163 });
10164 cx.assert_editor_state(
10165 &"
10166 fn main() {
10167 sample(ˇ)
10168 }
10169 "
10170 .unindent(),
10171 );
10172 handle_signature_help_request(&mut cx, mocked_response).await;
10173 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10174 .await;
10175 cx.editor(|editor, _, _| {
10176 let signature_help_state = editor.signature_help_state.popover().cloned();
10177 assert!(signature_help_state.is_some());
10178 assert_eq!(
10179 signature_help_state.unwrap().label,
10180 "param1: u8, param2: u8"
10181 );
10182 });
10183}
10184
10185#[gpui::test]
10186async fn test_signature_help(cx: &mut TestAppContext) {
10187 init_test(cx, |_| {});
10188 cx.update(|cx| {
10189 cx.update_global::<SettingsStore, _>(|settings, cx| {
10190 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10191 settings.auto_signature_help = Some(true);
10192 });
10193 });
10194 });
10195
10196 let mut cx = EditorLspTestContext::new_rust(
10197 lsp::ServerCapabilities {
10198 signature_help_provider: Some(lsp::SignatureHelpOptions {
10199 ..Default::default()
10200 }),
10201 ..Default::default()
10202 },
10203 cx,
10204 )
10205 .await;
10206
10207 // A test that directly calls `show_signature_help`
10208 cx.update_editor(|editor, window, cx| {
10209 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10210 });
10211
10212 let mocked_response = lsp::SignatureHelp {
10213 signatures: vec![lsp::SignatureInformation {
10214 label: "fn sample(param1: u8, param2: u8)".to_string(),
10215 documentation: None,
10216 parameters: Some(vec![
10217 lsp::ParameterInformation {
10218 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10219 documentation: None,
10220 },
10221 lsp::ParameterInformation {
10222 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10223 documentation: None,
10224 },
10225 ]),
10226 active_parameter: None,
10227 }],
10228 active_signature: Some(0),
10229 active_parameter: Some(0),
10230 };
10231 handle_signature_help_request(&mut cx, mocked_response).await;
10232
10233 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10234 .await;
10235
10236 cx.editor(|editor, _, _| {
10237 let signature_help_state = editor.signature_help_state.popover().cloned();
10238 assert!(signature_help_state.is_some());
10239 assert_eq!(
10240 signature_help_state.unwrap().label,
10241 "param1: u8, param2: u8"
10242 );
10243 });
10244
10245 // When exiting outside from inside the brackets, `signature_help` is closed.
10246 cx.set_state(indoc! {"
10247 fn main() {
10248 sample(ˇ);
10249 }
10250
10251 fn sample(param1: u8, param2: u8) {}
10252 "});
10253
10254 cx.update_editor(|editor, window, cx| {
10255 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10256 });
10257
10258 let mocked_response = lsp::SignatureHelp {
10259 signatures: Vec::new(),
10260 active_signature: None,
10261 active_parameter: None,
10262 };
10263 handle_signature_help_request(&mut cx, mocked_response).await;
10264
10265 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10266 .await;
10267
10268 cx.editor(|editor, _, _| {
10269 assert!(!editor.signature_help_state.is_shown());
10270 });
10271
10272 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10273 cx.set_state(indoc! {"
10274 fn main() {
10275 sample(ˇ);
10276 }
10277
10278 fn sample(param1: u8, param2: u8) {}
10279 "});
10280
10281 let mocked_response = lsp::SignatureHelp {
10282 signatures: vec![lsp::SignatureInformation {
10283 label: "fn sample(param1: u8, param2: u8)".to_string(),
10284 documentation: None,
10285 parameters: Some(vec![
10286 lsp::ParameterInformation {
10287 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10288 documentation: None,
10289 },
10290 lsp::ParameterInformation {
10291 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10292 documentation: None,
10293 },
10294 ]),
10295 active_parameter: None,
10296 }],
10297 active_signature: Some(0),
10298 active_parameter: Some(0),
10299 };
10300 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10301 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10302 .await;
10303 cx.editor(|editor, _, _| {
10304 assert!(editor.signature_help_state.is_shown());
10305 });
10306
10307 // Restore the popover with more parameter input
10308 cx.set_state(indoc! {"
10309 fn main() {
10310 sample(param1, param2ˇ);
10311 }
10312
10313 fn sample(param1: u8, param2: u8) {}
10314 "});
10315
10316 let mocked_response = lsp::SignatureHelp {
10317 signatures: vec![lsp::SignatureInformation {
10318 label: "fn sample(param1: u8, param2: u8)".to_string(),
10319 documentation: None,
10320 parameters: Some(vec![
10321 lsp::ParameterInformation {
10322 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10323 documentation: None,
10324 },
10325 lsp::ParameterInformation {
10326 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10327 documentation: None,
10328 },
10329 ]),
10330 active_parameter: None,
10331 }],
10332 active_signature: Some(0),
10333 active_parameter: Some(1),
10334 };
10335 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10336 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10337 .await;
10338
10339 // When selecting a range, the popover is gone.
10340 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10341 cx.update_editor(|editor, window, cx| {
10342 editor.change_selections(None, window, cx, |s| {
10343 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10344 })
10345 });
10346 cx.assert_editor_state(indoc! {"
10347 fn main() {
10348 sample(param1, «ˇparam2»);
10349 }
10350
10351 fn sample(param1: u8, param2: u8) {}
10352 "});
10353 cx.editor(|editor, _, _| {
10354 assert!(!editor.signature_help_state.is_shown());
10355 });
10356
10357 // When unselecting again, the popover is back if within the brackets.
10358 cx.update_editor(|editor, window, cx| {
10359 editor.change_selections(None, window, cx, |s| {
10360 s.select_ranges(Some(Point::new(1, 19)..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 handle_signature_help_request(&mut cx, mocked_response).await;
10371 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10372 .await;
10373 cx.editor(|editor, _, _| {
10374 assert!(editor.signature_help_state.is_shown());
10375 });
10376
10377 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10378 cx.update_editor(|editor, window, cx| {
10379 editor.change_selections(None, window, cx, |s| {
10380 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10381 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10382 })
10383 });
10384 cx.assert_editor_state(indoc! {"
10385 fn main() {
10386 sample(param1, ˇparam2);
10387 }
10388
10389 fn sample(param1: u8, param2: u8) {}
10390 "});
10391
10392 let mocked_response = lsp::SignatureHelp {
10393 signatures: vec![lsp::SignatureInformation {
10394 label: "fn sample(param1: u8, param2: u8)".to_string(),
10395 documentation: None,
10396 parameters: Some(vec![
10397 lsp::ParameterInformation {
10398 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10399 documentation: None,
10400 },
10401 lsp::ParameterInformation {
10402 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10403 documentation: None,
10404 },
10405 ]),
10406 active_parameter: None,
10407 }],
10408 active_signature: Some(0),
10409 active_parameter: Some(1),
10410 };
10411 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10412 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10413 .await;
10414 cx.update_editor(|editor, _, cx| {
10415 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10416 });
10417 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10418 .await;
10419 cx.update_editor(|editor, window, cx| {
10420 editor.change_selections(None, window, cx, |s| {
10421 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10422 })
10423 });
10424 cx.assert_editor_state(indoc! {"
10425 fn main() {
10426 sample(param1, «ˇparam2»);
10427 }
10428
10429 fn sample(param1: u8, param2: u8) {}
10430 "});
10431 cx.update_editor(|editor, window, cx| {
10432 editor.change_selections(None, window, cx, |s| {
10433 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10434 })
10435 });
10436 cx.assert_editor_state(indoc! {"
10437 fn main() {
10438 sample(param1, ˇparam2);
10439 }
10440
10441 fn sample(param1: u8, param2: u8) {}
10442 "});
10443 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10444 .await;
10445}
10446
10447#[gpui::test]
10448async fn test_completion_mode(cx: &mut TestAppContext) {
10449 init_test(cx, |_| {});
10450 let mut cx = EditorLspTestContext::new_rust(
10451 lsp::ServerCapabilities {
10452 completion_provider: Some(lsp::CompletionOptions {
10453 resolve_provider: Some(true),
10454 ..Default::default()
10455 }),
10456 ..Default::default()
10457 },
10458 cx,
10459 )
10460 .await;
10461
10462 struct Run {
10463 run_description: &'static str,
10464 initial_state: String,
10465 buffer_marked_text: String,
10466 completion_text: &'static str,
10467 expected_with_insert_mode: String,
10468 expected_with_replace_mode: String,
10469 expected_with_replace_subsequence_mode: String,
10470 expected_with_replace_suffix_mode: String,
10471 }
10472
10473 let runs = [
10474 Run {
10475 run_description: "Start of word matches completion text",
10476 initial_state: "before ediˇ after".into(),
10477 buffer_marked_text: "before <edi|> after".into(),
10478 completion_text: "editor",
10479 expected_with_insert_mode: "before editorˇ after".into(),
10480 expected_with_replace_mode: "before editorˇ after".into(),
10481 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10482 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10483 },
10484 Run {
10485 run_description: "Accept same text at the middle of the word",
10486 initial_state: "before ediˇtor after".into(),
10487 buffer_marked_text: "before <edi|tor> after".into(),
10488 completion_text: "editor",
10489 expected_with_insert_mode: "before editorˇtor after".into(),
10490 expected_with_replace_mode: "before editorˇ after".into(),
10491 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10492 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10493 },
10494 Run {
10495 run_description: "End of word matches completion text -- cursor at end",
10496 initial_state: "before torˇ after".into(),
10497 buffer_marked_text: "before <tor|> after".into(),
10498 completion_text: "editor",
10499 expected_with_insert_mode: "before editorˇ after".into(),
10500 expected_with_replace_mode: "before editorˇ after".into(),
10501 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10502 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10503 },
10504 Run {
10505 run_description: "End of word matches completion text -- cursor at start",
10506 initial_state: "before ˇtor after".into(),
10507 buffer_marked_text: "before <|tor> after".into(),
10508 completion_text: "editor",
10509 expected_with_insert_mode: "before editorˇtor after".into(),
10510 expected_with_replace_mode: "before editorˇ after".into(),
10511 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10512 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10513 },
10514 Run {
10515 run_description: "Prepend text containing whitespace",
10516 initial_state: "pˇfield: bool".into(),
10517 buffer_marked_text: "<p|field>: bool".into(),
10518 completion_text: "pub ",
10519 expected_with_insert_mode: "pub ˇfield: bool".into(),
10520 expected_with_replace_mode: "pub ˇ: bool".into(),
10521 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10522 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10523 },
10524 Run {
10525 run_description: "Add element to start of list",
10526 initial_state: "[element_ˇelement_2]".into(),
10527 buffer_marked_text: "[<element_|element_2>]".into(),
10528 completion_text: "element_1",
10529 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10530 expected_with_replace_mode: "[element_1ˇ]".into(),
10531 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10532 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10533 },
10534 Run {
10535 run_description: "Add element to start of list -- first and second elements are equal",
10536 initial_state: "[elˇelement]".into(),
10537 buffer_marked_text: "[<el|element>]".into(),
10538 completion_text: "element",
10539 expected_with_insert_mode: "[elementˇelement]".into(),
10540 expected_with_replace_mode: "[elementˇ]".into(),
10541 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10542 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10543 },
10544 Run {
10545 run_description: "Ends with matching suffix",
10546 initial_state: "SubˇError".into(),
10547 buffer_marked_text: "<Sub|Error>".into(),
10548 completion_text: "SubscriptionError",
10549 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10550 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10551 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10552 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10553 },
10554 Run {
10555 run_description: "Suffix is a subsequence -- contiguous",
10556 initial_state: "SubˇErr".into(),
10557 buffer_marked_text: "<Sub|Err>".into(),
10558 completion_text: "SubscriptionError",
10559 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10560 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10561 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10562 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10563 },
10564 Run {
10565 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10566 initial_state: "Suˇscrirr".into(),
10567 buffer_marked_text: "<Su|scrirr>".into(),
10568 completion_text: "SubscriptionError",
10569 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10570 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10571 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10572 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10573 },
10574 Run {
10575 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10576 initial_state: "foo(indˇix)".into(),
10577 buffer_marked_text: "foo(<ind|ix>)".into(),
10578 completion_text: "node_index",
10579 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10580 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10581 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10582 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10583 },
10584 ];
10585
10586 for run in runs {
10587 let run_variations = [
10588 (LspInsertMode::Insert, run.expected_with_insert_mode),
10589 (LspInsertMode::Replace, run.expected_with_replace_mode),
10590 (
10591 LspInsertMode::ReplaceSubsequence,
10592 run.expected_with_replace_subsequence_mode,
10593 ),
10594 (
10595 LspInsertMode::ReplaceSuffix,
10596 run.expected_with_replace_suffix_mode,
10597 ),
10598 ];
10599
10600 for (lsp_insert_mode, expected_text) in run_variations {
10601 eprintln!(
10602 "run = {:?}, mode = {lsp_insert_mode:.?}",
10603 run.run_description,
10604 );
10605
10606 update_test_language_settings(&mut cx, |settings| {
10607 settings.defaults.completions = Some(CompletionSettings {
10608 lsp_insert_mode,
10609 words: WordsCompletionMode::Disabled,
10610 lsp: true,
10611 lsp_fetch_timeout_ms: 0,
10612 });
10613 });
10614
10615 cx.set_state(&run.initial_state);
10616 cx.update_editor(|editor, window, cx| {
10617 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10618 });
10619
10620 let counter = Arc::new(AtomicUsize::new(0));
10621 handle_completion_request_with_insert_and_replace(
10622 &mut cx,
10623 &run.buffer_marked_text,
10624 vec![run.completion_text],
10625 counter.clone(),
10626 )
10627 .await;
10628 cx.condition(|editor, _| editor.context_menu_visible())
10629 .await;
10630 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10631
10632 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10633 editor
10634 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10635 .unwrap()
10636 });
10637 cx.assert_editor_state(&expected_text);
10638 handle_resolve_completion_request(&mut cx, None).await;
10639 apply_additional_edits.await.unwrap();
10640 }
10641 }
10642}
10643
10644#[gpui::test]
10645async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10646 init_test(cx, |_| {});
10647 let mut cx = EditorLspTestContext::new_rust(
10648 lsp::ServerCapabilities {
10649 completion_provider: Some(lsp::CompletionOptions {
10650 resolve_provider: Some(true),
10651 ..Default::default()
10652 }),
10653 ..Default::default()
10654 },
10655 cx,
10656 )
10657 .await;
10658
10659 let initial_state = "SubˇError";
10660 let buffer_marked_text = "<Sub|Error>";
10661 let completion_text = "SubscriptionError";
10662 let expected_with_insert_mode = "SubscriptionErrorˇError";
10663 let expected_with_replace_mode = "SubscriptionErrorˇ";
10664
10665 update_test_language_settings(&mut cx, |settings| {
10666 settings.defaults.completions = Some(CompletionSettings {
10667 words: WordsCompletionMode::Disabled,
10668 // set the opposite here to ensure that the action is overriding the default behavior
10669 lsp_insert_mode: LspInsertMode::Insert,
10670 lsp: true,
10671 lsp_fetch_timeout_ms: 0,
10672 });
10673 });
10674
10675 cx.set_state(initial_state);
10676 cx.update_editor(|editor, window, cx| {
10677 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10678 });
10679
10680 let counter = Arc::new(AtomicUsize::new(0));
10681 handle_completion_request_with_insert_and_replace(
10682 &mut cx,
10683 &buffer_marked_text,
10684 vec![completion_text],
10685 counter.clone(),
10686 )
10687 .await;
10688 cx.condition(|editor, _| editor.context_menu_visible())
10689 .await;
10690 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10691
10692 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10693 editor
10694 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10695 .unwrap()
10696 });
10697 cx.assert_editor_state(&expected_with_replace_mode);
10698 handle_resolve_completion_request(&mut cx, None).await;
10699 apply_additional_edits.await.unwrap();
10700
10701 update_test_language_settings(&mut cx, |settings| {
10702 settings.defaults.completions = Some(CompletionSettings {
10703 words: WordsCompletionMode::Disabled,
10704 // set the opposite here to ensure that the action is overriding the default behavior
10705 lsp_insert_mode: LspInsertMode::Replace,
10706 lsp: true,
10707 lsp_fetch_timeout_ms: 0,
10708 });
10709 });
10710
10711 cx.set_state(initial_state);
10712 cx.update_editor(|editor, window, cx| {
10713 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10714 });
10715 handle_completion_request_with_insert_and_replace(
10716 &mut cx,
10717 &buffer_marked_text,
10718 vec![completion_text],
10719 counter.clone(),
10720 )
10721 .await;
10722 cx.condition(|editor, _| editor.context_menu_visible())
10723 .await;
10724 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10725
10726 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10727 editor
10728 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10729 .unwrap()
10730 });
10731 cx.assert_editor_state(&expected_with_insert_mode);
10732 handle_resolve_completion_request(&mut cx, None).await;
10733 apply_additional_edits.await.unwrap();
10734}
10735
10736#[gpui::test]
10737async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10738 init_test(cx, |_| {});
10739 let mut cx = EditorLspTestContext::new_rust(
10740 lsp::ServerCapabilities {
10741 completion_provider: Some(lsp::CompletionOptions {
10742 resolve_provider: Some(true),
10743 ..Default::default()
10744 }),
10745 ..Default::default()
10746 },
10747 cx,
10748 )
10749 .await;
10750
10751 // scenario: surrounding text matches completion text
10752 let completion_text = "to_offset";
10753 let initial_state = indoc! {"
10754 1. buf.to_offˇsuffix
10755 2. buf.to_offˇsuf
10756 3. buf.to_offˇfix
10757 4. buf.to_offˇ
10758 5. into_offˇensive
10759 6. ˇsuffix
10760 7. let ˇ //
10761 8. aaˇzz
10762 9. buf.to_off«zzzzzˇ»suffix
10763 10. buf.«ˇzzzzz»suffix
10764 11. to_off«ˇzzzzz»
10765
10766 buf.to_offˇsuffix // newest cursor
10767 "};
10768 let completion_marked_buffer = indoc! {"
10769 1. buf.to_offsuffix
10770 2. buf.to_offsuf
10771 3. buf.to_offfix
10772 4. buf.to_off
10773 5. into_offensive
10774 6. suffix
10775 7. let //
10776 8. aazz
10777 9. buf.to_offzzzzzsuffix
10778 10. buf.zzzzzsuffix
10779 11. to_offzzzzz
10780
10781 buf.<to_off|suffix> // newest cursor
10782 "};
10783 let expected = indoc! {"
10784 1. buf.to_offsetˇ
10785 2. buf.to_offsetˇsuf
10786 3. buf.to_offsetˇfix
10787 4. buf.to_offsetˇ
10788 5. into_offsetˇensive
10789 6. to_offsetˇsuffix
10790 7. let to_offsetˇ //
10791 8. aato_offsetˇzz
10792 9. buf.to_offsetˇ
10793 10. buf.to_offsetˇsuffix
10794 11. to_offsetˇ
10795
10796 buf.to_offsetˇ // newest cursor
10797 "};
10798 cx.set_state(initial_state);
10799 cx.update_editor(|editor, window, cx| {
10800 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10801 });
10802 handle_completion_request_with_insert_and_replace(
10803 &mut cx,
10804 completion_marked_buffer,
10805 vec![completion_text],
10806 Arc::new(AtomicUsize::new(0)),
10807 )
10808 .await;
10809 cx.condition(|editor, _| editor.context_menu_visible())
10810 .await;
10811 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10812 editor
10813 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10814 .unwrap()
10815 });
10816 cx.assert_editor_state(expected);
10817 handle_resolve_completion_request(&mut cx, None).await;
10818 apply_additional_edits.await.unwrap();
10819
10820 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10821 let completion_text = "foo_and_bar";
10822 let initial_state = indoc! {"
10823 1. ooanbˇ
10824 2. zooanbˇ
10825 3. ooanbˇz
10826 4. zooanbˇz
10827 5. ooanˇ
10828 6. oanbˇ
10829
10830 ooanbˇ
10831 "};
10832 let completion_marked_buffer = indoc! {"
10833 1. ooanb
10834 2. zooanb
10835 3. ooanbz
10836 4. zooanbz
10837 5. ooan
10838 6. oanb
10839
10840 <ooanb|>
10841 "};
10842 let expected = indoc! {"
10843 1. foo_and_barˇ
10844 2. zfoo_and_barˇ
10845 3. foo_and_barˇz
10846 4. zfoo_and_barˇz
10847 5. ooanfoo_and_barˇ
10848 6. oanbfoo_and_barˇ
10849
10850 foo_and_barˇ
10851 "};
10852 cx.set_state(initial_state);
10853 cx.update_editor(|editor, window, cx| {
10854 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10855 });
10856 handle_completion_request_with_insert_and_replace(
10857 &mut cx,
10858 completion_marked_buffer,
10859 vec![completion_text],
10860 Arc::new(AtomicUsize::new(0)),
10861 )
10862 .await;
10863 cx.condition(|editor, _| editor.context_menu_visible())
10864 .await;
10865 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10866 editor
10867 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10868 .unwrap()
10869 });
10870 cx.assert_editor_state(expected);
10871 handle_resolve_completion_request(&mut cx, None).await;
10872 apply_additional_edits.await.unwrap();
10873
10874 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10875 // (expects the same as if it was inserted at the end)
10876 let completion_text = "foo_and_bar";
10877 let initial_state = indoc! {"
10878 1. ooˇanb
10879 2. zooˇanb
10880 3. ooˇanbz
10881 4. zooˇanbz
10882
10883 ooˇanb
10884 "};
10885 let completion_marked_buffer = indoc! {"
10886 1. ooanb
10887 2. zooanb
10888 3. ooanbz
10889 4. zooanbz
10890
10891 <oo|anb>
10892 "};
10893 let expected = indoc! {"
10894 1. foo_and_barˇ
10895 2. zfoo_and_barˇ
10896 3. foo_and_barˇz
10897 4. zfoo_and_barˇz
10898
10899 foo_and_barˇ
10900 "};
10901 cx.set_state(initial_state);
10902 cx.update_editor(|editor, window, cx| {
10903 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10904 });
10905 handle_completion_request_with_insert_and_replace(
10906 &mut cx,
10907 completion_marked_buffer,
10908 vec![completion_text],
10909 Arc::new(AtomicUsize::new(0)),
10910 )
10911 .await;
10912 cx.condition(|editor, _| editor.context_menu_visible())
10913 .await;
10914 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10915 editor
10916 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10917 .unwrap()
10918 });
10919 cx.assert_editor_state(expected);
10920 handle_resolve_completion_request(&mut cx, None).await;
10921 apply_additional_edits.await.unwrap();
10922}
10923
10924// This used to crash
10925#[gpui::test]
10926async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10927 init_test(cx, |_| {});
10928
10929 let buffer_text = indoc! {"
10930 fn main() {
10931 10.satu;
10932
10933 //
10934 // separate cursors so they open in different excerpts (manually reproducible)
10935 //
10936
10937 10.satu20;
10938 }
10939 "};
10940 let multibuffer_text_with_selections = indoc! {"
10941 fn main() {
10942 10.satuˇ;
10943
10944 //
10945
10946 //
10947
10948 10.satuˇ20;
10949 }
10950 "};
10951 let expected_multibuffer = indoc! {"
10952 fn main() {
10953 10.saturating_sub()ˇ;
10954
10955 //
10956
10957 //
10958
10959 10.saturating_sub()ˇ;
10960 }
10961 "};
10962
10963 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10964 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10965
10966 let fs = FakeFs::new(cx.executor());
10967 fs.insert_tree(
10968 path!("/a"),
10969 json!({
10970 "main.rs": buffer_text,
10971 }),
10972 )
10973 .await;
10974
10975 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10976 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10977 language_registry.add(rust_lang());
10978 let mut fake_servers = language_registry.register_fake_lsp(
10979 "Rust",
10980 FakeLspAdapter {
10981 capabilities: lsp::ServerCapabilities {
10982 completion_provider: Some(lsp::CompletionOptions {
10983 resolve_provider: None,
10984 ..lsp::CompletionOptions::default()
10985 }),
10986 ..lsp::ServerCapabilities::default()
10987 },
10988 ..FakeLspAdapter::default()
10989 },
10990 );
10991 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10992 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10993 let buffer = project
10994 .update(cx, |project, cx| {
10995 project.open_local_buffer(path!("/a/main.rs"), cx)
10996 })
10997 .await
10998 .unwrap();
10999
11000 let multi_buffer = cx.new(|cx| {
11001 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11002 multi_buffer.push_excerpts(
11003 buffer.clone(),
11004 [ExcerptRange::new(0..first_excerpt_end)],
11005 cx,
11006 );
11007 multi_buffer.push_excerpts(
11008 buffer.clone(),
11009 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11010 cx,
11011 );
11012 multi_buffer
11013 });
11014
11015 let editor = workspace
11016 .update(cx, |_, window, cx| {
11017 cx.new(|cx| {
11018 Editor::new(
11019 EditorMode::Full {
11020 scale_ui_elements_with_buffer_font_size: false,
11021 show_active_line_background: false,
11022 sized_by_content: false,
11023 },
11024 multi_buffer.clone(),
11025 Some(project.clone()),
11026 window,
11027 cx,
11028 )
11029 })
11030 })
11031 .unwrap();
11032
11033 let pane = workspace
11034 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11035 .unwrap();
11036 pane.update_in(cx, |pane, window, cx| {
11037 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11038 });
11039
11040 let fake_server = fake_servers.next().await.unwrap();
11041
11042 editor.update_in(cx, |editor, window, cx| {
11043 editor.change_selections(None, window, cx, |s| {
11044 s.select_ranges([
11045 Point::new(1, 11)..Point::new(1, 11),
11046 Point::new(7, 11)..Point::new(7, 11),
11047 ])
11048 });
11049
11050 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11051 });
11052
11053 editor.update_in(cx, |editor, window, cx| {
11054 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11055 });
11056
11057 fake_server
11058 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11059 let completion_item = lsp::CompletionItem {
11060 label: "saturating_sub()".into(),
11061 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11062 lsp::InsertReplaceEdit {
11063 new_text: "saturating_sub()".to_owned(),
11064 insert: lsp::Range::new(
11065 lsp::Position::new(7, 7),
11066 lsp::Position::new(7, 11),
11067 ),
11068 replace: lsp::Range::new(
11069 lsp::Position::new(7, 7),
11070 lsp::Position::new(7, 13),
11071 ),
11072 },
11073 )),
11074 ..lsp::CompletionItem::default()
11075 };
11076
11077 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11078 })
11079 .next()
11080 .await
11081 .unwrap();
11082
11083 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11084 .await;
11085
11086 editor
11087 .update_in(cx, |editor, window, cx| {
11088 editor
11089 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11090 .unwrap()
11091 })
11092 .await
11093 .unwrap();
11094
11095 editor.update(cx, |editor, cx| {
11096 assert_text_with_selections(editor, expected_multibuffer, cx);
11097 })
11098}
11099
11100#[gpui::test]
11101async fn test_completion(cx: &mut TestAppContext) {
11102 init_test(cx, |_| {});
11103
11104 let mut cx = EditorLspTestContext::new_rust(
11105 lsp::ServerCapabilities {
11106 completion_provider: Some(lsp::CompletionOptions {
11107 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11108 resolve_provider: Some(true),
11109 ..Default::default()
11110 }),
11111 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11112 ..Default::default()
11113 },
11114 cx,
11115 )
11116 .await;
11117 let counter = Arc::new(AtomicUsize::new(0));
11118
11119 cx.set_state(indoc! {"
11120 oneˇ
11121 two
11122 three
11123 "});
11124 cx.simulate_keystroke(".");
11125 handle_completion_request(
11126 &mut cx,
11127 indoc! {"
11128 one.|<>
11129 two
11130 three
11131 "},
11132 vec!["first_completion", "second_completion"],
11133 counter.clone(),
11134 )
11135 .await;
11136 cx.condition(|editor, _| editor.context_menu_visible())
11137 .await;
11138 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11139
11140 let _handler = handle_signature_help_request(
11141 &mut cx,
11142 lsp::SignatureHelp {
11143 signatures: vec![lsp::SignatureInformation {
11144 label: "test signature".to_string(),
11145 documentation: None,
11146 parameters: Some(vec![lsp::ParameterInformation {
11147 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11148 documentation: None,
11149 }]),
11150 active_parameter: None,
11151 }],
11152 active_signature: None,
11153 active_parameter: None,
11154 },
11155 );
11156 cx.update_editor(|editor, window, cx| {
11157 assert!(
11158 !editor.signature_help_state.is_shown(),
11159 "No signature help was called for"
11160 );
11161 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11162 });
11163 cx.run_until_parked();
11164 cx.update_editor(|editor, _, _| {
11165 assert!(
11166 !editor.signature_help_state.is_shown(),
11167 "No signature help should be shown when completions menu is open"
11168 );
11169 });
11170
11171 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11172 editor.context_menu_next(&Default::default(), window, cx);
11173 editor
11174 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11175 .unwrap()
11176 });
11177 cx.assert_editor_state(indoc! {"
11178 one.second_completionˇ
11179 two
11180 three
11181 "});
11182
11183 handle_resolve_completion_request(
11184 &mut cx,
11185 Some(vec![
11186 (
11187 //This overlaps with the primary completion edit which is
11188 //misbehavior from the LSP spec, test that we filter it out
11189 indoc! {"
11190 one.second_ˇcompletion
11191 two
11192 threeˇ
11193 "},
11194 "overlapping additional edit",
11195 ),
11196 (
11197 indoc! {"
11198 one.second_completion
11199 two
11200 threeˇ
11201 "},
11202 "\nadditional edit",
11203 ),
11204 ]),
11205 )
11206 .await;
11207 apply_additional_edits.await.unwrap();
11208 cx.assert_editor_state(indoc! {"
11209 one.second_completionˇ
11210 two
11211 three
11212 additional edit
11213 "});
11214
11215 cx.set_state(indoc! {"
11216 one.second_completion
11217 twoˇ
11218 threeˇ
11219 additional edit
11220 "});
11221 cx.simulate_keystroke(" ");
11222 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11223 cx.simulate_keystroke("s");
11224 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11225
11226 cx.assert_editor_state(indoc! {"
11227 one.second_completion
11228 two sˇ
11229 three sˇ
11230 additional edit
11231 "});
11232 handle_completion_request(
11233 &mut cx,
11234 indoc! {"
11235 one.second_completion
11236 two s
11237 three <s|>
11238 additional edit
11239 "},
11240 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11241 counter.clone(),
11242 )
11243 .await;
11244 cx.condition(|editor, _| editor.context_menu_visible())
11245 .await;
11246 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11247
11248 cx.simulate_keystroke("i");
11249
11250 handle_completion_request(
11251 &mut cx,
11252 indoc! {"
11253 one.second_completion
11254 two si
11255 three <si|>
11256 additional edit
11257 "},
11258 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11259 counter.clone(),
11260 )
11261 .await;
11262 cx.condition(|editor, _| editor.context_menu_visible())
11263 .await;
11264 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11265
11266 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11267 editor
11268 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11269 .unwrap()
11270 });
11271 cx.assert_editor_state(indoc! {"
11272 one.second_completion
11273 two sixth_completionˇ
11274 three sixth_completionˇ
11275 additional edit
11276 "});
11277
11278 apply_additional_edits.await.unwrap();
11279
11280 update_test_language_settings(&mut cx, |settings| {
11281 settings.defaults.show_completions_on_input = Some(false);
11282 });
11283 cx.set_state("editorˇ");
11284 cx.simulate_keystroke(".");
11285 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11286 cx.simulate_keystrokes("c l o");
11287 cx.assert_editor_state("editor.cloˇ");
11288 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11289 cx.update_editor(|editor, window, cx| {
11290 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11291 });
11292 handle_completion_request(
11293 &mut cx,
11294 "editor.<clo|>",
11295 vec!["close", "clobber"],
11296 counter.clone(),
11297 )
11298 .await;
11299 cx.condition(|editor, _| editor.context_menu_visible())
11300 .await;
11301 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11302
11303 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11304 editor
11305 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11306 .unwrap()
11307 });
11308 cx.assert_editor_state("editor.closeˇ");
11309 handle_resolve_completion_request(&mut cx, None).await;
11310 apply_additional_edits.await.unwrap();
11311}
11312
11313#[gpui::test]
11314async fn test_word_completion(cx: &mut TestAppContext) {
11315 let lsp_fetch_timeout_ms = 10;
11316 init_test(cx, |language_settings| {
11317 language_settings.defaults.completions = Some(CompletionSettings {
11318 words: WordsCompletionMode::Fallback,
11319 lsp: true,
11320 lsp_fetch_timeout_ms: 10,
11321 lsp_insert_mode: LspInsertMode::Insert,
11322 });
11323 });
11324
11325 let mut cx = EditorLspTestContext::new_rust(
11326 lsp::ServerCapabilities {
11327 completion_provider: Some(lsp::CompletionOptions {
11328 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11329 ..lsp::CompletionOptions::default()
11330 }),
11331 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11332 ..lsp::ServerCapabilities::default()
11333 },
11334 cx,
11335 )
11336 .await;
11337
11338 let throttle_completions = Arc::new(AtomicBool::new(false));
11339
11340 let lsp_throttle_completions = throttle_completions.clone();
11341 let _completion_requests_handler =
11342 cx.lsp
11343 .server
11344 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11345 let lsp_throttle_completions = lsp_throttle_completions.clone();
11346 let cx = cx.clone();
11347 async move {
11348 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11349 cx.background_executor()
11350 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11351 .await;
11352 }
11353 Ok(Some(lsp::CompletionResponse::Array(vec![
11354 lsp::CompletionItem {
11355 label: "first".into(),
11356 ..lsp::CompletionItem::default()
11357 },
11358 lsp::CompletionItem {
11359 label: "last".into(),
11360 ..lsp::CompletionItem::default()
11361 },
11362 ])))
11363 }
11364 });
11365
11366 cx.set_state(indoc! {"
11367 oneˇ
11368 two
11369 three
11370 "});
11371 cx.simulate_keystroke(".");
11372 cx.executor().run_until_parked();
11373 cx.condition(|editor, _| editor.context_menu_visible())
11374 .await;
11375 cx.update_editor(|editor, window, cx| {
11376 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11377 {
11378 assert_eq!(
11379 completion_menu_entries(&menu),
11380 &["first", "last"],
11381 "When LSP server is fast to reply, no fallback word completions are used"
11382 );
11383 } else {
11384 panic!("expected completion menu to be open");
11385 }
11386 editor.cancel(&Cancel, window, cx);
11387 });
11388 cx.executor().run_until_parked();
11389 cx.condition(|editor, _| !editor.context_menu_visible())
11390 .await;
11391
11392 throttle_completions.store(true, atomic::Ordering::Release);
11393 cx.simulate_keystroke(".");
11394 cx.executor()
11395 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11396 cx.executor().run_until_parked();
11397 cx.condition(|editor, _| editor.context_menu_visible())
11398 .await;
11399 cx.update_editor(|editor, _, _| {
11400 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11401 {
11402 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11403 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11404 } else {
11405 panic!("expected completion menu to be open");
11406 }
11407 });
11408}
11409
11410#[gpui::test]
11411async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11412 init_test(cx, |language_settings| {
11413 language_settings.defaults.completions = Some(CompletionSettings {
11414 words: WordsCompletionMode::Enabled,
11415 lsp: true,
11416 lsp_fetch_timeout_ms: 0,
11417 lsp_insert_mode: LspInsertMode::Insert,
11418 });
11419 });
11420
11421 let mut cx = EditorLspTestContext::new_rust(
11422 lsp::ServerCapabilities {
11423 completion_provider: Some(lsp::CompletionOptions {
11424 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11425 ..lsp::CompletionOptions::default()
11426 }),
11427 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11428 ..lsp::ServerCapabilities::default()
11429 },
11430 cx,
11431 )
11432 .await;
11433
11434 let _completion_requests_handler =
11435 cx.lsp
11436 .server
11437 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11438 Ok(Some(lsp::CompletionResponse::Array(vec![
11439 lsp::CompletionItem {
11440 label: "first".into(),
11441 ..lsp::CompletionItem::default()
11442 },
11443 lsp::CompletionItem {
11444 label: "last".into(),
11445 ..lsp::CompletionItem::default()
11446 },
11447 ])))
11448 });
11449
11450 cx.set_state(indoc! {"ˇ
11451 first
11452 last
11453 second
11454 "});
11455 cx.simulate_keystroke(".");
11456 cx.executor().run_until_parked();
11457 cx.condition(|editor, _| editor.context_menu_visible())
11458 .await;
11459 cx.update_editor(|editor, _, _| {
11460 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11461 {
11462 assert_eq!(
11463 completion_menu_entries(&menu),
11464 &["first", "last", "second"],
11465 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11466 );
11467 } else {
11468 panic!("expected completion menu to be open");
11469 }
11470 });
11471}
11472
11473#[gpui::test]
11474async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11475 init_test(cx, |language_settings| {
11476 language_settings.defaults.completions = Some(CompletionSettings {
11477 words: WordsCompletionMode::Disabled,
11478 lsp: true,
11479 lsp_fetch_timeout_ms: 0,
11480 lsp_insert_mode: LspInsertMode::Insert,
11481 });
11482 });
11483
11484 let mut cx = EditorLspTestContext::new_rust(
11485 lsp::ServerCapabilities {
11486 completion_provider: Some(lsp::CompletionOptions {
11487 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11488 ..lsp::CompletionOptions::default()
11489 }),
11490 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11491 ..lsp::ServerCapabilities::default()
11492 },
11493 cx,
11494 )
11495 .await;
11496
11497 let _completion_requests_handler =
11498 cx.lsp
11499 .server
11500 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11501 panic!("LSP completions should not be queried when dealing with word completions")
11502 });
11503
11504 cx.set_state(indoc! {"ˇ
11505 first
11506 last
11507 second
11508 "});
11509 cx.update_editor(|editor, window, cx| {
11510 editor.show_word_completions(&ShowWordCompletions, window, cx);
11511 });
11512 cx.executor().run_until_parked();
11513 cx.condition(|editor, _| editor.context_menu_visible())
11514 .await;
11515 cx.update_editor(|editor, _, _| {
11516 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11517 {
11518 assert_eq!(
11519 completion_menu_entries(&menu),
11520 &["first", "last", "second"],
11521 "`ShowWordCompletions` action should show word completions"
11522 );
11523 } else {
11524 panic!("expected completion menu to be open");
11525 }
11526 });
11527
11528 cx.simulate_keystroke("l");
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 &["last"],
11538 "After showing word completions, further editing should filter them and not query the LSP"
11539 );
11540 } else {
11541 panic!("expected completion menu to be open");
11542 }
11543 });
11544}
11545
11546#[gpui::test]
11547async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11548 init_test(cx, |language_settings| {
11549 language_settings.defaults.completions = Some(CompletionSettings {
11550 words: WordsCompletionMode::Fallback,
11551 lsp: false,
11552 lsp_fetch_timeout_ms: 0,
11553 lsp_insert_mode: LspInsertMode::Insert,
11554 });
11555 });
11556
11557 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11558
11559 cx.set_state(indoc! {"ˇ
11560 0_usize
11561 let
11562 33
11563 4.5f32
11564 "});
11565 cx.update_editor(|editor, window, cx| {
11566 editor.show_completions(&ShowCompletions::default(), window, cx);
11567 });
11568 cx.executor().run_until_parked();
11569 cx.condition(|editor, _| editor.context_menu_visible())
11570 .await;
11571 cx.update_editor(|editor, window, cx| {
11572 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11573 {
11574 assert_eq!(
11575 completion_menu_entries(&menu),
11576 &["let"],
11577 "With no digits in the completion query, no digits should be in the word completions"
11578 );
11579 } else {
11580 panic!("expected completion menu to be open");
11581 }
11582 editor.cancel(&Cancel, window, cx);
11583 });
11584
11585 cx.set_state(indoc! {"3ˇ
11586 0_usize
11587 let
11588 3
11589 33.35f32
11590 "});
11591 cx.update_editor(|editor, window, cx| {
11592 editor.show_completions(&ShowCompletions::default(), window, cx);
11593 });
11594 cx.executor().run_until_parked();
11595 cx.condition(|editor, _| editor.context_menu_visible())
11596 .await;
11597 cx.update_editor(|editor, _, _| {
11598 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11599 {
11600 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11601 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11602 } else {
11603 panic!("expected completion menu to be open");
11604 }
11605 });
11606}
11607
11608fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11609 let position = || lsp::Position {
11610 line: params.text_document_position.position.line,
11611 character: params.text_document_position.position.character,
11612 };
11613 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11614 range: lsp::Range {
11615 start: position(),
11616 end: position(),
11617 },
11618 new_text: text.to_string(),
11619 }))
11620}
11621
11622#[gpui::test]
11623async fn test_multiline_completion(cx: &mut TestAppContext) {
11624 init_test(cx, |_| {});
11625
11626 let fs = FakeFs::new(cx.executor());
11627 fs.insert_tree(
11628 path!("/a"),
11629 json!({
11630 "main.ts": "a",
11631 }),
11632 )
11633 .await;
11634
11635 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11636 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11637 let typescript_language = Arc::new(Language::new(
11638 LanguageConfig {
11639 name: "TypeScript".into(),
11640 matcher: LanguageMatcher {
11641 path_suffixes: vec!["ts".to_string()],
11642 ..LanguageMatcher::default()
11643 },
11644 line_comments: vec!["// ".into()],
11645 ..LanguageConfig::default()
11646 },
11647 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11648 ));
11649 language_registry.add(typescript_language.clone());
11650 let mut fake_servers = language_registry.register_fake_lsp(
11651 "TypeScript",
11652 FakeLspAdapter {
11653 capabilities: lsp::ServerCapabilities {
11654 completion_provider: Some(lsp::CompletionOptions {
11655 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11656 ..lsp::CompletionOptions::default()
11657 }),
11658 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11659 ..lsp::ServerCapabilities::default()
11660 },
11661 // Emulate vtsls label generation
11662 label_for_completion: Some(Box::new(|item, _| {
11663 let text = if let Some(description) = item
11664 .label_details
11665 .as_ref()
11666 .and_then(|label_details| label_details.description.as_ref())
11667 {
11668 format!("{} {}", item.label, description)
11669 } else if let Some(detail) = &item.detail {
11670 format!("{} {}", item.label, detail)
11671 } else {
11672 item.label.clone()
11673 };
11674 let len = text.len();
11675 Some(language::CodeLabel {
11676 text,
11677 runs: Vec::new(),
11678 filter_range: 0..len,
11679 })
11680 })),
11681 ..FakeLspAdapter::default()
11682 },
11683 );
11684 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11685 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11686 let worktree_id = workspace
11687 .update(cx, |workspace, _window, cx| {
11688 workspace.project().update(cx, |project, cx| {
11689 project.worktrees(cx).next().unwrap().read(cx).id()
11690 })
11691 })
11692 .unwrap();
11693 let _buffer = project
11694 .update(cx, |project, cx| {
11695 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11696 })
11697 .await
11698 .unwrap();
11699 let editor = workspace
11700 .update(cx, |workspace, window, cx| {
11701 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11702 })
11703 .unwrap()
11704 .await
11705 .unwrap()
11706 .downcast::<Editor>()
11707 .unwrap();
11708 let fake_server = fake_servers.next().await.unwrap();
11709
11710 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11711 let multiline_label_2 = "a\nb\nc\n";
11712 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11713 let multiline_description = "d\ne\nf\n";
11714 let multiline_detail_2 = "g\nh\ni\n";
11715
11716 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11717 move |params, _| async move {
11718 Ok(Some(lsp::CompletionResponse::Array(vec![
11719 lsp::CompletionItem {
11720 label: multiline_label.to_string(),
11721 text_edit: gen_text_edit(¶ms, "new_text_1"),
11722 ..lsp::CompletionItem::default()
11723 },
11724 lsp::CompletionItem {
11725 label: "single line label 1".to_string(),
11726 detail: Some(multiline_detail.to_string()),
11727 text_edit: gen_text_edit(¶ms, "new_text_2"),
11728 ..lsp::CompletionItem::default()
11729 },
11730 lsp::CompletionItem {
11731 label: "single line label 2".to_string(),
11732 label_details: Some(lsp::CompletionItemLabelDetails {
11733 description: Some(multiline_description.to_string()),
11734 detail: None,
11735 }),
11736 text_edit: gen_text_edit(¶ms, "new_text_2"),
11737 ..lsp::CompletionItem::default()
11738 },
11739 lsp::CompletionItem {
11740 label: multiline_label_2.to_string(),
11741 detail: Some(multiline_detail_2.to_string()),
11742 text_edit: gen_text_edit(¶ms, "new_text_3"),
11743 ..lsp::CompletionItem::default()
11744 },
11745 lsp::CompletionItem {
11746 label: "Label with many spaces and \t but without newlines".to_string(),
11747 detail: Some(
11748 "Details with many spaces and \t but without newlines".to_string(),
11749 ),
11750 text_edit: gen_text_edit(¶ms, "new_text_4"),
11751 ..lsp::CompletionItem::default()
11752 },
11753 ])))
11754 },
11755 );
11756
11757 editor.update_in(cx, |editor, window, cx| {
11758 cx.focus_self(window);
11759 editor.move_to_end(&MoveToEnd, window, cx);
11760 editor.handle_input(".", window, cx);
11761 });
11762 cx.run_until_parked();
11763 completion_handle.next().await.unwrap();
11764
11765 editor.update(cx, |editor, _| {
11766 assert!(editor.context_menu_visible());
11767 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11768 {
11769 let completion_labels = menu
11770 .completions
11771 .borrow()
11772 .iter()
11773 .map(|c| c.label.text.clone())
11774 .collect::<Vec<_>>();
11775 assert_eq!(
11776 completion_labels,
11777 &[
11778 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11779 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11780 "single line label 2 d e f ",
11781 "a b c g h i ",
11782 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11783 ],
11784 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11785 );
11786
11787 for completion in menu
11788 .completions
11789 .borrow()
11790 .iter() {
11791 assert_eq!(
11792 completion.label.filter_range,
11793 0..completion.label.text.len(),
11794 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11795 );
11796 }
11797 } else {
11798 panic!("expected completion menu to be open");
11799 }
11800 });
11801}
11802
11803#[gpui::test]
11804async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11805 init_test(cx, |_| {});
11806 let mut cx = EditorLspTestContext::new_rust(
11807 lsp::ServerCapabilities {
11808 completion_provider: Some(lsp::CompletionOptions {
11809 trigger_characters: Some(vec![".".to_string()]),
11810 ..Default::default()
11811 }),
11812 ..Default::default()
11813 },
11814 cx,
11815 )
11816 .await;
11817 cx.lsp
11818 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11819 Ok(Some(lsp::CompletionResponse::Array(vec![
11820 lsp::CompletionItem {
11821 label: "first".into(),
11822 ..Default::default()
11823 },
11824 lsp::CompletionItem {
11825 label: "last".into(),
11826 ..Default::default()
11827 },
11828 ])))
11829 });
11830 cx.set_state("variableˇ");
11831 cx.simulate_keystroke(".");
11832 cx.executor().run_until_parked();
11833
11834 cx.update_editor(|editor, _, _| {
11835 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11836 {
11837 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11838 } else {
11839 panic!("expected completion menu to be open");
11840 }
11841 });
11842
11843 cx.update_editor(|editor, window, cx| {
11844 editor.move_page_down(&MovePageDown::default(), window, cx);
11845 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11846 {
11847 assert!(
11848 menu.selected_item == 1,
11849 "expected PageDown to select the last item from the context menu"
11850 );
11851 } else {
11852 panic!("expected completion menu to stay open after PageDown");
11853 }
11854 });
11855
11856 cx.update_editor(|editor, window, cx| {
11857 editor.move_page_up(&MovePageUp::default(), window, cx);
11858 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11859 {
11860 assert!(
11861 menu.selected_item == 0,
11862 "expected PageUp to select the first item from the context menu"
11863 );
11864 } else {
11865 panic!("expected completion menu to stay open after PageUp");
11866 }
11867 });
11868}
11869
11870#[gpui::test]
11871async fn test_as_is_completions(cx: &mut TestAppContext) {
11872 init_test(cx, |_| {});
11873 let mut cx = EditorLspTestContext::new_rust(
11874 lsp::ServerCapabilities {
11875 completion_provider: Some(lsp::CompletionOptions {
11876 ..Default::default()
11877 }),
11878 ..Default::default()
11879 },
11880 cx,
11881 )
11882 .await;
11883 cx.lsp
11884 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11885 Ok(Some(lsp::CompletionResponse::Array(vec![
11886 lsp::CompletionItem {
11887 label: "unsafe".into(),
11888 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11889 range: lsp::Range {
11890 start: lsp::Position {
11891 line: 1,
11892 character: 2,
11893 },
11894 end: lsp::Position {
11895 line: 1,
11896 character: 3,
11897 },
11898 },
11899 new_text: "unsafe".to_string(),
11900 })),
11901 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11902 ..Default::default()
11903 },
11904 ])))
11905 });
11906 cx.set_state("fn a() {}\n nˇ");
11907 cx.executor().run_until_parked();
11908 cx.update_editor(|editor, window, cx| {
11909 editor.show_completions(
11910 &ShowCompletions {
11911 trigger: Some("\n".into()),
11912 },
11913 window,
11914 cx,
11915 );
11916 });
11917 cx.executor().run_until_parked();
11918
11919 cx.update_editor(|editor, window, cx| {
11920 editor.confirm_completion(&Default::default(), window, cx)
11921 });
11922 cx.executor().run_until_parked();
11923 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11924}
11925
11926#[gpui::test]
11927async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11928 init_test(cx, |_| {});
11929
11930 let mut cx = EditorLspTestContext::new_rust(
11931 lsp::ServerCapabilities {
11932 completion_provider: Some(lsp::CompletionOptions {
11933 trigger_characters: Some(vec![".".to_string()]),
11934 resolve_provider: Some(true),
11935 ..Default::default()
11936 }),
11937 ..Default::default()
11938 },
11939 cx,
11940 )
11941 .await;
11942
11943 cx.set_state("fn main() { let a = 2ˇ; }");
11944 cx.simulate_keystroke(".");
11945 let completion_item = lsp::CompletionItem {
11946 label: "Some".into(),
11947 kind: Some(lsp::CompletionItemKind::SNIPPET),
11948 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11949 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11950 kind: lsp::MarkupKind::Markdown,
11951 value: "```rust\nSome(2)\n```".to_string(),
11952 })),
11953 deprecated: Some(false),
11954 sort_text: Some("Some".to_string()),
11955 filter_text: Some("Some".to_string()),
11956 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11957 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11958 range: lsp::Range {
11959 start: lsp::Position {
11960 line: 0,
11961 character: 22,
11962 },
11963 end: lsp::Position {
11964 line: 0,
11965 character: 22,
11966 },
11967 },
11968 new_text: "Some(2)".to_string(),
11969 })),
11970 additional_text_edits: Some(vec![lsp::TextEdit {
11971 range: lsp::Range {
11972 start: lsp::Position {
11973 line: 0,
11974 character: 20,
11975 },
11976 end: lsp::Position {
11977 line: 0,
11978 character: 22,
11979 },
11980 },
11981 new_text: "".to_string(),
11982 }]),
11983 ..Default::default()
11984 };
11985
11986 let closure_completion_item = completion_item.clone();
11987 let counter = Arc::new(AtomicUsize::new(0));
11988 let counter_clone = counter.clone();
11989 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11990 let task_completion_item = closure_completion_item.clone();
11991 counter_clone.fetch_add(1, atomic::Ordering::Release);
11992 async move {
11993 Ok(Some(lsp::CompletionResponse::Array(vec![
11994 task_completion_item,
11995 ])))
11996 }
11997 });
11998
11999 cx.condition(|editor, _| editor.context_menu_visible())
12000 .await;
12001 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12002 assert!(request.next().await.is_some());
12003 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12004
12005 cx.simulate_keystrokes("S o m");
12006 cx.condition(|editor, _| editor.context_menu_visible())
12007 .await;
12008 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12009 assert!(request.next().await.is_some());
12010 assert!(request.next().await.is_some());
12011 assert!(request.next().await.is_some());
12012 request.close();
12013 assert!(request.next().await.is_none());
12014 assert_eq!(
12015 counter.load(atomic::Ordering::Acquire),
12016 4,
12017 "With the completions menu open, only one LSP request should happen per input"
12018 );
12019}
12020
12021#[gpui::test]
12022async fn test_toggle_comment(cx: &mut TestAppContext) {
12023 init_test(cx, |_| {});
12024 let mut cx = EditorTestContext::new(cx).await;
12025 let language = Arc::new(Language::new(
12026 LanguageConfig {
12027 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12028 ..Default::default()
12029 },
12030 Some(tree_sitter_rust::LANGUAGE.into()),
12031 ));
12032 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12033
12034 // If multiple selections intersect a line, the line is only toggled once.
12035 cx.set_state(indoc! {"
12036 fn a() {
12037 «//b();
12038 ˇ»// «c();
12039 //ˇ» d();
12040 }
12041 "});
12042
12043 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12044
12045 cx.assert_editor_state(indoc! {"
12046 fn a() {
12047 «b();
12048 c();
12049 ˇ» d();
12050 }
12051 "});
12052
12053 // The comment prefix is inserted at the same column for every line in a
12054 // selection.
12055 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12056
12057 cx.assert_editor_state(indoc! {"
12058 fn a() {
12059 // «b();
12060 // c();
12061 ˇ»// d();
12062 }
12063 "});
12064
12065 // If a selection ends at the beginning of a line, that line is not toggled.
12066 cx.set_selections_state(indoc! {"
12067 fn a() {
12068 // b();
12069 «// c();
12070 ˇ» // d();
12071 }
12072 "});
12073
12074 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12075
12076 cx.assert_editor_state(indoc! {"
12077 fn a() {
12078 // b();
12079 «c();
12080 ˇ» // d();
12081 }
12082 "});
12083
12084 // If a selection span a single line and is empty, the line is toggled.
12085 cx.set_state(indoc! {"
12086 fn a() {
12087 a();
12088 b();
12089 ˇ
12090 }
12091 "});
12092
12093 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12094
12095 cx.assert_editor_state(indoc! {"
12096 fn a() {
12097 a();
12098 b();
12099 //•ˇ
12100 }
12101 "});
12102
12103 // If a selection span multiple lines, empty lines are not toggled.
12104 cx.set_state(indoc! {"
12105 fn a() {
12106 «a();
12107
12108 c();ˇ»
12109 }
12110 "});
12111
12112 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12113
12114 cx.assert_editor_state(indoc! {"
12115 fn a() {
12116 // «a();
12117
12118 // c();ˇ»
12119 }
12120 "});
12121
12122 // If a selection includes multiple comment prefixes, all lines are uncommented.
12123 cx.set_state(indoc! {"
12124 fn a() {
12125 «// a();
12126 /// b();
12127 //! c();ˇ»
12128 }
12129 "});
12130
12131 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12132
12133 cx.assert_editor_state(indoc! {"
12134 fn a() {
12135 «a();
12136 b();
12137 c();ˇ»
12138 }
12139 "});
12140}
12141
12142#[gpui::test]
12143async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12144 init_test(cx, |_| {});
12145 let mut cx = EditorTestContext::new(cx).await;
12146 let language = Arc::new(Language::new(
12147 LanguageConfig {
12148 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12149 ..Default::default()
12150 },
12151 Some(tree_sitter_rust::LANGUAGE.into()),
12152 ));
12153 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12154
12155 let toggle_comments = &ToggleComments {
12156 advance_downwards: false,
12157 ignore_indent: true,
12158 };
12159
12160 // If multiple selections intersect a line, the line is only toggled once.
12161 cx.set_state(indoc! {"
12162 fn a() {
12163 // «b();
12164 // c();
12165 // ˇ» d();
12166 }
12167 "});
12168
12169 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12170
12171 cx.assert_editor_state(indoc! {"
12172 fn a() {
12173 «b();
12174 c();
12175 ˇ» d();
12176 }
12177 "});
12178
12179 // The comment prefix is inserted at the beginning of each line
12180 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12181
12182 cx.assert_editor_state(indoc! {"
12183 fn a() {
12184 // «b();
12185 // c();
12186 // ˇ» d();
12187 }
12188 "});
12189
12190 // If a selection ends at the beginning of a line, that line is not toggled.
12191 cx.set_selections_state(indoc! {"
12192 fn a() {
12193 // b();
12194 // «c();
12195 ˇ»// d();
12196 }
12197 "});
12198
12199 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12200
12201 cx.assert_editor_state(indoc! {"
12202 fn a() {
12203 // b();
12204 «c();
12205 ˇ»// d();
12206 }
12207 "});
12208
12209 // If a selection span a single line and is empty, the line is toggled.
12210 cx.set_state(indoc! {"
12211 fn a() {
12212 a();
12213 b();
12214 ˇ
12215 }
12216 "});
12217
12218 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12219
12220 cx.assert_editor_state(indoc! {"
12221 fn a() {
12222 a();
12223 b();
12224 //ˇ
12225 }
12226 "});
12227
12228 // If a selection span multiple lines, empty lines are not toggled.
12229 cx.set_state(indoc! {"
12230 fn a() {
12231 «a();
12232
12233 c();ˇ»
12234 }
12235 "});
12236
12237 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12238
12239 cx.assert_editor_state(indoc! {"
12240 fn a() {
12241 // «a();
12242
12243 // c();ˇ»
12244 }
12245 "});
12246
12247 // If a selection includes multiple comment prefixes, all lines are uncommented.
12248 cx.set_state(indoc! {"
12249 fn a() {
12250 // «a();
12251 /// b();
12252 //! c();ˇ»
12253 }
12254 "});
12255
12256 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12257
12258 cx.assert_editor_state(indoc! {"
12259 fn a() {
12260 «a();
12261 b();
12262 c();ˇ»
12263 }
12264 "});
12265}
12266
12267#[gpui::test]
12268async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12269 init_test(cx, |_| {});
12270
12271 let language = Arc::new(Language::new(
12272 LanguageConfig {
12273 line_comments: vec!["// ".into()],
12274 ..Default::default()
12275 },
12276 Some(tree_sitter_rust::LANGUAGE.into()),
12277 ));
12278
12279 let mut cx = EditorTestContext::new(cx).await;
12280
12281 cx.language_registry().add(language.clone());
12282 cx.update_buffer(|buffer, cx| {
12283 buffer.set_language(Some(language), cx);
12284 });
12285
12286 let toggle_comments = &ToggleComments {
12287 advance_downwards: true,
12288 ignore_indent: false,
12289 };
12290
12291 // Single cursor on one line -> advance
12292 // Cursor moves horizontally 3 characters as well on non-blank line
12293 cx.set_state(indoc!(
12294 "fn a() {
12295 ˇdog();
12296 cat();
12297 }"
12298 ));
12299 cx.update_editor(|editor, window, cx| {
12300 editor.toggle_comments(toggle_comments, window, cx);
12301 });
12302 cx.assert_editor_state(indoc!(
12303 "fn a() {
12304 // dog();
12305 catˇ();
12306 }"
12307 ));
12308
12309 // Single selection on one line -> don't advance
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 // Multiple cursors on one line -> advance
12327 cx.set_state(indoc!(
12328 "fn a() {
12329 ˇdˇog();
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, with selection -> don't 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 // ˇdˇog«()ˇ»;
12356 cat();
12357 }"
12358 ));
12359
12360 // Single cursor on one line -> advance
12361 // Cursor moves to column 0 on blank line
12362 cx.set_state(indoc!(
12363 "fn a() {
12364 ˇdog();
12365
12366 cat();
12367 }"
12368 ));
12369 cx.update_editor(|editor, window, cx| {
12370 editor.toggle_comments(toggle_comments, window, cx);
12371 });
12372 cx.assert_editor_state(indoc!(
12373 "fn a() {
12374 // dog();
12375 ˇ
12376 cat();
12377 }"
12378 ));
12379
12380 // Single cursor on one line -> advance
12381 // Cursor starts and ends at column 0
12382 cx.set_state(indoc!(
12383 "fn a() {
12384 ˇ dog();
12385 cat();
12386 }"
12387 ));
12388 cx.update_editor(|editor, window, cx| {
12389 editor.toggle_comments(toggle_comments, window, cx);
12390 });
12391 cx.assert_editor_state(indoc!(
12392 "fn a() {
12393 // dog();
12394 ˇ cat();
12395 }"
12396 ));
12397}
12398
12399#[gpui::test]
12400async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12401 init_test(cx, |_| {});
12402
12403 let mut cx = EditorTestContext::new(cx).await;
12404
12405 let html_language = Arc::new(
12406 Language::new(
12407 LanguageConfig {
12408 name: "HTML".into(),
12409 block_comment: Some(("<!-- ".into(), " -->".into())),
12410 ..Default::default()
12411 },
12412 Some(tree_sitter_html::LANGUAGE.into()),
12413 )
12414 .with_injection_query(
12415 r#"
12416 (script_element
12417 (raw_text) @injection.content
12418 (#set! injection.language "javascript"))
12419 "#,
12420 )
12421 .unwrap(),
12422 );
12423
12424 let javascript_language = Arc::new(Language::new(
12425 LanguageConfig {
12426 name: "JavaScript".into(),
12427 line_comments: vec!["// ".into()],
12428 ..Default::default()
12429 },
12430 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12431 ));
12432
12433 cx.language_registry().add(html_language.clone());
12434 cx.language_registry().add(javascript_language.clone());
12435 cx.update_buffer(|buffer, cx| {
12436 buffer.set_language(Some(html_language), cx);
12437 });
12438
12439 // Toggle comments for empty selections
12440 cx.set_state(
12441 &r#"
12442 <p>A</p>ˇ
12443 <p>B</p>ˇ
12444 <p>C</p>ˇ
12445 "#
12446 .unindent(),
12447 );
12448 cx.update_editor(|editor, window, cx| {
12449 editor.toggle_comments(&ToggleComments::default(), window, cx)
12450 });
12451 cx.assert_editor_state(
12452 &r#"
12453 <!-- <p>A</p>ˇ -->
12454 <!-- <p>B</p>ˇ -->
12455 <!-- <p>C</p>ˇ -->
12456 "#
12457 .unindent(),
12458 );
12459 cx.update_editor(|editor, window, cx| {
12460 editor.toggle_comments(&ToggleComments::default(), window, cx)
12461 });
12462 cx.assert_editor_state(
12463 &r#"
12464 <p>A</p>ˇ
12465 <p>B</p>ˇ
12466 <p>C</p>ˇ
12467 "#
12468 .unindent(),
12469 );
12470
12471 // Toggle comments for mixture of empty and non-empty selections, where
12472 // multiple selections occupy a given line.
12473 cx.set_state(
12474 &r#"
12475 <p>A«</p>
12476 <p>ˇ»B</p>ˇ
12477 <p>C«</p>
12478 <p>ˇ»D</p>ˇ
12479 "#
12480 .unindent(),
12481 );
12482
12483 cx.update_editor(|editor, window, cx| {
12484 editor.toggle_comments(&ToggleComments::default(), window, cx)
12485 });
12486 cx.assert_editor_state(
12487 &r#"
12488 <!-- <p>A«</p>
12489 <p>ˇ»B</p>ˇ -->
12490 <!-- <p>C«</p>
12491 <p>ˇ»D</p>ˇ -->
12492 "#
12493 .unindent(),
12494 );
12495 cx.update_editor(|editor, window, cx| {
12496 editor.toggle_comments(&ToggleComments::default(), window, cx)
12497 });
12498 cx.assert_editor_state(
12499 &r#"
12500 <p>A«</p>
12501 <p>ˇ»B</p>ˇ
12502 <p>C«</p>
12503 <p>ˇ»D</p>ˇ
12504 "#
12505 .unindent(),
12506 );
12507
12508 // Toggle comments when different languages are active for different
12509 // selections.
12510 cx.set_state(
12511 &r#"
12512 ˇ<script>
12513 ˇvar x = new Y();
12514 ˇ</script>
12515 "#
12516 .unindent(),
12517 );
12518 cx.executor().run_until_parked();
12519 cx.update_editor(|editor, window, cx| {
12520 editor.toggle_comments(&ToggleComments::default(), window, cx)
12521 });
12522 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12523 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12524 cx.assert_editor_state(
12525 &r#"
12526 <!-- ˇ<script> -->
12527 // ˇvar x = new Y();
12528 <!-- ˇ</script> -->
12529 "#
12530 .unindent(),
12531 );
12532}
12533
12534#[gpui::test]
12535fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12536 init_test(cx, |_| {});
12537
12538 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12539 let multibuffer = cx.new(|cx| {
12540 let mut multibuffer = MultiBuffer::new(ReadWrite);
12541 multibuffer.push_excerpts(
12542 buffer.clone(),
12543 [
12544 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12545 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12546 ],
12547 cx,
12548 );
12549 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12550 multibuffer
12551 });
12552
12553 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12554 editor.update_in(cx, |editor, window, cx| {
12555 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12556 editor.change_selections(None, window, cx, |s| {
12557 s.select_ranges([
12558 Point::new(0, 0)..Point::new(0, 0),
12559 Point::new(1, 0)..Point::new(1, 0),
12560 ])
12561 });
12562
12563 editor.handle_input("X", window, cx);
12564 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12565 assert_eq!(
12566 editor.selections.ranges(cx),
12567 [
12568 Point::new(0, 1)..Point::new(0, 1),
12569 Point::new(1, 1)..Point::new(1, 1),
12570 ]
12571 );
12572
12573 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12574 editor.change_selections(None, window, cx, |s| {
12575 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12576 });
12577 editor.backspace(&Default::default(), window, cx);
12578 assert_eq!(editor.text(cx), "Xa\nbbb");
12579 assert_eq!(
12580 editor.selections.ranges(cx),
12581 [Point::new(1, 0)..Point::new(1, 0)]
12582 );
12583
12584 editor.change_selections(None, window, cx, |s| {
12585 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12586 });
12587 editor.backspace(&Default::default(), window, cx);
12588 assert_eq!(editor.text(cx), "X\nbb");
12589 assert_eq!(
12590 editor.selections.ranges(cx),
12591 [Point::new(0, 1)..Point::new(0, 1)]
12592 );
12593 });
12594}
12595
12596#[gpui::test]
12597fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12598 init_test(cx, |_| {});
12599
12600 let markers = vec![('[', ']').into(), ('(', ')').into()];
12601 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12602 indoc! {"
12603 [aaaa
12604 (bbbb]
12605 cccc)",
12606 },
12607 markers.clone(),
12608 );
12609 let excerpt_ranges = markers.into_iter().map(|marker| {
12610 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12611 ExcerptRange::new(context.clone())
12612 });
12613 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12614 let multibuffer = cx.new(|cx| {
12615 let mut multibuffer = MultiBuffer::new(ReadWrite);
12616 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12617 multibuffer
12618 });
12619
12620 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12621 editor.update_in(cx, |editor, window, cx| {
12622 let (expected_text, selection_ranges) = marked_text_ranges(
12623 indoc! {"
12624 aaaa
12625 bˇbbb
12626 bˇbbˇb
12627 cccc"
12628 },
12629 true,
12630 );
12631 assert_eq!(editor.text(cx), expected_text);
12632 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12633
12634 editor.handle_input("X", window, cx);
12635
12636 let (expected_text, expected_selections) = marked_text_ranges(
12637 indoc! {"
12638 aaaa
12639 bXˇbbXb
12640 bXˇbbXˇb
12641 cccc"
12642 },
12643 false,
12644 );
12645 assert_eq!(editor.text(cx), expected_text);
12646 assert_eq!(editor.selections.ranges(cx), expected_selections);
12647
12648 editor.newline(&Newline, window, cx);
12649 let (expected_text, expected_selections) = marked_text_ranges(
12650 indoc! {"
12651 aaaa
12652 bX
12653 ˇbbX
12654 b
12655 bX
12656 ˇbbX
12657 ˇ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}
12666
12667#[gpui::test]
12668fn test_refresh_selections(cx: &mut TestAppContext) {
12669 init_test(cx, |_| {});
12670
12671 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12672 let mut excerpt1_id = None;
12673 let multibuffer = cx.new(|cx| {
12674 let mut multibuffer = MultiBuffer::new(ReadWrite);
12675 excerpt1_id = multibuffer
12676 .push_excerpts(
12677 buffer.clone(),
12678 [
12679 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12680 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12681 ],
12682 cx,
12683 )
12684 .into_iter()
12685 .next();
12686 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12687 multibuffer
12688 });
12689
12690 let editor = cx.add_window(|window, cx| {
12691 let mut editor = build_editor(multibuffer.clone(), window, cx);
12692 let snapshot = editor.snapshot(window, cx);
12693 editor.change_selections(None, window, cx, |s| {
12694 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12695 });
12696 editor.begin_selection(
12697 Point::new(2, 1).to_display_point(&snapshot),
12698 true,
12699 1,
12700 window,
12701 cx,
12702 );
12703 assert_eq!(
12704 editor.selections.ranges(cx),
12705 [
12706 Point::new(1, 3)..Point::new(1, 3),
12707 Point::new(2, 1)..Point::new(2, 1),
12708 ]
12709 );
12710 editor
12711 });
12712
12713 // Refreshing selections is a no-op when excerpts haven't changed.
12714 _ = editor.update(cx, |editor, window, cx| {
12715 editor.change_selections(None, window, cx, |s| s.refresh());
12716 assert_eq!(
12717 editor.selections.ranges(cx),
12718 [
12719 Point::new(1, 3)..Point::new(1, 3),
12720 Point::new(2, 1)..Point::new(2, 1),
12721 ]
12722 );
12723 });
12724
12725 multibuffer.update(cx, |multibuffer, cx| {
12726 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12727 });
12728 _ = editor.update(cx, |editor, window, cx| {
12729 // Removing an excerpt causes the first selection to become degenerate.
12730 assert_eq!(
12731 editor.selections.ranges(cx),
12732 [
12733 Point::new(0, 0)..Point::new(0, 0),
12734 Point::new(0, 1)..Point::new(0, 1)
12735 ]
12736 );
12737
12738 // Refreshing selections will relocate the first selection to the original buffer
12739 // location.
12740 editor.change_selections(None, window, cx, |s| s.refresh());
12741 assert_eq!(
12742 editor.selections.ranges(cx),
12743 [
12744 Point::new(0, 1)..Point::new(0, 1),
12745 Point::new(0, 3)..Point::new(0, 3)
12746 ]
12747 );
12748 assert!(editor.selections.pending_anchor().is_some());
12749 });
12750}
12751
12752#[gpui::test]
12753fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12754 init_test(cx, |_| {});
12755
12756 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12757 let mut excerpt1_id = None;
12758 let multibuffer = cx.new(|cx| {
12759 let mut multibuffer = MultiBuffer::new(ReadWrite);
12760 excerpt1_id = multibuffer
12761 .push_excerpts(
12762 buffer.clone(),
12763 [
12764 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12765 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12766 ],
12767 cx,
12768 )
12769 .into_iter()
12770 .next();
12771 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12772 multibuffer
12773 });
12774
12775 let editor = cx.add_window(|window, cx| {
12776 let mut editor = build_editor(multibuffer.clone(), window, cx);
12777 let snapshot = editor.snapshot(window, cx);
12778 editor.begin_selection(
12779 Point::new(1, 3).to_display_point(&snapshot),
12780 false,
12781 1,
12782 window,
12783 cx,
12784 );
12785 assert_eq!(
12786 editor.selections.ranges(cx),
12787 [Point::new(1, 3)..Point::new(1, 3)]
12788 );
12789 editor
12790 });
12791
12792 multibuffer.update(cx, |multibuffer, cx| {
12793 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12794 });
12795 _ = editor.update(cx, |editor, window, cx| {
12796 assert_eq!(
12797 editor.selections.ranges(cx),
12798 [Point::new(0, 0)..Point::new(0, 0)]
12799 );
12800
12801 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12802 editor.change_selections(None, window, cx, |s| s.refresh());
12803 assert_eq!(
12804 editor.selections.ranges(cx),
12805 [Point::new(0, 3)..Point::new(0, 3)]
12806 );
12807 assert!(editor.selections.pending_anchor().is_some());
12808 });
12809}
12810
12811#[gpui::test]
12812async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12813 init_test(cx, |_| {});
12814
12815 let language = Arc::new(
12816 Language::new(
12817 LanguageConfig {
12818 brackets: BracketPairConfig {
12819 pairs: vec![
12820 BracketPair {
12821 start: "{".to_string(),
12822 end: "}".to_string(),
12823 close: true,
12824 surround: true,
12825 newline: true,
12826 },
12827 BracketPair {
12828 start: "/* ".to_string(),
12829 end: " */".to_string(),
12830 close: true,
12831 surround: true,
12832 newline: true,
12833 },
12834 ],
12835 ..Default::default()
12836 },
12837 ..Default::default()
12838 },
12839 Some(tree_sitter_rust::LANGUAGE.into()),
12840 )
12841 .with_indents_query("")
12842 .unwrap(),
12843 );
12844
12845 let text = concat!(
12846 "{ }\n", //
12847 " x\n", //
12848 " /* */\n", //
12849 "x\n", //
12850 "{{} }\n", //
12851 );
12852
12853 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12854 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12855 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12856 editor
12857 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12858 .await;
12859
12860 editor.update_in(cx, |editor, window, cx| {
12861 editor.change_selections(None, window, cx, |s| {
12862 s.select_display_ranges([
12863 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12864 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12865 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12866 ])
12867 });
12868 editor.newline(&Newline, window, cx);
12869
12870 assert_eq!(
12871 editor.buffer().read(cx).read(cx).text(),
12872 concat!(
12873 "{ \n", // Suppress rustfmt
12874 "\n", //
12875 "}\n", //
12876 " x\n", //
12877 " /* \n", //
12878 " \n", //
12879 " */\n", //
12880 "x\n", //
12881 "{{} \n", //
12882 "}\n", //
12883 )
12884 );
12885 });
12886}
12887
12888#[gpui::test]
12889fn test_highlighted_ranges(cx: &mut TestAppContext) {
12890 init_test(cx, |_| {});
12891
12892 let editor = cx.add_window(|window, cx| {
12893 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12894 build_editor(buffer.clone(), window, cx)
12895 });
12896
12897 _ = editor.update(cx, |editor, window, cx| {
12898 struct Type1;
12899 struct Type2;
12900
12901 let buffer = editor.buffer.read(cx).snapshot(cx);
12902
12903 let anchor_range =
12904 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12905
12906 editor.highlight_background::<Type1>(
12907 &[
12908 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12909 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12910 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12911 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12912 ],
12913 |_| Hsla::red(),
12914 cx,
12915 );
12916 editor.highlight_background::<Type2>(
12917 &[
12918 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12919 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12920 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12921 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12922 ],
12923 |_| Hsla::green(),
12924 cx,
12925 );
12926
12927 let snapshot = editor.snapshot(window, cx);
12928 let mut highlighted_ranges = editor.background_highlights_in_range(
12929 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12930 &snapshot,
12931 cx.theme().colors(),
12932 );
12933 // Enforce a consistent ordering based on color without relying on the ordering of the
12934 // highlight's `TypeId` which is non-executor.
12935 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12936 assert_eq!(
12937 highlighted_ranges,
12938 &[
12939 (
12940 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12941 Hsla::red(),
12942 ),
12943 (
12944 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12945 Hsla::red(),
12946 ),
12947 (
12948 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12949 Hsla::green(),
12950 ),
12951 (
12952 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12953 Hsla::green(),
12954 ),
12955 ]
12956 );
12957 assert_eq!(
12958 editor.background_highlights_in_range(
12959 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12960 &snapshot,
12961 cx.theme().colors(),
12962 ),
12963 &[(
12964 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12965 Hsla::red(),
12966 )]
12967 );
12968 });
12969}
12970
12971#[gpui::test]
12972async fn test_following(cx: &mut TestAppContext) {
12973 init_test(cx, |_| {});
12974
12975 let fs = FakeFs::new(cx.executor());
12976 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12977
12978 let buffer = project.update(cx, |project, cx| {
12979 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12980 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12981 });
12982 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12983 let follower = cx.update(|cx| {
12984 cx.open_window(
12985 WindowOptions {
12986 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12987 gpui::Point::new(px(0.), px(0.)),
12988 gpui::Point::new(px(10.), px(80.)),
12989 ))),
12990 ..Default::default()
12991 },
12992 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12993 )
12994 .unwrap()
12995 });
12996
12997 let is_still_following = Rc::new(RefCell::new(true));
12998 let follower_edit_event_count = Rc::new(RefCell::new(0));
12999 let pending_update = Rc::new(RefCell::new(None));
13000 let leader_entity = leader.root(cx).unwrap();
13001 let follower_entity = follower.root(cx).unwrap();
13002 _ = follower.update(cx, {
13003 let update = pending_update.clone();
13004 let is_still_following = is_still_following.clone();
13005 let follower_edit_event_count = follower_edit_event_count.clone();
13006 |_, window, cx| {
13007 cx.subscribe_in(
13008 &leader_entity,
13009 window,
13010 move |_, leader, event, window, cx| {
13011 leader.read(cx).add_event_to_update_proto(
13012 event,
13013 &mut update.borrow_mut(),
13014 window,
13015 cx,
13016 );
13017 },
13018 )
13019 .detach();
13020
13021 cx.subscribe_in(
13022 &follower_entity,
13023 window,
13024 move |_, _, event: &EditorEvent, _window, _cx| {
13025 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13026 *is_still_following.borrow_mut() = false;
13027 }
13028
13029 if let EditorEvent::BufferEdited = event {
13030 *follower_edit_event_count.borrow_mut() += 1;
13031 }
13032 },
13033 )
13034 .detach();
13035 }
13036 });
13037
13038 // Update the selections only
13039 _ = leader.update(cx, |leader, window, cx| {
13040 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13041 });
13042 follower
13043 .update(cx, |follower, window, cx| {
13044 follower.apply_update_proto(
13045 &project,
13046 pending_update.borrow_mut().take().unwrap(),
13047 window,
13048 cx,
13049 )
13050 })
13051 .unwrap()
13052 .await
13053 .unwrap();
13054 _ = follower.update(cx, |follower, _, cx| {
13055 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13056 });
13057 assert!(*is_still_following.borrow());
13058 assert_eq!(*follower_edit_event_count.borrow(), 0);
13059
13060 // Update the scroll position only
13061 _ = leader.update(cx, |leader, window, cx| {
13062 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13063 });
13064 follower
13065 .update(cx, |follower, window, cx| {
13066 follower.apply_update_proto(
13067 &project,
13068 pending_update.borrow_mut().take().unwrap(),
13069 window,
13070 cx,
13071 )
13072 })
13073 .unwrap()
13074 .await
13075 .unwrap();
13076 assert_eq!(
13077 follower
13078 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13079 .unwrap(),
13080 gpui::Point::new(1.5, 3.5)
13081 );
13082 assert!(*is_still_following.borrow());
13083 assert_eq!(*follower_edit_event_count.borrow(), 0);
13084
13085 // Update the selections and scroll position. The follower's scroll position is updated
13086 // via autoscroll, not via the leader's exact scroll position.
13087 _ = leader.update(cx, |leader, window, cx| {
13088 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13089 leader.request_autoscroll(Autoscroll::newest(), cx);
13090 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13091 });
13092 follower
13093 .update(cx, |follower, window, cx| {
13094 follower.apply_update_proto(
13095 &project,
13096 pending_update.borrow_mut().take().unwrap(),
13097 window,
13098 cx,
13099 )
13100 })
13101 .unwrap()
13102 .await
13103 .unwrap();
13104 _ = follower.update(cx, |follower, _, cx| {
13105 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13106 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13107 });
13108 assert!(*is_still_following.borrow());
13109
13110 // Creating a pending selection that precedes another selection
13111 _ = leader.update(cx, |leader, window, cx| {
13112 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13113 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13114 });
13115 follower
13116 .update(cx, |follower, window, cx| {
13117 follower.apply_update_proto(
13118 &project,
13119 pending_update.borrow_mut().take().unwrap(),
13120 window,
13121 cx,
13122 )
13123 })
13124 .unwrap()
13125 .await
13126 .unwrap();
13127 _ = follower.update(cx, |follower, _, cx| {
13128 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13129 });
13130 assert!(*is_still_following.borrow());
13131
13132 // Extend the pending selection so that it surrounds another selection
13133 _ = leader.update(cx, |leader, window, cx| {
13134 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13135 });
13136 follower
13137 .update(cx, |follower, window, cx| {
13138 follower.apply_update_proto(
13139 &project,
13140 pending_update.borrow_mut().take().unwrap(),
13141 window,
13142 cx,
13143 )
13144 })
13145 .unwrap()
13146 .await
13147 .unwrap();
13148 _ = follower.update(cx, |follower, _, cx| {
13149 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13150 });
13151
13152 // Scrolling locally breaks the follow
13153 _ = follower.update(cx, |follower, window, cx| {
13154 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13155 follower.set_scroll_anchor(
13156 ScrollAnchor {
13157 anchor: top_anchor,
13158 offset: gpui::Point::new(0.0, 0.5),
13159 },
13160 window,
13161 cx,
13162 );
13163 });
13164 assert!(!(*is_still_following.borrow()));
13165}
13166
13167#[gpui::test]
13168async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13169 init_test(cx, |_| {});
13170
13171 let fs = FakeFs::new(cx.executor());
13172 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13173 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13174 let pane = workspace
13175 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13176 .unwrap();
13177
13178 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13179
13180 let leader = pane.update_in(cx, |_, window, cx| {
13181 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13182 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13183 });
13184
13185 // Start following the editor when it has no excerpts.
13186 let mut state_message =
13187 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13188 let workspace_entity = workspace.root(cx).unwrap();
13189 let follower_1 = cx
13190 .update_window(*workspace.deref(), |_, window, cx| {
13191 Editor::from_state_proto(
13192 workspace_entity,
13193 ViewId {
13194 creator: CollaboratorId::PeerId(PeerId::default()),
13195 id: 0,
13196 },
13197 &mut state_message,
13198 window,
13199 cx,
13200 )
13201 })
13202 .unwrap()
13203 .unwrap()
13204 .await
13205 .unwrap();
13206
13207 let update_message = Rc::new(RefCell::new(None));
13208 follower_1.update_in(cx, {
13209 let update = update_message.clone();
13210 |_, window, cx| {
13211 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13212 leader.read(cx).add_event_to_update_proto(
13213 event,
13214 &mut update.borrow_mut(),
13215 window,
13216 cx,
13217 );
13218 })
13219 .detach();
13220 }
13221 });
13222
13223 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13224 (
13225 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13226 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13227 )
13228 });
13229
13230 // Insert some excerpts.
13231 leader.update(cx, |leader, cx| {
13232 leader.buffer.update(cx, |multibuffer, cx| {
13233 multibuffer.set_excerpts_for_path(
13234 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13235 buffer_1.clone(),
13236 vec![
13237 Point::row_range(0..3),
13238 Point::row_range(1..6),
13239 Point::row_range(12..15),
13240 ],
13241 0,
13242 cx,
13243 );
13244 multibuffer.set_excerpts_for_path(
13245 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13246 buffer_2.clone(),
13247 vec![Point::row_range(0..6), Point::row_range(8..12)],
13248 0,
13249 cx,
13250 );
13251 });
13252 });
13253
13254 // Apply the update of adding the excerpts.
13255 follower_1
13256 .update_in(cx, |follower, window, cx| {
13257 follower.apply_update_proto(
13258 &project,
13259 update_message.borrow().clone().unwrap(),
13260 window,
13261 cx,
13262 )
13263 })
13264 .await
13265 .unwrap();
13266 assert_eq!(
13267 follower_1.update(cx, |editor, cx| editor.text(cx)),
13268 leader.update(cx, |editor, cx| editor.text(cx))
13269 );
13270 update_message.borrow_mut().take();
13271
13272 // Start following separately after it already has excerpts.
13273 let mut state_message =
13274 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13275 let workspace_entity = workspace.root(cx).unwrap();
13276 let follower_2 = cx
13277 .update_window(*workspace.deref(), |_, window, cx| {
13278 Editor::from_state_proto(
13279 workspace_entity,
13280 ViewId {
13281 creator: CollaboratorId::PeerId(PeerId::default()),
13282 id: 0,
13283 },
13284 &mut state_message,
13285 window,
13286 cx,
13287 )
13288 })
13289 .unwrap()
13290 .unwrap()
13291 .await
13292 .unwrap();
13293 assert_eq!(
13294 follower_2.update(cx, |editor, cx| editor.text(cx)),
13295 leader.update(cx, |editor, cx| editor.text(cx))
13296 );
13297
13298 // Remove some excerpts.
13299 leader.update(cx, |leader, cx| {
13300 leader.buffer.update(cx, |multibuffer, cx| {
13301 let excerpt_ids = multibuffer.excerpt_ids();
13302 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13303 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13304 });
13305 });
13306
13307 // Apply the update of removing the excerpts.
13308 follower_1
13309 .update_in(cx, |follower, window, cx| {
13310 follower.apply_update_proto(
13311 &project,
13312 update_message.borrow().clone().unwrap(),
13313 window,
13314 cx,
13315 )
13316 })
13317 .await
13318 .unwrap();
13319 follower_2
13320 .update_in(cx, |follower, window, cx| {
13321 follower.apply_update_proto(
13322 &project,
13323 update_message.borrow().clone().unwrap(),
13324 window,
13325 cx,
13326 )
13327 })
13328 .await
13329 .unwrap();
13330 update_message.borrow_mut().take();
13331 assert_eq!(
13332 follower_1.update(cx, |editor, cx| editor.text(cx)),
13333 leader.update(cx, |editor, cx| editor.text(cx))
13334 );
13335}
13336
13337#[gpui::test]
13338async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13339 init_test(cx, |_| {});
13340
13341 let mut cx = EditorTestContext::new(cx).await;
13342 let lsp_store =
13343 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13344
13345 cx.set_state(indoc! {"
13346 ˇfn func(abc def: i32) -> u32 {
13347 }
13348 "});
13349
13350 cx.update(|_, cx| {
13351 lsp_store.update(cx, |lsp_store, cx| {
13352 lsp_store
13353 .update_diagnostics(
13354 LanguageServerId(0),
13355 lsp::PublishDiagnosticsParams {
13356 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13357 version: None,
13358 diagnostics: vec![
13359 lsp::Diagnostic {
13360 range: lsp::Range::new(
13361 lsp::Position::new(0, 11),
13362 lsp::Position::new(0, 12),
13363 ),
13364 severity: Some(lsp::DiagnosticSeverity::ERROR),
13365 ..Default::default()
13366 },
13367 lsp::Diagnostic {
13368 range: lsp::Range::new(
13369 lsp::Position::new(0, 12),
13370 lsp::Position::new(0, 15),
13371 ),
13372 severity: Some(lsp::DiagnosticSeverity::ERROR),
13373 ..Default::default()
13374 },
13375 lsp::Diagnostic {
13376 range: lsp::Range::new(
13377 lsp::Position::new(0, 25),
13378 lsp::Position::new(0, 28),
13379 ),
13380 severity: Some(lsp::DiagnosticSeverity::ERROR),
13381 ..Default::default()
13382 },
13383 ],
13384 },
13385 &[],
13386 cx,
13387 )
13388 .unwrap()
13389 });
13390 });
13391
13392 executor.run_until_parked();
13393
13394 cx.update_editor(|editor, window, cx| {
13395 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13396 });
13397
13398 cx.assert_editor_state(indoc! {"
13399 fn func(abc def: i32) -> ˇu32 {
13400 }
13401 "});
13402
13403 cx.update_editor(|editor, window, cx| {
13404 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13405 });
13406
13407 cx.assert_editor_state(indoc! {"
13408 fn func(abc ˇdef: i32) -> u32 {
13409 }
13410 "});
13411
13412 cx.update_editor(|editor, window, cx| {
13413 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13414 });
13415
13416 cx.assert_editor_state(indoc! {"
13417 fn func(abcˇ def: i32) -> u32 {
13418 }
13419 "});
13420
13421 cx.update_editor(|editor, window, cx| {
13422 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13423 });
13424
13425 cx.assert_editor_state(indoc! {"
13426 fn func(abc def: i32) -> ˇu32 {
13427 }
13428 "});
13429}
13430
13431#[gpui::test]
13432async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13433 init_test(cx, |_| {});
13434
13435 let mut cx = EditorTestContext::new(cx).await;
13436
13437 let diff_base = r#"
13438 use some::mod;
13439
13440 const A: u32 = 42;
13441
13442 fn main() {
13443 println!("hello");
13444
13445 println!("world");
13446 }
13447 "#
13448 .unindent();
13449
13450 // Edits are modified, removed, modified, added
13451 cx.set_state(
13452 &r#"
13453 use some::modified;
13454
13455 ˇ
13456 fn main() {
13457 println!("hello there");
13458
13459 println!("around the");
13460 println!("world");
13461 }
13462 "#
13463 .unindent(),
13464 );
13465
13466 cx.set_head_text(&diff_base);
13467 executor.run_until_parked();
13468
13469 cx.update_editor(|editor, window, cx| {
13470 //Wrap around the bottom of the buffer
13471 for _ in 0..3 {
13472 editor.go_to_next_hunk(&GoToHunk, window, cx);
13473 }
13474 });
13475
13476 cx.assert_editor_state(
13477 &r#"
13478 ˇuse some::modified;
13479
13480
13481 fn main() {
13482 println!("hello there");
13483
13484 println!("around the");
13485 println!("world");
13486 }
13487 "#
13488 .unindent(),
13489 );
13490
13491 cx.update_editor(|editor, window, cx| {
13492 //Wrap around the top of the buffer
13493 for _ in 0..2 {
13494 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13495 }
13496 });
13497
13498 cx.assert_editor_state(
13499 &r#"
13500 use some::modified;
13501
13502
13503 fn main() {
13504 ˇ println!("hello there");
13505
13506 println!("around the");
13507 println!("world");
13508 }
13509 "#
13510 .unindent(),
13511 );
13512
13513 cx.update_editor(|editor, window, cx| {
13514 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13515 });
13516
13517 cx.assert_editor_state(
13518 &r#"
13519 use some::modified;
13520
13521 ˇ
13522 fn main() {
13523 println!("hello there");
13524
13525 println!("around the");
13526 println!("world");
13527 }
13528 "#
13529 .unindent(),
13530 );
13531
13532 cx.update_editor(|editor, window, cx| {
13533 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13534 });
13535
13536 cx.assert_editor_state(
13537 &r#"
13538 ˇuse some::modified;
13539
13540
13541 fn main() {
13542 println!("hello there");
13543
13544 println!("around the");
13545 println!("world");
13546 }
13547 "#
13548 .unindent(),
13549 );
13550
13551 cx.update_editor(|editor, window, cx| {
13552 for _ in 0..2 {
13553 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13554 }
13555 });
13556
13557 cx.assert_editor_state(
13558 &r#"
13559 use some::modified;
13560
13561
13562 fn main() {
13563 ˇ println!("hello there");
13564
13565 println!("around the");
13566 println!("world");
13567 }
13568 "#
13569 .unindent(),
13570 );
13571
13572 cx.update_editor(|editor, window, cx| {
13573 editor.fold(&Fold, window, cx);
13574 });
13575
13576 cx.update_editor(|editor, window, cx| {
13577 editor.go_to_next_hunk(&GoToHunk, window, cx);
13578 });
13579
13580 cx.assert_editor_state(
13581 &r#"
13582 ˇuse some::modified;
13583
13584
13585 fn main() {
13586 println!("hello there");
13587
13588 println!("around the");
13589 println!("world");
13590 }
13591 "#
13592 .unindent(),
13593 );
13594}
13595
13596#[test]
13597fn test_split_words() {
13598 fn split(text: &str) -> Vec<&str> {
13599 split_words(text).collect()
13600 }
13601
13602 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13603 assert_eq!(split("hello_world"), &["hello_", "world"]);
13604 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13605 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13606 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13607 assert_eq!(split("helloworld"), &["helloworld"]);
13608
13609 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13610}
13611
13612#[gpui::test]
13613async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13614 init_test(cx, |_| {});
13615
13616 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13617 let mut assert = |before, after| {
13618 let _state_context = cx.set_state(before);
13619 cx.run_until_parked();
13620 cx.update_editor(|editor, window, cx| {
13621 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13622 });
13623 cx.run_until_parked();
13624 cx.assert_editor_state(after);
13625 };
13626
13627 // Outside bracket jumps to outside of matching bracket
13628 assert("console.logˇ(var);", "console.log(var)ˇ;");
13629 assert("console.log(var)ˇ;", "console.logˇ(var);");
13630
13631 // Inside bracket jumps to inside of matching bracket
13632 assert("console.log(ˇvar);", "console.log(varˇ);");
13633 assert("console.log(varˇ);", "console.log(ˇvar);");
13634
13635 // When outside a bracket and inside, favor jumping to the inside bracket
13636 assert(
13637 "console.log('foo', [1, 2, 3]ˇ);",
13638 "console.log(ˇ'foo', [1, 2, 3]);",
13639 );
13640 assert(
13641 "console.log(ˇ'foo', [1, 2, 3]);",
13642 "console.log('foo', [1, 2, 3]ˇ);",
13643 );
13644
13645 // Bias forward if two options are equally likely
13646 assert(
13647 "let result = curried_fun()ˇ();",
13648 "let result = curried_fun()()ˇ;",
13649 );
13650
13651 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13652 assert(
13653 indoc! {"
13654 function test() {
13655 console.log('test')ˇ
13656 }"},
13657 indoc! {"
13658 function test() {
13659 console.logˇ('test')
13660 }"},
13661 );
13662}
13663
13664#[gpui::test]
13665async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13666 init_test(cx, |_| {});
13667
13668 let fs = FakeFs::new(cx.executor());
13669 fs.insert_tree(
13670 path!("/a"),
13671 json!({
13672 "main.rs": "fn main() { let a = 5; }",
13673 "other.rs": "// Test file",
13674 }),
13675 )
13676 .await;
13677 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13678
13679 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13680 language_registry.add(Arc::new(Language::new(
13681 LanguageConfig {
13682 name: "Rust".into(),
13683 matcher: LanguageMatcher {
13684 path_suffixes: vec!["rs".to_string()],
13685 ..Default::default()
13686 },
13687 brackets: BracketPairConfig {
13688 pairs: vec![BracketPair {
13689 start: "{".to_string(),
13690 end: "}".to_string(),
13691 close: true,
13692 surround: true,
13693 newline: true,
13694 }],
13695 disabled_scopes_by_bracket_ix: Vec::new(),
13696 },
13697 ..Default::default()
13698 },
13699 Some(tree_sitter_rust::LANGUAGE.into()),
13700 )));
13701 let mut fake_servers = language_registry.register_fake_lsp(
13702 "Rust",
13703 FakeLspAdapter {
13704 capabilities: lsp::ServerCapabilities {
13705 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13706 first_trigger_character: "{".to_string(),
13707 more_trigger_character: None,
13708 }),
13709 ..Default::default()
13710 },
13711 ..Default::default()
13712 },
13713 );
13714
13715 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13716
13717 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13718
13719 let worktree_id = workspace
13720 .update(cx, |workspace, _, cx| {
13721 workspace.project().update(cx, |project, cx| {
13722 project.worktrees(cx).next().unwrap().read(cx).id()
13723 })
13724 })
13725 .unwrap();
13726
13727 let buffer = project
13728 .update(cx, |project, cx| {
13729 project.open_local_buffer(path!("/a/main.rs"), cx)
13730 })
13731 .await
13732 .unwrap();
13733 let editor_handle = workspace
13734 .update(cx, |workspace, window, cx| {
13735 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13736 })
13737 .unwrap()
13738 .await
13739 .unwrap()
13740 .downcast::<Editor>()
13741 .unwrap();
13742
13743 cx.executor().start_waiting();
13744 let fake_server = fake_servers.next().await.unwrap();
13745
13746 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13747 |params, _| async move {
13748 assert_eq!(
13749 params.text_document_position.text_document.uri,
13750 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13751 );
13752 assert_eq!(
13753 params.text_document_position.position,
13754 lsp::Position::new(0, 21),
13755 );
13756
13757 Ok(Some(vec![lsp::TextEdit {
13758 new_text: "]".to_string(),
13759 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13760 }]))
13761 },
13762 );
13763
13764 editor_handle.update_in(cx, |editor, window, cx| {
13765 window.focus(&editor.focus_handle(cx));
13766 editor.change_selections(None, window, cx, |s| {
13767 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13768 });
13769 editor.handle_input("{", window, cx);
13770 });
13771
13772 cx.executor().run_until_parked();
13773
13774 buffer.update(cx, |buffer, _| {
13775 assert_eq!(
13776 buffer.text(),
13777 "fn main() { let a = {5}; }",
13778 "No extra braces from on type formatting should appear in the buffer"
13779 )
13780 });
13781}
13782
13783#[gpui::test]
13784async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13785 init_test(cx, |_| {});
13786
13787 let fs = FakeFs::new(cx.executor());
13788 fs.insert_tree(
13789 path!("/a"),
13790 json!({
13791 "main.rs": "fn main() { let a = 5; }",
13792 "other.rs": "// Test file",
13793 }),
13794 )
13795 .await;
13796
13797 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13798
13799 let server_restarts = Arc::new(AtomicUsize::new(0));
13800 let closure_restarts = Arc::clone(&server_restarts);
13801 let language_server_name = "test language server";
13802 let language_name: LanguageName = "Rust".into();
13803
13804 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13805 language_registry.add(Arc::new(Language::new(
13806 LanguageConfig {
13807 name: language_name.clone(),
13808 matcher: LanguageMatcher {
13809 path_suffixes: vec!["rs".to_string()],
13810 ..Default::default()
13811 },
13812 ..Default::default()
13813 },
13814 Some(tree_sitter_rust::LANGUAGE.into()),
13815 )));
13816 let mut fake_servers = language_registry.register_fake_lsp(
13817 "Rust",
13818 FakeLspAdapter {
13819 name: language_server_name,
13820 initialization_options: Some(json!({
13821 "testOptionValue": true
13822 })),
13823 initializer: Some(Box::new(move |fake_server| {
13824 let task_restarts = Arc::clone(&closure_restarts);
13825 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13826 task_restarts.fetch_add(1, atomic::Ordering::Release);
13827 futures::future::ready(Ok(()))
13828 });
13829 })),
13830 ..Default::default()
13831 },
13832 );
13833
13834 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13835 let _buffer = project
13836 .update(cx, |project, cx| {
13837 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13838 })
13839 .await
13840 .unwrap();
13841 let _fake_server = fake_servers.next().await.unwrap();
13842 update_test_language_settings(cx, |language_settings| {
13843 language_settings.languages.insert(
13844 language_name.clone(),
13845 LanguageSettingsContent {
13846 tab_size: NonZeroU32::new(8),
13847 ..Default::default()
13848 },
13849 );
13850 });
13851 cx.executor().run_until_parked();
13852 assert_eq!(
13853 server_restarts.load(atomic::Ordering::Acquire),
13854 0,
13855 "Should not restart LSP server on an unrelated change"
13856 );
13857
13858 update_test_project_settings(cx, |project_settings| {
13859 project_settings.lsp.insert(
13860 "Some other server name".into(),
13861 LspSettings {
13862 binary: None,
13863 settings: None,
13864 initialization_options: Some(json!({
13865 "some other init value": false
13866 })),
13867 enable_lsp_tasks: false,
13868 },
13869 );
13870 });
13871 cx.executor().run_until_parked();
13872 assert_eq!(
13873 server_restarts.load(atomic::Ordering::Acquire),
13874 0,
13875 "Should not restart LSP server on an unrelated LSP settings change"
13876 );
13877
13878 update_test_project_settings(cx, |project_settings| {
13879 project_settings.lsp.insert(
13880 language_server_name.into(),
13881 LspSettings {
13882 binary: None,
13883 settings: None,
13884 initialization_options: Some(json!({
13885 "anotherInitValue": false
13886 })),
13887 enable_lsp_tasks: false,
13888 },
13889 );
13890 });
13891 cx.executor().run_until_parked();
13892 assert_eq!(
13893 server_restarts.load(atomic::Ordering::Acquire),
13894 1,
13895 "Should restart LSP server on a related LSP settings change"
13896 );
13897
13898 update_test_project_settings(cx, |project_settings| {
13899 project_settings.lsp.insert(
13900 language_server_name.into(),
13901 LspSettings {
13902 binary: None,
13903 settings: None,
13904 initialization_options: Some(json!({
13905 "anotherInitValue": false
13906 })),
13907 enable_lsp_tasks: false,
13908 },
13909 );
13910 });
13911 cx.executor().run_until_parked();
13912 assert_eq!(
13913 server_restarts.load(atomic::Ordering::Acquire),
13914 1,
13915 "Should not restart LSP server on a related LSP settings change that is the same"
13916 );
13917
13918 update_test_project_settings(cx, |project_settings| {
13919 project_settings.lsp.insert(
13920 language_server_name.into(),
13921 LspSettings {
13922 binary: None,
13923 settings: None,
13924 initialization_options: None,
13925 enable_lsp_tasks: false,
13926 },
13927 );
13928 });
13929 cx.executor().run_until_parked();
13930 assert_eq!(
13931 server_restarts.load(atomic::Ordering::Acquire),
13932 2,
13933 "Should restart LSP server on another related LSP settings change"
13934 );
13935}
13936
13937#[gpui::test]
13938async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13939 init_test(cx, |_| {});
13940
13941 let mut cx = EditorLspTestContext::new_rust(
13942 lsp::ServerCapabilities {
13943 completion_provider: Some(lsp::CompletionOptions {
13944 trigger_characters: Some(vec![".".to_string()]),
13945 resolve_provider: Some(true),
13946 ..Default::default()
13947 }),
13948 ..Default::default()
13949 },
13950 cx,
13951 )
13952 .await;
13953
13954 cx.set_state("fn main() { let a = 2ˇ; }");
13955 cx.simulate_keystroke(".");
13956 let completion_item = lsp::CompletionItem {
13957 label: "some".into(),
13958 kind: Some(lsp::CompletionItemKind::SNIPPET),
13959 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13960 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13961 kind: lsp::MarkupKind::Markdown,
13962 value: "```rust\nSome(2)\n```".to_string(),
13963 })),
13964 deprecated: Some(false),
13965 sort_text: Some("fffffff2".to_string()),
13966 filter_text: Some("some".to_string()),
13967 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13968 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13969 range: lsp::Range {
13970 start: lsp::Position {
13971 line: 0,
13972 character: 22,
13973 },
13974 end: lsp::Position {
13975 line: 0,
13976 character: 22,
13977 },
13978 },
13979 new_text: "Some(2)".to_string(),
13980 })),
13981 additional_text_edits: Some(vec![lsp::TextEdit {
13982 range: lsp::Range {
13983 start: lsp::Position {
13984 line: 0,
13985 character: 20,
13986 },
13987 end: lsp::Position {
13988 line: 0,
13989 character: 22,
13990 },
13991 },
13992 new_text: "".to_string(),
13993 }]),
13994 ..Default::default()
13995 };
13996
13997 let closure_completion_item = completion_item.clone();
13998 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13999 let task_completion_item = closure_completion_item.clone();
14000 async move {
14001 Ok(Some(lsp::CompletionResponse::Array(vec![
14002 task_completion_item,
14003 ])))
14004 }
14005 });
14006
14007 request.next().await;
14008
14009 cx.condition(|editor, _| editor.context_menu_visible())
14010 .await;
14011 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14012 editor
14013 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14014 .unwrap()
14015 });
14016 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14017
14018 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14019 let task_completion_item = completion_item.clone();
14020 async move { Ok(task_completion_item) }
14021 })
14022 .next()
14023 .await
14024 .unwrap();
14025 apply_additional_edits.await.unwrap();
14026 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14027}
14028
14029#[gpui::test]
14030async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14031 init_test(cx, |_| {});
14032
14033 let mut cx = EditorLspTestContext::new_rust(
14034 lsp::ServerCapabilities {
14035 completion_provider: Some(lsp::CompletionOptions {
14036 trigger_characters: Some(vec![".".to_string()]),
14037 resolve_provider: Some(true),
14038 ..Default::default()
14039 }),
14040 ..Default::default()
14041 },
14042 cx,
14043 )
14044 .await;
14045
14046 cx.set_state("fn main() { let a = 2ˇ; }");
14047 cx.simulate_keystroke(".");
14048
14049 let item1 = lsp::CompletionItem {
14050 label: "method id()".to_string(),
14051 filter_text: Some("id".to_string()),
14052 detail: None,
14053 documentation: None,
14054 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14055 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14056 new_text: ".id".to_string(),
14057 })),
14058 ..lsp::CompletionItem::default()
14059 };
14060
14061 let item2 = lsp::CompletionItem {
14062 label: "other".to_string(),
14063 filter_text: Some("other".to_string()),
14064 detail: None,
14065 documentation: None,
14066 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14067 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14068 new_text: ".other".to_string(),
14069 })),
14070 ..lsp::CompletionItem::default()
14071 };
14072
14073 let item1 = item1.clone();
14074 cx.set_request_handler::<lsp::request::Completion, _, _>({
14075 let item1 = item1.clone();
14076 move |_, _, _| {
14077 let item1 = item1.clone();
14078 let item2 = item2.clone();
14079 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14080 }
14081 })
14082 .next()
14083 .await;
14084
14085 cx.condition(|editor, _| editor.context_menu_visible())
14086 .await;
14087 cx.update_editor(|editor, _, _| {
14088 let context_menu = editor.context_menu.borrow_mut();
14089 let context_menu = context_menu
14090 .as_ref()
14091 .expect("Should have the context menu deployed");
14092 match context_menu {
14093 CodeContextMenu::Completions(completions_menu) => {
14094 let completions = completions_menu.completions.borrow_mut();
14095 assert_eq!(
14096 completions
14097 .iter()
14098 .map(|completion| &completion.label.text)
14099 .collect::<Vec<_>>(),
14100 vec!["method id()", "other"]
14101 )
14102 }
14103 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14104 }
14105 });
14106
14107 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14108 let item1 = item1.clone();
14109 move |_, item_to_resolve, _| {
14110 let item1 = item1.clone();
14111 async move {
14112 if item1 == item_to_resolve {
14113 Ok(lsp::CompletionItem {
14114 label: "method id()".to_string(),
14115 filter_text: Some("id".to_string()),
14116 detail: Some("Now resolved!".to_string()),
14117 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14118 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14119 range: lsp::Range::new(
14120 lsp::Position::new(0, 22),
14121 lsp::Position::new(0, 22),
14122 ),
14123 new_text: ".id".to_string(),
14124 })),
14125 ..lsp::CompletionItem::default()
14126 })
14127 } else {
14128 Ok(item_to_resolve)
14129 }
14130 }
14131 }
14132 })
14133 .next()
14134 .await
14135 .unwrap();
14136 cx.run_until_parked();
14137
14138 cx.update_editor(|editor, window, cx| {
14139 editor.context_menu_next(&Default::default(), window, cx);
14140 });
14141
14142 cx.update_editor(|editor, _, _| {
14143 let context_menu = editor.context_menu.borrow_mut();
14144 let context_menu = context_menu
14145 .as_ref()
14146 .expect("Should have the context menu deployed");
14147 match context_menu {
14148 CodeContextMenu::Completions(completions_menu) => {
14149 let completions = completions_menu.completions.borrow_mut();
14150 assert_eq!(
14151 completions
14152 .iter()
14153 .map(|completion| &completion.label.text)
14154 .collect::<Vec<_>>(),
14155 vec!["method id() Now resolved!", "other"],
14156 "Should update first completion label, but not second as the filter text did not match."
14157 );
14158 }
14159 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14160 }
14161 });
14162}
14163
14164#[gpui::test]
14165async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14166 init_test(cx, |_| {});
14167 let mut cx = EditorLspTestContext::new_rust(
14168 lsp::ServerCapabilities {
14169 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14170 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14171 completion_provider: Some(lsp::CompletionOptions {
14172 resolve_provider: Some(true),
14173 ..Default::default()
14174 }),
14175 ..Default::default()
14176 },
14177 cx,
14178 )
14179 .await;
14180 cx.set_state(indoc! {"
14181 struct TestStruct {
14182 field: i32
14183 }
14184
14185 fn mainˇ() {
14186 let unused_var = 42;
14187 let test_struct = TestStruct { field: 42 };
14188 }
14189 "});
14190 let symbol_range = cx.lsp_range(indoc! {"
14191 struct TestStruct {
14192 field: i32
14193 }
14194
14195 «fn main»() {
14196 let unused_var = 42;
14197 let test_struct = TestStruct { field: 42 };
14198 }
14199 "});
14200 let mut hover_requests =
14201 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14202 Ok(Some(lsp::Hover {
14203 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14204 kind: lsp::MarkupKind::Markdown,
14205 value: "Function documentation".to_string(),
14206 }),
14207 range: Some(symbol_range),
14208 }))
14209 });
14210
14211 // Case 1: Test that code action menu hide hover popover
14212 cx.dispatch_action(Hover);
14213 hover_requests.next().await;
14214 cx.condition(|editor, _| editor.hover_state.visible()).await;
14215 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14216 move |_, _, _| async move {
14217 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14218 lsp::CodeAction {
14219 title: "Remove unused variable".to_string(),
14220 kind: Some(CodeActionKind::QUICKFIX),
14221 edit: Some(lsp::WorkspaceEdit {
14222 changes: Some(
14223 [(
14224 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14225 vec![lsp::TextEdit {
14226 range: lsp::Range::new(
14227 lsp::Position::new(5, 4),
14228 lsp::Position::new(5, 27),
14229 ),
14230 new_text: "".to_string(),
14231 }],
14232 )]
14233 .into_iter()
14234 .collect(),
14235 ),
14236 ..Default::default()
14237 }),
14238 ..Default::default()
14239 },
14240 )]))
14241 },
14242 );
14243 cx.update_editor(|editor, window, cx| {
14244 editor.toggle_code_actions(
14245 &ToggleCodeActions {
14246 deployed_from: None,
14247 quick_launch: false,
14248 },
14249 window,
14250 cx,
14251 );
14252 });
14253 code_action_requests.next().await;
14254 cx.run_until_parked();
14255 cx.condition(|editor, _| editor.context_menu_visible())
14256 .await;
14257 cx.update_editor(|editor, _, _| {
14258 assert!(
14259 !editor.hover_state.visible(),
14260 "Hover popover should be hidden when code action menu is shown"
14261 );
14262 // Hide code actions
14263 editor.context_menu.take();
14264 });
14265
14266 // Case 2: Test that code completions hide hover popover
14267 cx.dispatch_action(Hover);
14268 hover_requests.next().await;
14269 cx.condition(|editor, _| editor.hover_state.visible()).await;
14270 let counter = Arc::new(AtomicUsize::new(0));
14271 let mut completion_requests =
14272 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14273 let counter = counter.clone();
14274 async move {
14275 counter.fetch_add(1, atomic::Ordering::Release);
14276 Ok(Some(lsp::CompletionResponse::Array(vec![
14277 lsp::CompletionItem {
14278 label: "main".into(),
14279 kind: Some(lsp::CompletionItemKind::FUNCTION),
14280 detail: Some("() -> ()".to_string()),
14281 ..Default::default()
14282 },
14283 lsp::CompletionItem {
14284 label: "TestStruct".into(),
14285 kind: Some(lsp::CompletionItemKind::STRUCT),
14286 detail: Some("struct TestStruct".to_string()),
14287 ..Default::default()
14288 },
14289 ])))
14290 }
14291 });
14292 cx.update_editor(|editor, window, cx| {
14293 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14294 });
14295 completion_requests.next().await;
14296 cx.condition(|editor, _| editor.context_menu_visible())
14297 .await;
14298 cx.update_editor(|editor, _, _| {
14299 assert!(
14300 !editor.hover_state.visible(),
14301 "Hover popover should be hidden when completion menu is shown"
14302 );
14303 });
14304}
14305
14306#[gpui::test]
14307async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14308 init_test(cx, |_| {});
14309
14310 let mut cx = EditorLspTestContext::new_rust(
14311 lsp::ServerCapabilities {
14312 completion_provider: Some(lsp::CompletionOptions {
14313 trigger_characters: Some(vec![".".to_string()]),
14314 resolve_provider: Some(true),
14315 ..Default::default()
14316 }),
14317 ..Default::default()
14318 },
14319 cx,
14320 )
14321 .await;
14322
14323 cx.set_state("fn main() { let a = 2ˇ; }");
14324 cx.simulate_keystroke(".");
14325
14326 let unresolved_item_1 = lsp::CompletionItem {
14327 label: "id".to_string(),
14328 filter_text: Some("id".to_string()),
14329 detail: None,
14330 documentation: None,
14331 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14332 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14333 new_text: ".id".to_string(),
14334 })),
14335 ..lsp::CompletionItem::default()
14336 };
14337 let resolved_item_1 = lsp::CompletionItem {
14338 additional_text_edits: Some(vec![lsp::TextEdit {
14339 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14340 new_text: "!!".to_string(),
14341 }]),
14342 ..unresolved_item_1.clone()
14343 };
14344 let unresolved_item_2 = lsp::CompletionItem {
14345 label: "other".to_string(),
14346 filter_text: Some("other".to_string()),
14347 detail: None,
14348 documentation: None,
14349 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14350 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14351 new_text: ".other".to_string(),
14352 })),
14353 ..lsp::CompletionItem::default()
14354 };
14355 let resolved_item_2 = lsp::CompletionItem {
14356 additional_text_edits: Some(vec![lsp::TextEdit {
14357 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14358 new_text: "??".to_string(),
14359 }]),
14360 ..unresolved_item_2.clone()
14361 };
14362
14363 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14364 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14365 cx.lsp
14366 .server
14367 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14368 let unresolved_item_1 = unresolved_item_1.clone();
14369 let resolved_item_1 = resolved_item_1.clone();
14370 let unresolved_item_2 = unresolved_item_2.clone();
14371 let resolved_item_2 = resolved_item_2.clone();
14372 let resolve_requests_1 = resolve_requests_1.clone();
14373 let resolve_requests_2 = resolve_requests_2.clone();
14374 move |unresolved_request, _| {
14375 let unresolved_item_1 = unresolved_item_1.clone();
14376 let resolved_item_1 = resolved_item_1.clone();
14377 let unresolved_item_2 = unresolved_item_2.clone();
14378 let resolved_item_2 = resolved_item_2.clone();
14379 let resolve_requests_1 = resolve_requests_1.clone();
14380 let resolve_requests_2 = resolve_requests_2.clone();
14381 async move {
14382 if unresolved_request == unresolved_item_1 {
14383 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14384 Ok(resolved_item_1.clone())
14385 } else if unresolved_request == unresolved_item_2 {
14386 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14387 Ok(resolved_item_2.clone())
14388 } else {
14389 panic!("Unexpected completion item {unresolved_request:?}")
14390 }
14391 }
14392 }
14393 })
14394 .detach();
14395
14396 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14397 let unresolved_item_1 = unresolved_item_1.clone();
14398 let unresolved_item_2 = unresolved_item_2.clone();
14399 async move {
14400 Ok(Some(lsp::CompletionResponse::Array(vec![
14401 unresolved_item_1,
14402 unresolved_item_2,
14403 ])))
14404 }
14405 })
14406 .next()
14407 .await;
14408
14409 cx.condition(|editor, _| editor.context_menu_visible())
14410 .await;
14411 cx.update_editor(|editor, _, _| {
14412 let context_menu = editor.context_menu.borrow_mut();
14413 let context_menu = context_menu
14414 .as_ref()
14415 .expect("Should have the context menu deployed");
14416 match context_menu {
14417 CodeContextMenu::Completions(completions_menu) => {
14418 let completions = completions_menu.completions.borrow_mut();
14419 assert_eq!(
14420 completions
14421 .iter()
14422 .map(|completion| &completion.label.text)
14423 .collect::<Vec<_>>(),
14424 vec!["id", "other"]
14425 )
14426 }
14427 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14428 }
14429 });
14430 cx.run_until_parked();
14431
14432 cx.update_editor(|editor, window, cx| {
14433 editor.context_menu_next(&ContextMenuNext, window, cx);
14434 });
14435 cx.run_until_parked();
14436 cx.update_editor(|editor, window, cx| {
14437 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14438 });
14439 cx.run_until_parked();
14440 cx.update_editor(|editor, window, cx| {
14441 editor.context_menu_next(&ContextMenuNext, window, cx);
14442 });
14443 cx.run_until_parked();
14444 cx.update_editor(|editor, window, cx| {
14445 editor
14446 .compose_completion(&ComposeCompletion::default(), window, cx)
14447 .expect("No task returned")
14448 })
14449 .await
14450 .expect("Completion failed");
14451 cx.run_until_parked();
14452
14453 cx.update_editor(|editor, _, cx| {
14454 assert_eq!(
14455 resolve_requests_1.load(atomic::Ordering::Acquire),
14456 1,
14457 "Should always resolve once despite multiple selections"
14458 );
14459 assert_eq!(
14460 resolve_requests_2.load(atomic::Ordering::Acquire),
14461 1,
14462 "Should always resolve once after multiple selections and applying the completion"
14463 );
14464 assert_eq!(
14465 editor.text(cx),
14466 "fn main() { let a = ??.other; }",
14467 "Should use resolved data when applying the completion"
14468 );
14469 });
14470}
14471
14472#[gpui::test]
14473async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14474 init_test(cx, |_| {});
14475
14476 let item_0 = lsp::CompletionItem {
14477 label: "abs".into(),
14478 insert_text: Some("abs".into()),
14479 data: Some(json!({ "very": "special"})),
14480 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14481 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14482 lsp::InsertReplaceEdit {
14483 new_text: "abs".to_string(),
14484 insert: lsp::Range::default(),
14485 replace: lsp::Range::default(),
14486 },
14487 )),
14488 ..lsp::CompletionItem::default()
14489 };
14490 let items = iter::once(item_0.clone())
14491 .chain((11..51).map(|i| lsp::CompletionItem {
14492 label: format!("item_{}", i),
14493 insert_text: Some(format!("item_{}", i)),
14494 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14495 ..lsp::CompletionItem::default()
14496 }))
14497 .collect::<Vec<_>>();
14498
14499 let default_commit_characters = vec!["?".to_string()];
14500 let default_data = json!({ "default": "data"});
14501 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14502 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14503 let default_edit_range = lsp::Range {
14504 start: lsp::Position {
14505 line: 0,
14506 character: 5,
14507 },
14508 end: lsp::Position {
14509 line: 0,
14510 character: 5,
14511 },
14512 };
14513
14514 let mut cx = EditorLspTestContext::new_rust(
14515 lsp::ServerCapabilities {
14516 completion_provider: Some(lsp::CompletionOptions {
14517 trigger_characters: Some(vec![".".to_string()]),
14518 resolve_provider: Some(true),
14519 ..Default::default()
14520 }),
14521 ..Default::default()
14522 },
14523 cx,
14524 )
14525 .await;
14526
14527 cx.set_state("fn main() { let a = 2ˇ; }");
14528 cx.simulate_keystroke(".");
14529
14530 let completion_data = default_data.clone();
14531 let completion_characters = default_commit_characters.clone();
14532 let completion_items = items.clone();
14533 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14534 let default_data = completion_data.clone();
14535 let default_commit_characters = completion_characters.clone();
14536 let items = completion_items.clone();
14537 async move {
14538 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14539 items,
14540 item_defaults: Some(lsp::CompletionListItemDefaults {
14541 data: Some(default_data.clone()),
14542 commit_characters: Some(default_commit_characters.clone()),
14543 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14544 default_edit_range,
14545 )),
14546 insert_text_format: Some(default_insert_text_format),
14547 insert_text_mode: Some(default_insert_text_mode),
14548 }),
14549 ..lsp::CompletionList::default()
14550 })))
14551 }
14552 })
14553 .next()
14554 .await;
14555
14556 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14557 cx.lsp
14558 .server
14559 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14560 let closure_resolved_items = resolved_items.clone();
14561 move |item_to_resolve, _| {
14562 let closure_resolved_items = closure_resolved_items.clone();
14563 async move {
14564 closure_resolved_items.lock().push(item_to_resolve.clone());
14565 Ok(item_to_resolve)
14566 }
14567 }
14568 })
14569 .detach();
14570
14571 cx.condition(|editor, _| editor.context_menu_visible())
14572 .await;
14573 cx.run_until_parked();
14574 cx.update_editor(|editor, _, _| {
14575 let menu = editor.context_menu.borrow_mut();
14576 match menu.as_ref().expect("should have the completions menu") {
14577 CodeContextMenu::Completions(completions_menu) => {
14578 assert_eq!(
14579 completions_menu
14580 .entries
14581 .borrow()
14582 .iter()
14583 .map(|mat| mat.string.clone())
14584 .collect::<Vec<String>>(),
14585 items
14586 .iter()
14587 .map(|completion| completion.label.clone())
14588 .collect::<Vec<String>>()
14589 );
14590 }
14591 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14592 }
14593 });
14594 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14595 // with 4 from the end.
14596 assert_eq!(
14597 *resolved_items.lock(),
14598 [&items[0..16], &items[items.len() - 4..items.len()]]
14599 .concat()
14600 .iter()
14601 .cloned()
14602 .map(|mut item| {
14603 if item.data.is_none() {
14604 item.data = Some(default_data.clone());
14605 }
14606 item
14607 })
14608 .collect::<Vec<lsp::CompletionItem>>(),
14609 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14610 );
14611 resolved_items.lock().clear();
14612
14613 cx.update_editor(|editor, window, cx| {
14614 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14615 });
14616 cx.run_until_parked();
14617 // Completions that have already been resolved are skipped.
14618 assert_eq!(
14619 *resolved_items.lock(),
14620 items[items.len() - 16..items.len() - 4]
14621 .iter()
14622 .cloned()
14623 .map(|mut item| {
14624 if item.data.is_none() {
14625 item.data = Some(default_data.clone());
14626 }
14627 item
14628 })
14629 .collect::<Vec<lsp::CompletionItem>>()
14630 );
14631 resolved_items.lock().clear();
14632}
14633
14634#[gpui::test]
14635async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14636 init_test(cx, |_| {});
14637
14638 let mut cx = EditorLspTestContext::new(
14639 Language::new(
14640 LanguageConfig {
14641 matcher: LanguageMatcher {
14642 path_suffixes: vec!["jsx".into()],
14643 ..Default::default()
14644 },
14645 overrides: [(
14646 "element".into(),
14647 LanguageConfigOverride {
14648 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14649 ..Default::default()
14650 },
14651 )]
14652 .into_iter()
14653 .collect(),
14654 ..Default::default()
14655 },
14656 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14657 )
14658 .with_override_query("(jsx_self_closing_element) @element")
14659 .unwrap(),
14660 lsp::ServerCapabilities {
14661 completion_provider: Some(lsp::CompletionOptions {
14662 trigger_characters: Some(vec![":".to_string()]),
14663 ..Default::default()
14664 }),
14665 ..Default::default()
14666 },
14667 cx,
14668 )
14669 .await;
14670
14671 cx.lsp
14672 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14673 Ok(Some(lsp::CompletionResponse::Array(vec![
14674 lsp::CompletionItem {
14675 label: "bg-blue".into(),
14676 ..Default::default()
14677 },
14678 lsp::CompletionItem {
14679 label: "bg-red".into(),
14680 ..Default::default()
14681 },
14682 lsp::CompletionItem {
14683 label: "bg-yellow".into(),
14684 ..Default::default()
14685 },
14686 ])))
14687 });
14688
14689 cx.set_state(r#"<p class="bgˇ" />"#);
14690
14691 // Trigger completion when typing a dash, because the dash is an extra
14692 // word character in the 'element' scope, which contains the cursor.
14693 cx.simulate_keystroke("-");
14694 cx.executor().run_until_parked();
14695 cx.update_editor(|editor, _, _| {
14696 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14697 {
14698 assert_eq!(
14699 completion_menu_entries(&menu),
14700 &["bg-red", "bg-blue", "bg-yellow"]
14701 );
14702 } else {
14703 panic!("expected completion menu to be open");
14704 }
14705 });
14706
14707 cx.simulate_keystroke("l");
14708 cx.executor().run_until_parked();
14709 cx.update_editor(|editor, _, _| {
14710 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14711 {
14712 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14713 } else {
14714 panic!("expected completion menu to be open");
14715 }
14716 });
14717
14718 // When filtering completions, consider the character after the '-' to
14719 // be the start of a subword.
14720 cx.set_state(r#"<p class="yelˇ" />"#);
14721 cx.simulate_keystroke("l");
14722 cx.executor().run_until_parked();
14723 cx.update_editor(|editor, _, _| {
14724 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14725 {
14726 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14727 } else {
14728 panic!("expected completion menu to be open");
14729 }
14730 });
14731}
14732
14733fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14734 let entries = menu.entries.borrow();
14735 entries.iter().map(|mat| mat.string.clone()).collect()
14736}
14737
14738#[gpui::test]
14739async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14740 init_test(cx, |settings| {
14741 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14742 FormatterList(vec![Formatter::Prettier].into()),
14743 ))
14744 });
14745
14746 let fs = FakeFs::new(cx.executor());
14747 fs.insert_file(path!("/file.ts"), Default::default()).await;
14748
14749 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14750 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14751
14752 language_registry.add(Arc::new(Language::new(
14753 LanguageConfig {
14754 name: "TypeScript".into(),
14755 matcher: LanguageMatcher {
14756 path_suffixes: vec!["ts".to_string()],
14757 ..Default::default()
14758 },
14759 ..Default::default()
14760 },
14761 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14762 )));
14763 update_test_language_settings(cx, |settings| {
14764 settings.defaults.prettier = Some(PrettierSettings {
14765 allowed: true,
14766 ..PrettierSettings::default()
14767 });
14768 });
14769
14770 let test_plugin = "test_plugin";
14771 let _ = language_registry.register_fake_lsp(
14772 "TypeScript",
14773 FakeLspAdapter {
14774 prettier_plugins: vec![test_plugin],
14775 ..Default::default()
14776 },
14777 );
14778
14779 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14780 let buffer = project
14781 .update(cx, |project, cx| {
14782 project.open_local_buffer(path!("/file.ts"), cx)
14783 })
14784 .await
14785 .unwrap();
14786
14787 let buffer_text = "one\ntwo\nthree\n";
14788 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14789 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14790 editor.update_in(cx, |editor, window, cx| {
14791 editor.set_text(buffer_text, window, cx)
14792 });
14793
14794 editor
14795 .update_in(cx, |editor, window, cx| {
14796 editor.perform_format(
14797 project.clone(),
14798 FormatTrigger::Manual,
14799 FormatTarget::Buffers,
14800 window,
14801 cx,
14802 )
14803 })
14804 .unwrap()
14805 .await;
14806 assert_eq!(
14807 editor.update(cx, |editor, cx| editor.text(cx)),
14808 buffer_text.to_string() + prettier_format_suffix,
14809 "Test prettier formatting was not applied to the original buffer text",
14810 );
14811
14812 update_test_language_settings(cx, |settings| {
14813 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14814 });
14815 let format = editor.update_in(cx, |editor, window, cx| {
14816 editor.perform_format(
14817 project.clone(),
14818 FormatTrigger::Manual,
14819 FormatTarget::Buffers,
14820 window,
14821 cx,
14822 )
14823 });
14824 format.await.unwrap();
14825 assert_eq!(
14826 editor.update(cx, |editor, cx| editor.text(cx)),
14827 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14828 "Autoformatting (via test prettier) was not applied to the original buffer text",
14829 );
14830}
14831
14832#[gpui::test]
14833async fn test_addition_reverts(cx: &mut TestAppContext) {
14834 init_test(cx, |_| {});
14835 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14836 let base_text = indoc! {r#"
14837 struct Row;
14838 struct Row1;
14839 struct Row2;
14840
14841 struct Row4;
14842 struct Row5;
14843 struct Row6;
14844
14845 struct Row8;
14846 struct Row9;
14847 struct Row10;"#};
14848
14849 // When addition hunks are not adjacent to carets, no hunk revert is performed
14850 assert_hunk_revert(
14851 indoc! {r#"struct Row;
14852 struct Row1;
14853 struct Row1.1;
14854 struct Row1.2;
14855 struct Row2;ˇ
14856
14857 struct Row4;
14858 struct Row5;
14859 struct Row6;
14860
14861 struct Row8;
14862 ˇstruct Row9;
14863 struct Row9.1;
14864 struct Row9.2;
14865 struct Row9.3;
14866 struct Row10;"#},
14867 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
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 base_text,
14885 &mut cx,
14886 );
14887 // Same for selections
14888 assert_hunk_revert(
14889 indoc! {r#"struct Row;
14890 struct Row1;
14891 struct Row2;
14892 struct Row2.1;
14893 struct Row2.2;
14894 «ˇ
14895 struct Row4;
14896 struct» Row5;
14897 «struct Row6;
14898 ˇ»
14899 struct Row9.1;
14900 struct Row9.2;
14901 struct Row9.3;
14902 struct Row8;
14903 struct Row9;
14904 struct Row10;"#},
14905 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
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 base_text,
14923 &mut cx,
14924 );
14925
14926 // When carets and selections intersect the addition hunks, those are reverted.
14927 // Adjacent carets got merged.
14928 assert_hunk_revert(
14929 indoc! {r#"struct Row;
14930 ˇ// something on the top
14931 struct Row1;
14932 struct Row2;
14933 struct Roˇw3.1;
14934 struct Row2.2;
14935 struct Row2.3;ˇ
14936
14937 struct Row4;
14938 struct ˇRow5.1;
14939 struct Row5.2;
14940 struct «Rowˇ»5.3;
14941 struct Row5;
14942 struct Row6;
14943 ˇ
14944 struct Row9.1;
14945 struct «Rowˇ»9.2;
14946 struct «ˇRow»9.3;
14947 struct Row8;
14948 struct Row9;
14949 «ˇ// something on bottom»
14950 struct Row10;"#},
14951 vec![
14952 DiffHunkStatusKind::Added,
14953 DiffHunkStatusKind::Added,
14954 DiffHunkStatusKind::Added,
14955 DiffHunkStatusKind::Added,
14956 DiffHunkStatusKind::Added,
14957 ],
14958 indoc! {r#"struct Row;
14959 ˇstruct Row1;
14960 struct Row2;
14961 ˇ
14962 struct Row4;
14963 ˇstruct Row5;
14964 struct Row6;
14965 ˇ
14966 ˇstruct Row8;
14967 struct Row9;
14968 ˇstruct Row10;"#},
14969 base_text,
14970 &mut cx,
14971 );
14972}
14973
14974#[gpui::test]
14975async fn test_modification_reverts(cx: &mut TestAppContext) {
14976 init_test(cx, |_| {});
14977 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14978 let base_text = indoc! {r#"
14979 struct Row;
14980 struct Row1;
14981 struct Row2;
14982
14983 struct Row4;
14984 struct Row5;
14985 struct Row6;
14986
14987 struct Row8;
14988 struct Row9;
14989 struct Row10;"#};
14990
14991 // Modification hunks behave the same as the addition ones.
14992 assert_hunk_revert(
14993 indoc! {r#"struct Row;
14994 struct Row1;
14995 struct Row33;
14996 ˇ
14997 struct Row4;
14998 struct Row5;
14999 struct Row6;
15000 ˇ
15001 struct Row99;
15002 struct Row9;
15003 struct Row10;"#},
15004 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15005 indoc! {r#"struct Row;
15006 struct Row1;
15007 struct Row33;
15008 ˇ
15009 struct Row4;
15010 struct Row5;
15011 struct Row6;
15012 ˇ
15013 struct Row99;
15014 struct Row9;
15015 struct Row10;"#},
15016 base_text,
15017 &mut cx,
15018 );
15019 assert_hunk_revert(
15020 indoc! {r#"struct Row;
15021 struct Row1;
15022 struct Row33;
15023 «ˇ
15024 struct Row4;
15025 struct» Row5;
15026 «struct Row6;
15027 ˇ»
15028 struct Row99;
15029 struct Row9;
15030 struct Row10;"#},
15031 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15032 indoc! {r#"struct Row;
15033 struct Row1;
15034 struct Row33;
15035 «ˇ
15036 struct Row4;
15037 struct» Row5;
15038 «struct Row6;
15039 ˇ»
15040 struct Row99;
15041 struct Row9;
15042 struct Row10;"#},
15043 base_text,
15044 &mut cx,
15045 );
15046
15047 assert_hunk_revert(
15048 indoc! {r#"ˇstruct Row1.1;
15049 struct Row1;
15050 «ˇstr»uct Row22;
15051
15052 struct ˇRow44;
15053 struct Row5;
15054 struct «Rˇ»ow66;ˇ
15055
15056 «struˇ»ct Row88;
15057 struct Row9;
15058 struct Row1011;ˇ"#},
15059 vec![
15060 DiffHunkStatusKind::Modified,
15061 DiffHunkStatusKind::Modified,
15062 DiffHunkStatusKind::Modified,
15063 DiffHunkStatusKind::Modified,
15064 DiffHunkStatusKind::Modified,
15065 DiffHunkStatusKind::Modified,
15066 ],
15067 indoc! {r#"struct Row;
15068 ˇstruct Row1;
15069 struct Row2;
15070 ˇ
15071 struct Row4;
15072 ˇstruct Row5;
15073 struct Row6;
15074 ˇ
15075 struct Row8;
15076 ˇstruct Row9;
15077 struct Row10;ˇ"#},
15078 base_text,
15079 &mut cx,
15080 );
15081}
15082
15083#[gpui::test]
15084async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15085 init_test(cx, |_| {});
15086 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15087 let base_text = indoc! {r#"
15088 one
15089
15090 two
15091 three
15092 "#};
15093
15094 cx.set_head_text(base_text);
15095 cx.set_state("\nˇ\n");
15096 cx.executor().run_until_parked();
15097 cx.update_editor(|editor, _window, cx| {
15098 editor.expand_selected_diff_hunks(cx);
15099 });
15100 cx.executor().run_until_parked();
15101 cx.update_editor(|editor, window, cx| {
15102 editor.backspace(&Default::default(), window, cx);
15103 });
15104 cx.run_until_parked();
15105 cx.assert_state_with_diff(
15106 indoc! {r#"
15107
15108 - two
15109 - threeˇ
15110 +
15111 "#}
15112 .to_string(),
15113 );
15114}
15115
15116#[gpui::test]
15117async fn test_deletion_reverts(cx: &mut TestAppContext) {
15118 init_test(cx, |_| {});
15119 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15120 let base_text = indoc! {r#"struct Row;
15121struct Row1;
15122struct Row2;
15123
15124struct Row4;
15125struct Row5;
15126struct Row6;
15127
15128struct Row8;
15129struct Row9;
15130struct Row10;"#};
15131
15132 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15133 assert_hunk_revert(
15134 indoc! {r#"struct Row;
15135 struct Row2;
15136
15137 ˇstruct Row4;
15138 struct Row5;
15139 struct Row6;
15140 ˇ
15141 struct Row8;
15142 struct Row10;"#},
15143 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15144 indoc! {r#"struct Row;
15145 struct Row2;
15146
15147 ˇstruct Row4;
15148 struct Row5;
15149 struct Row6;
15150 ˇ
15151 struct Row8;
15152 struct Row10;"#},
15153 base_text,
15154 &mut cx,
15155 );
15156 assert_hunk_revert(
15157 indoc! {r#"struct Row;
15158 struct Row2;
15159
15160 «ˇstruct Row4;
15161 struct» Row5;
15162 «struct Row6;
15163 ˇ»
15164 struct Row8;
15165 struct Row10;"#},
15166 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15167 indoc! {r#"struct Row;
15168 struct Row2;
15169
15170 «ˇstruct Row4;
15171 struct» Row5;
15172 «struct Row6;
15173 ˇ»
15174 struct Row8;
15175 struct Row10;"#},
15176 base_text,
15177 &mut cx,
15178 );
15179
15180 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15181 assert_hunk_revert(
15182 indoc! {r#"struct Row;
15183 ˇstruct Row2;
15184
15185 struct Row4;
15186 struct Row5;
15187 struct Row6;
15188
15189 struct Row8;ˇ
15190 struct Row10;"#},
15191 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15192 indoc! {r#"struct Row;
15193 struct Row1;
15194 ˇstruct Row2;
15195
15196 struct Row4;
15197 struct Row5;
15198 struct Row6;
15199
15200 struct Row8;ˇ
15201 struct Row9;
15202 struct Row10;"#},
15203 base_text,
15204 &mut cx,
15205 );
15206 assert_hunk_revert(
15207 indoc! {r#"struct Row;
15208 struct Row2«ˇ;
15209 struct Row4;
15210 struct» Row5;
15211 «struct Row6;
15212
15213 struct Row8;ˇ»
15214 struct Row10;"#},
15215 vec![
15216 DiffHunkStatusKind::Deleted,
15217 DiffHunkStatusKind::Deleted,
15218 DiffHunkStatusKind::Deleted,
15219 ],
15220 indoc! {r#"struct Row;
15221 struct Row1;
15222 struct Row2«ˇ;
15223
15224 struct Row4;
15225 struct» Row5;
15226 «struct Row6;
15227
15228 struct Row8;ˇ»
15229 struct Row9;
15230 struct Row10;"#},
15231 base_text,
15232 &mut cx,
15233 );
15234}
15235
15236#[gpui::test]
15237async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15238 init_test(cx, |_| {});
15239
15240 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15241 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15242 let base_text_3 =
15243 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15244
15245 let text_1 = edit_first_char_of_every_line(base_text_1);
15246 let text_2 = edit_first_char_of_every_line(base_text_2);
15247 let text_3 = edit_first_char_of_every_line(base_text_3);
15248
15249 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15250 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15251 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15252
15253 let multibuffer = cx.new(|cx| {
15254 let mut multibuffer = MultiBuffer::new(ReadWrite);
15255 multibuffer.push_excerpts(
15256 buffer_1.clone(),
15257 [
15258 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15259 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15260 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15261 ],
15262 cx,
15263 );
15264 multibuffer.push_excerpts(
15265 buffer_2.clone(),
15266 [
15267 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15268 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15269 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15270 ],
15271 cx,
15272 );
15273 multibuffer.push_excerpts(
15274 buffer_3.clone(),
15275 [
15276 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15277 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15278 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15279 ],
15280 cx,
15281 );
15282 multibuffer
15283 });
15284
15285 let fs = FakeFs::new(cx.executor());
15286 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15287 let (editor, cx) = cx
15288 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15289 editor.update_in(cx, |editor, _window, cx| {
15290 for (buffer, diff_base) in [
15291 (buffer_1.clone(), base_text_1),
15292 (buffer_2.clone(), base_text_2),
15293 (buffer_3.clone(), base_text_3),
15294 ] {
15295 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15296 editor
15297 .buffer
15298 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15299 }
15300 });
15301 cx.executor().run_until_parked();
15302
15303 editor.update_in(cx, |editor, window, cx| {
15304 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}");
15305 editor.select_all(&SelectAll, window, cx);
15306 editor.git_restore(&Default::default(), window, cx);
15307 });
15308 cx.executor().run_until_parked();
15309
15310 // When all ranges are selected, all buffer hunks are reverted.
15311 editor.update(cx, |editor, cx| {
15312 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");
15313 });
15314 buffer_1.update(cx, |buffer, _| {
15315 assert_eq!(buffer.text(), base_text_1);
15316 });
15317 buffer_2.update(cx, |buffer, _| {
15318 assert_eq!(buffer.text(), base_text_2);
15319 });
15320 buffer_3.update(cx, |buffer, _| {
15321 assert_eq!(buffer.text(), base_text_3);
15322 });
15323
15324 editor.update_in(cx, |editor, window, cx| {
15325 editor.undo(&Default::default(), window, cx);
15326 });
15327
15328 editor.update_in(cx, |editor, window, cx| {
15329 editor.change_selections(None, window, cx, |s| {
15330 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15331 });
15332 editor.git_restore(&Default::default(), window, cx);
15333 });
15334
15335 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15336 // but not affect buffer_2 and its related excerpts.
15337 editor.update(cx, |editor, cx| {
15338 assert_eq!(
15339 editor.text(cx),
15340 "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}"
15341 );
15342 });
15343 buffer_1.update(cx, |buffer, _| {
15344 assert_eq!(buffer.text(), base_text_1);
15345 });
15346 buffer_2.update(cx, |buffer, _| {
15347 assert_eq!(
15348 buffer.text(),
15349 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15350 );
15351 });
15352 buffer_3.update(cx, |buffer, _| {
15353 assert_eq!(
15354 buffer.text(),
15355 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15356 );
15357 });
15358
15359 fn edit_first_char_of_every_line(text: &str) -> String {
15360 text.split('\n')
15361 .map(|line| format!("X{}", &line[1..]))
15362 .collect::<Vec<_>>()
15363 .join("\n")
15364 }
15365}
15366
15367#[gpui::test]
15368async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15369 init_test(cx, |_| {});
15370
15371 let cols = 4;
15372 let rows = 10;
15373 let sample_text_1 = sample_text(rows, cols, 'a');
15374 assert_eq!(
15375 sample_text_1,
15376 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15377 );
15378 let sample_text_2 = sample_text(rows, cols, 'l');
15379 assert_eq!(
15380 sample_text_2,
15381 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15382 );
15383 let sample_text_3 = sample_text(rows, cols, 'v');
15384 assert_eq!(
15385 sample_text_3,
15386 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15387 );
15388
15389 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15390 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15391 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15392
15393 let multi_buffer = cx.new(|cx| {
15394 let mut multibuffer = MultiBuffer::new(ReadWrite);
15395 multibuffer.push_excerpts(
15396 buffer_1.clone(),
15397 [
15398 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15399 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15400 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15401 ],
15402 cx,
15403 );
15404 multibuffer.push_excerpts(
15405 buffer_2.clone(),
15406 [
15407 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15408 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15409 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15410 ],
15411 cx,
15412 );
15413 multibuffer.push_excerpts(
15414 buffer_3.clone(),
15415 [
15416 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15417 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15418 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15419 ],
15420 cx,
15421 );
15422 multibuffer
15423 });
15424
15425 let fs = FakeFs::new(cx.executor());
15426 fs.insert_tree(
15427 "/a",
15428 json!({
15429 "main.rs": sample_text_1,
15430 "other.rs": sample_text_2,
15431 "lib.rs": sample_text_3,
15432 }),
15433 )
15434 .await;
15435 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15436 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15437 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15438 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15439 Editor::new(
15440 EditorMode::full(),
15441 multi_buffer,
15442 Some(project.clone()),
15443 window,
15444 cx,
15445 )
15446 });
15447 let multibuffer_item_id = workspace
15448 .update(cx, |workspace, window, cx| {
15449 assert!(
15450 workspace.active_item(cx).is_none(),
15451 "active item should be None before the first item is added"
15452 );
15453 workspace.add_item_to_active_pane(
15454 Box::new(multi_buffer_editor.clone()),
15455 None,
15456 true,
15457 window,
15458 cx,
15459 );
15460 let active_item = workspace
15461 .active_item(cx)
15462 .expect("should have an active item after adding the multi buffer");
15463 assert!(
15464 !active_item.is_singleton(cx),
15465 "A multi buffer was expected to active after adding"
15466 );
15467 active_item.item_id()
15468 })
15469 .unwrap();
15470 cx.executor().run_until_parked();
15471
15472 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15473 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15474 s.select_ranges(Some(1..2))
15475 });
15476 editor.open_excerpts(&OpenExcerpts, window, cx);
15477 });
15478 cx.executor().run_until_parked();
15479 let first_item_id = workspace
15480 .update(cx, |workspace, window, cx| {
15481 let active_item = workspace
15482 .active_item(cx)
15483 .expect("should have an active item after navigating into the 1st buffer");
15484 let first_item_id = active_item.item_id();
15485 assert_ne!(
15486 first_item_id, multibuffer_item_id,
15487 "Should navigate into the 1st buffer and activate it"
15488 );
15489 assert!(
15490 active_item.is_singleton(cx),
15491 "New active item should be a singleton buffer"
15492 );
15493 assert_eq!(
15494 active_item
15495 .act_as::<Editor>(cx)
15496 .expect("should have navigated into an editor for the 1st buffer")
15497 .read(cx)
15498 .text(cx),
15499 sample_text_1
15500 );
15501
15502 workspace
15503 .go_back(workspace.active_pane().downgrade(), window, cx)
15504 .detach_and_log_err(cx);
15505
15506 first_item_id
15507 })
15508 .unwrap();
15509 cx.executor().run_until_parked();
15510 workspace
15511 .update(cx, |workspace, _, cx| {
15512 let active_item = workspace
15513 .active_item(cx)
15514 .expect("should have an active item after navigating back");
15515 assert_eq!(
15516 active_item.item_id(),
15517 multibuffer_item_id,
15518 "Should navigate back to the multi buffer"
15519 );
15520 assert!(!active_item.is_singleton(cx));
15521 })
15522 .unwrap();
15523
15524 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15525 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15526 s.select_ranges(Some(39..40))
15527 });
15528 editor.open_excerpts(&OpenExcerpts, window, cx);
15529 });
15530 cx.executor().run_until_parked();
15531 let second_item_id = workspace
15532 .update(cx, |workspace, window, cx| {
15533 let active_item = workspace
15534 .active_item(cx)
15535 .expect("should have an active item after navigating into the 2nd buffer");
15536 let second_item_id = active_item.item_id();
15537 assert_ne!(
15538 second_item_id, multibuffer_item_id,
15539 "Should navigate away from the multibuffer"
15540 );
15541 assert_ne!(
15542 second_item_id, first_item_id,
15543 "Should navigate into the 2nd buffer and activate it"
15544 );
15545 assert!(
15546 active_item.is_singleton(cx),
15547 "New active item should be a singleton buffer"
15548 );
15549 assert_eq!(
15550 active_item
15551 .act_as::<Editor>(cx)
15552 .expect("should have navigated into an editor")
15553 .read(cx)
15554 .text(cx),
15555 sample_text_2
15556 );
15557
15558 workspace
15559 .go_back(workspace.active_pane().downgrade(), window, cx)
15560 .detach_and_log_err(cx);
15561
15562 second_item_id
15563 })
15564 .unwrap();
15565 cx.executor().run_until_parked();
15566 workspace
15567 .update(cx, |workspace, _, cx| {
15568 let active_item = workspace
15569 .active_item(cx)
15570 .expect("should have an active item after navigating back from the 2nd buffer");
15571 assert_eq!(
15572 active_item.item_id(),
15573 multibuffer_item_id,
15574 "Should navigate back from the 2nd buffer to the multi buffer"
15575 );
15576 assert!(!active_item.is_singleton(cx));
15577 })
15578 .unwrap();
15579
15580 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15581 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15582 s.select_ranges(Some(70..70))
15583 });
15584 editor.open_excerpts(&OpenExcerpts, window, cx);
15585 });
15586 cx.executor().run_until_parked();
15587 workspace
15588 .update(cx, |workspace, window, cx| {
15589 let active_item = workspace
15590 .active_item(cx)
15591 .expect("should have an active item after navigating into the 3rd buffer");
15592 let third_item_id = active_item.item_id();
15593 assert_ne!(
15594 third_item_id, multibuffer_item_id,
15595 "Should navigate into the 3rd buffer and activate it"
15596 );
15597 assert_ne!(third_item_id, first_item_id);
15598 assert_ne!(third_item_id, second_item_id);
15599 assert!(
15600 active_item.is_singleton(cx),
15601 "New active item should be a singleton buffer"
15602 );
15603 assert_eq!(
15604 active_item
15605 .act_as::<Editor>(cx)
15606 .expect("should have navigated into an editor")
15607 .read(cx)
15608 .text(cx),
15609 sample_text_3
15610 );
15611
15612 workspace
15613 .go_back(workspace.active_pane().downgrade(), window, cx)
15614 .detach_and_log_err(cx);
15615 })
15616 .unwrap();
15617 cx.executor().run_until_parked();
15618 workspace
15619 .update(cx, |workspace, _, cx| {
15620 let active_item = workspace
15621 .active_item(cx)
15622 .expect("should have an active item after navigating back from the 3rd buffer");
15623 assert_eq!(
15624 active_item.item_id(),
15625 multibuffer_item_id,
15626 "Should navigate back from the 3rd buffer to the multi buffer"
15627 );
15628 assert!(!active_item.is_singleton(cx));
15629 })
15630 .unwrap();
15631}
15632
15633#[gpui::test]
15634async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15635 init_test(cx, |_| {});
15636
15637 let mut cx = EditorTestContext::new(cx).await;
15638
15639 let diff_base = r#"
15640 use some::mod;
15641
15642 const A: u32 = 42;
15643
15644 fn main() {
15645 println!("hello");
15646
15647 println!("world");
15648 }
15649 "#
15650 .unindent();
15651
15652 cx.set_state(
15653 &r#"
15654 use some::modified;
15655
15656 ˇ
15657 fn main() {
15658 println!("hello there");
15659
15660 println!("around the");
15661 println!("world");
15662 }
15663 "#
15664 .unindent(),
15665 );
15666
15667 cx.set_head_text(&diff_base);
15668 executor.run_until_parked();
15669
15670 cx.update_editor(|editor, window, cx| {
15671 editor.go_to_next_hunk(&GoToHunk, window, cx);
15672 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15673 });
15674 executor.run_until_parked();
15675 cx.assert_state_with_diff(
15676 r#"
15677 use some::modified;
15678
15679
15680 fn main() {
15681 - println!("hello");
15682 + ˇ println!("hello there");
15683
15684 println!("around the");
15685 println!("world");
15686 }
15687 "#
15688 .unindent(),
15689 );
15690
15691 cx.update_editor(|editor, window, cx| {
15692 for _ in 0..2 {
15693 editor.go_to_next_hunk(&GoToHunk, window, cx);
15694 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15695 }
15696 });
15697 executor.run_until_parked();
15698 cx.assert_state_with_diff(
15699 r#"
15700 - use some::mod;
15701 + ˇuse some::modified;
15702
15703
15704 fn main() {
15705 - println!("hello");
15706 + println!("hello there");
15707
15708 + println!("around the");
15709 println!("world");
15710 }
15711 "#
15712 .unindent(),
15713 );
15714
15715 cx.update_editor(|editor, window, cx| {
15716 editor.go_to_next_hunk(&GoToHunk, window, cx);
15717 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15718 });
15719 executor.run_until_parked();
15720 cx.assert_state_with_diff(
15721 r#"
15722 - use some::mod;
15723 + use some::modified;
15724
15725 - const A: u32 = 42;
15726 ˇ
15727 fn main() {
15728 - println!("hello");
15729 + println!("hello there");
15730
15731 + println!("around the");
15732 println!("world");
15733 }
15734 "#
15735 .unindent(),
15736 );
15737
15738 cx.update_editor(|editor, window, cx| {
15739 editor.cancel(&Cancel, window, cx);
15740 });
15741
15742 cx.assert_state_with_diff(
15743 r#"
15744 use some::modified;
15745
15746 ˇ
15747 fn main() {
15748 println!("hello there");
15749
15750 println!("around the");
15751 println!("world");
15752 }
15753 "#
15754 .unindent(),
15755 );
15756}
15757
15758#[gpui::test]
15759async fn test_diff_base_change_with_expanded_diff_hunks(
15760 executor: BackgroundExecutor,
15761 cx: &mut TestAppContext,
15762) {
15763 init_test(cx, |_| {});
15764
15765 let mut cx = EditorTestContext::new(cx).await;
15766
15767 let diff_base = r#"
15768 use some::mod1;
15769 use some::mod2;
15770
15771 const A: u32 = 42;
15772 const B: u32 = 42;
15773 const C: u32 = 42;
15774
15775 fn main() {
15776 println!("hello");
15777
15778 println!("world");
15779 }
15780 "#
15781 .unindent();
15782
15783 cx.set_state(
15784 &r#"
15785 use some::mod2;
15786
15787 const A: u32 = 42;
15788 const C: u32 = 42;
15789
15790 fn main(ˇ) {
15791 //println!("hello");
15792
15793 println!("world");
15794 //
15795 //
15796 }
15797 "#
15798 .unindent(),
15799 );
15800
15801 cx.set_head_text(&diff_base);
15802 executor.run_until_parked();
15803
15804 cx.update_editor(|editor, window, cx| {
15805 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15806 });
15807 executor.run_until_parked();
15808 cx.assert_state_with_diff(
15809 r#"
15810 - use some::mod1;
15811 use some::mod2;
15812
15813 const A: u32 = 42;
15814 - const B: u32 = 42;
15815 const C: u32 = 42;
15816
15817 fn main(ˇ) {
15818 - println!("hello");
15819 + //println!("hello");
15820
15821 println!("world");
15822 + //
15823 + //
15824 }
15825 "#
15826 .unindent(),
15827 );
15828
15829 cx.set_head_text("new diff base!");
15830 executor.run_until_parked();
15831 cx.assert_state_with_diff(
15832 r#"
15833 - new diff base!
15834 + use some::mod2;
15835 +
15836 + const A: u32 = 42;
15837 + const C: u32 = 42;
15838 +
15839 + fn main(ˇ) {
15840 + //println!("hello");
15841 +
15842 + println!("world");
15843 + //
15844 + //
15845 + }
15846 "#
15847 .unindent(),
15848 );
15849}
15850
15851#[gpui::test]
15852async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15853 init_test(cx, |_| {});
15854
15855 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15856 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15857 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15858 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15859 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15860 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15861
15862 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15863 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15864 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15865
15866 let multi_buffer = cx.new(|cx| {
15867 let mut multibuffer = MultiBuffer::new(ReadWrite);
15868 multibuffer.push_excerpts(
15869 buffer_1.clone(),
15870 [
15871 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15872 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15873 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15874 ],
15875 cx,
15876 );
15877 multibuffer.push_excerpts(
15878 buffer_2.clone(),
15879 [
15880 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15881 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15882 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15883 ],
15884 cx,
15885 );
15886 multibuffer.push_excerpts(
15887 buffer_3.clone(),
15888 [
15889 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15890 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15891 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15892 ],
15893 cx,
15894 );
15895 multibuffer
15896 });
15897
15898 let editor =
15899 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15900 editor
15901 .update(cx, |editor, _window, cx| {
15902 for (buffer, diff_base) in [
15903 (buffer_1.clone(), file_1_old),
15904 (buffer_2.clone(), file_2_old),
15905 (buffer_3.clone(), file_3_old),
15906 ] {
15907 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15908 editor
15909 .buffer
15910 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15911 }
15912 })
15913 .unwrap();
15914
15915 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15916 cx.run_until_parked();
15917
15918 cx.assert_editor_state(
15919 &"
15920 ˇaaa
15921 ccc
15922 ddd
15923
15924 ggg
15925 hhh
15926
15927
15928 lll
15929 mmm
15930 NNN
15931
15932 qqq
15933 rrr
15934
15935 uuu
15936 111
15937 222
15938 333
15939
15940 666
15941 777
15942
15943 000
15944 !!!"
15945 .unindent(),
15946 );
15947
15948 cx.update_editor(|editor, window, cx| {
15949 editor.select_all(&SelectAll, window, cx);
15950 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15951 });
15952 cx.executor().run_until_parked();
15953
15954 cx.assert_state_with_diff(
15955 "
15956 «aaa
15957 - bbb
15958 ccc
15959 ddd
15960
15961 ggg
15962 hhh
15963
15964
15965 lll
15966 mmm
15967 - nnn
15968 + NNN
15969
15970 qqq
15971 rrr
15972
15973 uuu
15974 111
15975 222
15976 333
15977
15978 + 666
15979 777
15980
15981 000
15982 !!!ˇ»"
15983 .unindent(),
15984 );
15985}
15986
15987#[gpui::test]
15988async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15989 init_test(cx, |_| {});
15990
15991 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15992 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15993
15994 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15995 let multi_buffer = cx.new(|cx| {
15996 let mut multibuffer = MultiBuffer::new(ReadWrite);
15997 multibuffer.push_excerpts(
15998 buffer.clone(),
15999 [
16000 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16001 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16002 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16003 ],
16004 cx,
16005 );
16006 multibuffer
16007 });
16008
16009 let editor =
16010 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16011 editor
16012 .update(cx, |editor, _window, cx| {
16013 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16014 editor
16015 .buffer
16016 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16017 })
16018 .unwrap();
16019
16020 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16021 cx.run_until_parked();
16022
16023 cx.update_editor(|editor, window, cx| {
16024 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16025 });
16026 cx.executor().run_until_parked();
16027
16028 // When the start of a hunk coincides with the start of its excerpt,
16029 // the hunk is expanded. When the start of a a hunk is earlier than
16030 // the start of its excerpt, the hunk is not expanded.
16031 cx.assert_state_with_diff(
16032 "
16033 ˇaaa
16034 - bbb
16035 + BBB
16036
16037 - ddd
16038 - eee
16039 + DDD
16040 + EEE
16041 fff
16042
16043 iii
16044 "
16045 .unindent(),
16046 );
16047}
16048
16049#[gpui::test]
16050async fn test_edits_around_expanded_insertion_hunks(
16051 executor: BackgroundExecutor,
16052 cx: &mut TestAppContext,
16053) {
16054 init_test(cx, |_| {});
16055
16056 let mut cx = EditorTestContext::new(cx).await;
16057
16058 let diff_base = r#"
16059 use some::mod1;
16060 use some::mod2;
16061
16062 const A: u32 = 42;
16063
16064 fn main() {
16065 println!("hello");
16066
16067 println!("world");
16068 }
16069 "#
16070 .unindent();
16071 executor.run_until_parked();
16072 cx.set_state(
16073 &r#"
16074 use some::mod1;
16075 use some::mod2;
16076
16077 const A: u32 = 42;
16078 const B: u32 = 42;
16079 const C: u32 = 42;
16080 ˇ
16081
16082 fn main() {
16083 println!("hello");
16084
16085 println!("world");
16086 }
16087 "#
16088 .unindent(),
16089 );
16090
16091 cx.set_head_text(&diff_base);
16092 executor.run_until_parked();
16093
16094 cx.update_editor(|editor, window, cx| {
16095 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16096 });
16097 executor.run_until_parked();
16098
16099 cx.assert_state_with_diff(
16100 r#"
16101 use some::mod1;
16102 use some::mod2;
16103
16104 const A: u32 = 42;
16105 + const B: u32 = 42;
16106 + const C: u32 = 42;
16107 + ˇ
16108
16109 fn main() {
16110 println!("hello");
16111
16112 println!("world");
16113 }
16114 "#
16115 .unindent(),
16116 );
16117
16118 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16119 executor.run_until_parked();
16120
16121 cx.assert_state_with_diff(
16122 r#"
16123 use some::mod1;
16124 use some::mod2;
16125
16126 const A: u32 = 42;
16127 + const B: u32 = 42;
16128 + const C: u32 = 42;
16129 + const D: u32 = 42;
16130 + ˇ
16131
16132 fn main() {
16133 println!("hello");
16134
16135 println!("world");
16136 }
16137 "#
16138 .unindent(),
16139 );
16140
16141 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16142 executor.run_until_parked();
16143
16144 cx.assert_state_with_diff(
16145 r#"
16146 use some::mod1;
16147 use some::mod2;
16148
16149 const A: u32 = 42;
16150 + const B: u32 = 42;
16151 + const C: u32 = 42;
16152 + const D: u32 = 42;
16153 + const E: u32 = 42;
16154 + ˇ
16155
16156 fn main() {
16157 println!("hello");
16158
16159 println!("world");
16160 }
16161 "#
16162 .unindent(),
16163 );
16164
16165 cx.update_editor(|editor, window, cx| {
16166 editor.delete_line(&DeleteLine, window, cx);
16167 });
16168 executor.run_until_parked();
16169
16170 cx.assert_state_with_diff(
16171 r#"
16172 use some::mod1;
16173 use some::mod2;
16174
16175 const A: u32 = 42;
16176 + const B: u32 = 42;
16177 + const C: u32 = 42;
16178 + const D: u32 = 42;
16179 + const E: u32 = 42;
16180 ˇ
16181 fn main() {
16182 println!("hello");
16183
16184 println!("world");
16185 }
16186 "#
16187 .unindent(),
16188 );
16189
16190 cx.update_editor(|editor, window, cx| {
16191 editor.move_up(&MoveUp, window, cx);
16192 editor.delete_line(&DeleteLine, window, cx);
16193 editor.move_up(&MoveUp, window, cx);
16194 editor.delete_line(&DeleteLine, window, cx);
16195 editor.move_up(&MoveUp, window, cx);
16196 editor.delete_line(&DeleteLine, window, cx);
16197 });
16198 executor.run_until_parked();
16199 cx.assert_state_with_diff(
16200 r#"
16201 use some::mod1;
16202 use some::mod2;
16203
16204 const A: u32 = 42;
16205 + const B: u32 = 42;
16206 ˇ
16207 fn main() {
16208 println!("hello");
16209
16210 println!("world");
16211 }
16212 "#
16213 .unindent(),
16214 );
16215
16216 cx.update_editor(|editor, window, cx| {
16217 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16218 editor.delete_line(&DeleteLine, window, cx);
16219 });
16220 executor.run_until_parked();
16221 cx.assert_state_with_diff(
16222 r#"
16223 ˇ
16224 fn main() {
16225 println!("hello");
16226
16227 println!("world");
16228 }
16229 "#
16230 .unindent(),
16231 );
16232}
16233
16234#[gpui::test]
16235async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16236 init_test(cx, |_| {});
16237
16238 let mut cx = EditorTestContext::new(cx).await;
16239 cx.set_head_text(indoc! { "
16240 one
16241 two
16242 three
16243 four
16244 five
16245 "
16246 });
16247 cx.set_state(indoc! { "
16248 one
16249 ˇthree
16250 five
16251 "});
16252 cx.run_until_parked();
16253 cx.update_editor(|editor, window, cx| {
16254 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16255 });
16256 cx.assert_state_with_diff(
16257 indoc! { "
16258 one
16259 - two
16260 ˇthree
16261 - four
16262 five
16263 "}
16264 .to_string(),
16265 );
16266 cx.update_editor(|editor, window, cx| {
16267 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16268 });
16269
16270 cx.assert_state_with_diff(
16271 indoc! { "
16272 one
16273 ˇthree
16274 five
16275 "}
16276 .to_string(),
16277 );
16278
16279 cx.set_state(indoc! { "
16280 one
16281 ˇTWO
16282 three
16283 four
16284 five
16285 "});
16286 cx.run_until_parked();
16287 cx.update_editor(|editor, window, cx| {
16288 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16289 });
16290
16291 cx.assert_state_with_diff(
16292 indoc! { "
16293 one
16294 - two
16295 + ˇTWO
16296 three
16297 four
16298 five
16299 "}
16300 .to_string(),
16301 );
16302 cx.update_editor(|editor, window, cx| {
16303 editor.move_up(&Default::default(), window, cx);
16304 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16305 });
16306 cx.assert_state_with_diff(
16307 indoc! { "
16308 one
16309 ˇTWO
16310 three
16311 four
16312 five
16313 "}
16314 .to_string(),
16315 );
16316}
16317
16318#[gpui::test]
16319async fn test_edits_around_expanded_deletion_hunks(
16320 executor: BackgroundExecutor,
16321 cx: &mut TestAppContext,
16322) {
16323 init_test(cx, |_| {});
16324
16325 let mut cx = EditorTestContext::new(cx).await;
16326
16327 let diff_base = r#"
16328 use some::mod1;
16329 use some::mod2;
16330
16331 const A: u32 = 42;
16332 const B: u32 = 42;
16333 const C: u32 = 42;
16334
16335
16336 fn main() {
16337 println!("hello");
16338
16339 println!("world");
16340 }
16341 "#
16342 .unindent();
16343 executor.run_until_parked();
16344 cx.set_state(
16345 &r#"
16346 use some::mod1;
16347 use some::mod2;
16348
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 );
16361
16362 cx.set_head_text(&diff_base);
16363 executor.run_until_parked();
16364
16365 cx.update_editor(|editor, window, cx| {
16366 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16367 });
16368 executor.run_until_parked();
16369
16370 cx.assert_state_with_diff(
16371 r#"
16372 use some::mod1;
16373 use some::mod2;
16374
16375 - const A: u32 = 42;
16376 ˇconst B: u32 = 42;
16377 const C: u32 = 42;
16378
16379
16380 fn main() {
16381 println!("hello");
16382
16383 println!("world");
16384 }
16385 "#
16386 .unindent(),
16387 );
16388
16389 cx.update_editor(|editor, window, cx| {
16390 editor.delete_line(&DeleteLine, window, cx);
16391 });
16392 executor.run_until_parked();
16393 cx.assert_state_with_diff(
16394 r#"
16395 use some::mod1;
16396 use some::mod2;
16397
16398 - const A: u32 = 42;
16399 - const B: u32 = 42;
16400 ˇconst C: u32 = 42;
16401
16402
16403 fn main() {
16404 println!("hello");
16405
16406 println!("world");
16407 }
16408 "#
16409 .unindent(),
16410 );
16411
16412 cx.update_editor(|editor, window, cx| {
16413 editor.delete_line(&DeleteLine, window, cx);
16414 });
16415 executor.run_until_parked();
16416 cx.assert_state_with_diff(
16417 r#"
16418 use some::mod1;
16419 use some::mod2;
16420
16421 - const A: u32 = 42;
16422 - const B: u32 = 42;
16423 - const C: u32 = 42;
16424 ˇ
16425
16426 fn main() {
16427 println!("hello");
16428
16429 println!("world");
16430 }
16431 "#
16432 .unindent(),
16433 );
16434
16435 cx.update_editor(|editor, window, cx| {
16436 editor.handle_input("replacement", window, cx);
16437 });
16438 executor.run_until_parked();
16439 cx.assert_state_with_diff(
16440 r#"
16441 use some::mod1;
16442 use some::mod2;
16443
16444 - const A: u32 = 42;
16445 - const B: u32 = 42;
16446 - const C: u32 = 42;
16447 -
16448 + replacementˇ
16449
16450 fn main() {
16451 println!("hello");
16452
16453 println!("world");
16454 }
16455 "#
16456 .unindent(),
16457 );
16458}
16459
16460#[gpui::test]
16461async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16462 init_test(cx, |_| {});
16463
16464 let mut cx = EditorTestContext::new(cx).await;
16465
16466 let base_text = r#"
16467 one
16468 two
16469 three
16470 four
16471 five
16472 "#
16473 .unindent();
16474 executor.run_until_parked();
16475 cx.set_state(
16476 &r#"
16477 one
16478 two
16479 fˇour
16480 five
16481 "#
16482 .unindent(),
16483 );
16484
16485 cx.set_head_text(&base_text);
16486 executor.run_until_parked();
16487
16488 cx.update_editor(|editor, window, cx| {
16489 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16490 });
16491 executor.run_until_parked();
16492
16493 cx.assert_state_with_diff(
16494 r#"
16495 one
16496 two
16497 - three
16498 fˇour
16499 five
16500 "#
16501 .unindent(),
16502 );
16503
16504 cx.update_editor(|editor, window, cx| {
16505 editor.backspace(&Backspace, window, cx);
16506 editor.backspace(&Backspace, window, cx);
16507 });
16508 executor.run_until_parked();
16509 cx.assert_state_with_diff(
16510 r#"
16511 one
16512 two
16513 - threeˇ
16514 - four
16515 + our
16516 five
16517 "#
16518 .unindent(),
16519 );
16520}
16521
16522#[gpui::test]
16523async fn test_edit_after_expanded_modification_hunk(
16524 executor: BackgroundExecutor,
16525 cx: &mut TestAppContext,
16526) {
16527 init_test(cx, |_| {});
16528
16529 let mut cx = EditorTestContext::new(cx).await;
16530
16531 let diff_base = r#"
16532 use some::mod1;
16533 use some::mod2;
16534
16535 const A: u32 = 42;
16536 const B: u32 = 42;
16537 const C: u32 = 42;
16538 const D: u32 = 42;
16539
16540
16541 fn main() {
16542 println!("hello");
16543
16544 println!("world");
16545 }"#
16546 .unindent();
16547
16548 cx.set_state(
16549 &r#"
16550 use some::mod1;
16551 use some::mod2;
16552
16553 const A: u32 = 42;
16554 const B: u32 = 42;
16555 const C: u32 = 43ˇ
16556 const D: u32 = 42;
16557
16558
16559 fn main() {
16560 println!("hello");
16561
16562 println!("world");
16563 }"#
16564 .unindent(),
16565 );
16566
16567 cx.set_head_text(&diff_base);
16568 executor.run_until_parked();
16569 cx.update_editor(|editor, window, cx| {
16570 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16571 });
16572 executor.run_until_parked();
16573
16574 cx.assert_state_with_diff(
16575 r#"
16576 use some::mod1;
16577 use some::mod2;
16578
16579 const A: u32 = 42;
16580 const B: u32 = 42;
16581 - const C: u32 = 42;
16582 + const C: u32 = 43ˇ
16583 const D: u32 = 42;
16584
16585
16586 fn main() {
16587 println!("hello");
16588
16589 println!("world");
16590 }"#
16591 .unindent(),
16592 );
16593
16594 cx.update_editor(|editor, window, cx| {
16595 editor.handle_input("\nnew_line\n", window, cx);
16596 });
16597 executor.run_until_parked();
16598
16599 cx.assert_state_with_diff(
16600 r#"
16601 use some::mod1;
16602 use some::mod2;
16603
16604 const A: u32 = 42;
16605 const B: u32 = 42;
16606 - const C: u32 = 42;
16607 + const C: u32 = 43
16608 + new_line
16609 + ˇ
16610 const D: u32 = 42;
16611
16612
16613 fn main() {
16614 println!("hello");
16615
16616 println!("world");
16617 }"#
16618 .unindent(),
16619 );
16620}
16621
16622#[gpui::test]
16623async fn test_stage_and_unstage_added_file_hunk(
16624 executor: BackgroundExecutor,
16625 cx: &mut TestAppContext,
16626) {
16627 init_test(cx, |_| {});
16628
16629 let mut cx = EditorTestContext::new(cx).await;
16630 cx.update_editor(|editor, _, cx| {
16631 editor.set_expand_all_diff_hunks(cx);
16632 });
16633
16634 let working_copy = r#"
16635 ˇfn main() {
16636 println!("hello, world!");
16637 }
16638 "#
16639 .unindent();
16640
16641 cx.set_state(&working_copy);
16642 executor.run_until_parked();
16643
16644 cx.assert_state_with_diff(
16645 r#"
16646 + ˇfn main() {
16647 + println!("hello, world!");
16648 + }
16649 "#
16650 .unindent(),
16651 );
16652 cx.assert_index_text(None);
16653
16654 cx.update_editor(|editor, window, cx| {
16655 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16656 });
16657 executor.run_until_parked();
16658 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16659 cx.assert_state_with_diff(
16660 r#"
16661 + ˇfn main() {
16662 + println!("hello, world!");
16663 + }
16664 "#
16665 .unindent(),
16666 );
16667
16668 cx.update_editor(|editor, window, cx| {
16669 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16670 });
16671 executor.run_until_parked();
16672 cx.assert_index_text(None);
16673}
16674
16675async fn setup_indent_guides_editor(
16676 text: &str,
16677 cx: &mut TestAppContext,
16678) -> (BufferId, EditorTestContext) {
16679 init_test(cx, |_| {});
16680
16681 let mut cx = EditorTestContext::new(cx).await;
16682
16683 let buffer_id = cx.update_editor(|editor, window, cx| {
16684 editor.set_text(text, window, cx);
16685 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16686
16687 buffer_ids[0]
16688 });
16689
16690 (buffer_id, cx)
16691}
16692
16693fn assert_indent_guides(
16694 range: Range<u32>,
16695 expected: Vec<IndentGuide>,
16696 active_indices: Option<Vec<usize>>,
16697 cx: &mut EditorTestContext,
16698) {
16699 let indent_guides = cx.update_editor(|editor, window, cx| {
16700 let snapshot = editor.snapshot(window, cx).display_snapshot;
16701 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16702 editor,
16703 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16704 true,
16705 &snapshot,
16706 cx,
16707 );
16708
16709 indent_guides.sort_by(|a, b| {
16710 a.depth.cmp(&b.depth).then(
16711 a.start_row
16712 .cmp(&b.start_row)
16713 .then(a.end_row.cmp(&b.end_row)),
16714 )
16715 });
16716 indent_guides
16717 });
16718
16719 if let Some(expected) = active_indices {
16720 let active_indices = cx.update_editor(|editor, window, cx| {
16721 let snapshot = editor.snapshot(window, cx).display_snapshot;
16722 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16723 });
16724
16725 assert_eq!(
16726 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16727 expected,
16728 "Active indent guide indices do not match"
16729 );
16730 }
16731
16732 assert_eq!(indent_guides, expected, "Indent guides do not match");
16733}
16734
16735fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16736 IndentGuide {
16737 buffer_id,
16738 start_row: MultiBufferRow(start_row),
16739 end_row: MultiBufferRow(end_row),
16740 depth,
16741 tab_size: 4,
16742 settings: IndentGuideSettings {
16743 enabled: true,
16744 line_width: 1,
16745 active_line_width: 1,
16746 ..Default::default()
16747 },
16748 }
16749}
16750
16751#[gpui::test]
16752async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16753 let (buffer_id, mut cx) = setup_indent_guides_editor(
16754 &"
16755 fn main() {
16756 let a = 1;
16757 }"
16758 .unindent(),
16759 cx,
16760 )
16761 .await;
16762
16763 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16764}
16765
16766#[gpui::test]
16767async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16768 let (buffer_id, mut cx) = setup_indent_guides_editor(
16769 &"
16770 fn main() {
16771 let a = 1;
16772 let b = 2;
16773 }"
16774 .unindent(),
16775 cx,
16776 )
16777 .await;
16778
16779 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16780}
16781
16782#[gpui::test]
16783async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16784 let (buffer_id, mut cx) = setup_indent_guides_editor(
16785 &"
16786 fn main() {
16787 let a = 1;
16788 if a == 3 {
16789 let b = 2;
16790 } else {
16791 let c = 3;
16792 }
16793 }"
16794 .unindent(),
16795 cx,
16796 )
16797 .await;
16798
16799 assert_indent_guides(
16800 0..8,
16801 vec![
16802 indent_guide(buffer_id, 1, 6, 0),
16803 indent_guide(buffer_id, 3, 3, 1),
16804 indent_guide(buffer_id, 5, 5, 1),
16805 ],
16806 None,
16807 &mut cx,
16808 );
16809}
16810
16811#[gpui::test]
16812async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16813 let (buffer_id, mut cx) = setup_indent_guides_editor(
16814 &"
16815 fn main() {
16816 let a = 1;
16817 let b = 2;
16818 let c = 3;
16819 }"
16820 .unindent(),
16821 cx,
16822 )
16823 .await;
16824
16825 assert_indent_guides(
16826 0..5,
16827 vec![
16828 indent_guide(buffer_id, 1, 3, 0),
16829 indent_guide(buffer_id, 2, 2, 1),
16830 ],
16831 None,
16832 &mut cx,
16833 );
16834}
16835
16836#[gpui::test]
16837async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16838 let (buffer_id, mut cx) = setup_indent_guides_editor(
16839 &"
16840 fn main() {
16841 let a = 1;
16842
16843 let c = 3;
16844 }"
16845 .unindent(),
16846 cx,
16847 )
16848 .await;
16849
16850 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16851}
16852
16853#[gpui::test]
16854async fn test_indent_guide_complex(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 if a == 3 {
16863 let b = 2;
16864 } else {
16865 let c = 3;
16866 }
16867 }"
16868 .unindent(),
16869 cx,
16870 )
16871 .await;
16872
16873 assert_indent_guides(
16874 0..11,
16875 vec![
16876 indent_guide(buffer_id, 1, 9, 0),
16877 indent_guide(buffer_id, 6, 6, 1),
16878 indent_guide(buffer_id, 8, 8, 1),
16879 ],
16880 None,
16881 &mut cx,
16882 );
16883}
16884
16885#[gpui::test]
16886async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16887 let (buffer_id, mut cx) = setup_indent_guides_editor(
16888 &"
16889 fn main() {
16890 let a = 1;
16891
16892 let c = 3;
16893
16894 if a == 3 {
16895 let b = 2;
16896 } else {
16897 let c = 3;
16898 }
16899 }"
16900 .unindent(),
16901 cx,
16902 )
16903 .await;
16904
16905 assert_indent_guides(
16906 1..11,
16907 vec![
16908 indent_guide(buffer_id, 1, 9, 0),
16909 indent_guide(buffer_id, 6, 6, 1),
16910 indent_guide(buffer_id, 8, 8, 1),
16911 ],
16912 None,
16913 &mut cx,
16914 );
16915}
16916
16917#[gpui::test]
16918async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16919 let (buffer_id, mut cx) = setup_indent_guides_editor(
16920 &"
16921 fn main() {
16922 let a = 1;
16923
16924 let c = 3;
16925
16926 if a == 3 {
16927 let b = 2;
16928 } else {
16929 let c = 3;
16930 }
16931 }"
16932 .unindent(),
16933 cx,
16934 )
16935 .await;
16936
16937 assert_indent_guides(
16938 1..10,
16939 vec![
16940 indent_guide(buffer_id, 1, 9, 0),
16941 indent_guide(buffer_id, 6, 6, 1),
16942 indent_guide(buffer_id, 8, 8, 1),
16943 ],
16944 None,
16945 &mut cx,
16946 );
16947}
16948
16949#[gpui::test]
16950async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16951 let (buffer_id, mut cx) = setup_indent_guides_editor(
16952 &"
16953 block1
16954 block2
16955 block3
16956 block4
16957 block2
16958 block1
16959 block1"
16960 .unindent(),
16961 cx,
16962 )
16963 .await;
16964
16965 assert_indent_guides(
16966 1..10,
16967 vec![
16968 indent_guide(buffer_id, 1, 4, 0),
16969 indent_guide(buffer_id, 2, 3, 1),
16970 indent_guide(buffer_id, 3, 3, 2),
16971 ],
16972 None,
16973 &mut cx,
16974 );
16975}
16976
16977#[gpui::test]
16978async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16979 let (buffer_id, mut cx) = setup_indent_guides_editor(
16980 &"
16981 block1
16982 block2
16983 block3
16984
16985 block1
16986 block1"
16987 .unindent(),
16988 cx,
16989 )
16990 .await;
16991
16992 assert_indent_guides(
16993 0..6,
16994 vec![
16995 indent_guide(buffer_id, 1, 2, 0),
16996 indent_guide(buffer_id, 2, 2, 1),
16997 ],
16998 None,
16999 &mut cx,
17000 );
17001}
17002
17003#[gpui::test]
17004async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17005 let (buffer_id, mut cx) = setup_indent_guides_editor(
17006 &"
17007 block1
17008
17009
17010
17011 block2
17012 "
17013 .unindent(),
17014 cx,
17015 )
17016 .await;
17017
17018 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17019}
17020
17021#[gpui::test]
17022async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17023 let (buffer_id, mut cx) = setup_indent_guides_editor(
17024 &"
17025 def a:
17026 \tb = 3
17027 \tif True:
17028 \t\tc = 4
17029 \t\td = 5
17030 \tprint(b)
17031 "
17032 .unindent(),
17033 cx,
17034 )
17035 .await;
17036
17037 assert_indent_guides(
17038 0..6,
17039 vec![
17040 indent_guide(buffer_id, 1, 5, 0),
17041 indent_guide(buffer_id, 3, 4, 1),
17042 ],
17043 None,
17044 &mut cx,
17045 );
17046}
17047
17048#[gpui::test]
17049async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17050 let (buffer_id, mut cx) = setup_indent_guides_editor(
17051 &"
17052 fn main() {
17053 let a = 1;
17054 }"
17055 .unindent(),
17056 cx,
17057 )
17058 .await;
17059
17060 cx.update_editor(|editor, window, cx| {
17061 editor.change_selections(None, window, cx, |s| {
17062 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17063 });
17064 });
17065
17066 assert_indent_guides(
17067 0..3,
17068 vec![indent_guide(buffer_id, 1, 1, 0)],
17069 Some(vec![0]),
17070 &mut cx,
17071 );
17072}
17073
17074#[gpui::test]
17075async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17076 let (buffer_id, mut cx) = setup_indent_guides_editor(
17077 &"
17078 fn main() {
17079 if 1 == 2 {
17080 let a = 1;
17081 }
17082 }"
17083 .unindent(),
17084 cx,
17085 )
17086 .await;
17087
17088 cx.update_editor(|editor, window, cx| {
17089 editor.change_selections(None, window, cx, |s| {
17090 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17091 });
17092 });
17093
17094 assert_indent_guides(
17095 0..4,
17096 vec![
17097 indent_guide(buffer_id, 1, 3, 0),
17098 indent_guide(buffer_id, 2, 2, 1),
17099 ],
17100 Some(vec![1]),
17101 &mut cx,
17102 );
17103
17104 cx.update_editor(|editor, window, cx| {
17105 editor.change_selections(None, window, cx, |s| {
17106 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17107 });
17108 });
17109
17110 assert_indent_guides(
17111 0..4,
17112 vec![
17113 indent_guide(buffer_id, 1, 3, 0),
17114 indent_guide(buffer_id, 2, 2, 1),
17115 ],
17116 Some(vec![1]),
17117 &mut cx,
17118 );
17119
17120 cx.update_editor(|editor, window, cx| {
17121 editor.change_selections(None, window, cx, |s| {
17122 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17123 });
17124 });
17125
17126 assert_indent_guides(
17127 0..4,
17128 vec![
17129 indent_guide(buffer_id, 1, 3, 0),
17130 indent_guide(buffer_id, 2, 2, 1),
17131 ],
17132 Some(vec![0]),
17133 &mut cx,
17134 );
17135}
17136
17137#[gpui::test]
17138async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17139 let (buffer_id, mut cx) = setup_indent_guides_editor(
17140 &"
17141 fn main() {
17142 let a = 1;
17143
17144 let b = 2;
17145 }"
17146 .unindent(),
17147 cx,
17148 )
17149 .await;
17150
17151 cx.update_editor(|editor, window, cx| {
17152 editor.change_selections(None, window, cx, |s| {
17153 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17154 });
17155 });
17156
17157 assert_indent_guides(
17158 0..5,
17159 vec![indent_guide(buffer_id, 1, 3, 0)],
17160 Some(vec![0]),
17161 &mut cx,
17162 );
17163}
17164
17165#[gpui::test]
17166async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17167 let (buffer_id, mut cx) = setup_indent_guides_editor(
17168 &"
17169 def m:
17170 a = 1
17171 pass"
17172 .unindent(),
17173 cx,
17174 )
17175 .await;
17176
17177 cx.update_editor(|editor, window, cx| {
17178 editor.change_selections(None, window, cx, |s| {
17179 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17180 });
17181 });
17182
17183 assert_indent_guides(
17184 0..3,
17185 vec![indent_guide(buffer_id, 1, 2, 0)],
17186 Some(vec![0]),
17187 &mut cx,
17188 );
17189}
17190
17191#[gpui::test]
17192async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17193 init_test(cx, |_| {});
17194 let mut cx = EditorTestContext::new(cx).await;
17195 let text = indoc! {
17196 "
17197 impl A {
17198 fn b() {
17199 0;
17200 3;
17201 5;
17202 6;
17203 7;
17204 }
17205 }
17206 "
17207 };
17208 let base_text = indoc! {
17209 "
17210 impl A {
17211 fn b() {
17212 0;
17213 1;
17214 2;
17215 3;
17216 4;
17217 }
17218 fn c() {
17219 5;
17220 6;
17221 7;
17222 }
17223 }
17224 "
17225 };
17226
17227 cx.update_editor(|editor, window, cx| {
17228 editor.set_text(text, window, cx);
17229
17230 editor.buffer().update(cx, |multibuffer, cx| {
17231 let buffer = multibuffer.as_singleton().unwrap();
17232 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17233
17234 multibuffer.set_all_diff_hunks_expanded(cx);
17235 multibuffer.add_diff(diff, cx);
17236
17237 buffer.read(cx).remote_id()
17238 })
17239 });
17240 cx.run_until_parked();
17241
17242 cx.assert_state_with_diff(
17243 indoc! { "
17244 impl A {
17245 fn b() {
17246 0;
17247 - 1;
17248 - 2;
17249 3;
17250 - 4;
17251 - }
17252 - fn c() {
17253 5;
17254 6;
17255 7;
17256 }
17257 }
17258 ˇ"
17259 }
17260 .to_string(),
17261 );
17262
17263 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17264 editor
17265 .snapshot(window, cx)
17266 .buffer_snapshot
17267 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17268 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17269 .collect::<Vec<_>>()
17270 });
17271 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17272 assert_eq!(
17273 actual_guides,
17274 vec![
17275 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17276 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17277 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17278 ]
17279 );
17280}
17281
17282#[gpui::test]
17283async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17284 init_test(cx, |_| {});
17285 let mut cx = EditorTestContext::new(cx).await;
17286
17287 let diff_base = r#"
17288 a
17289 b
17290 c
17291 "#
17292 .unindent();
17293
17294 cx.set_state(
17295 &r#"
17296 ˇA
17297 b
17298 C
17299 "#
17300 .unindent(),
17301 );
17302 cx.set_head_text(&diff_base);
17303 cx.update_editor(|editor, window, cx| {
17304 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17305 });
17306 executor.run_until_parked();
17307
17308 let both_hunks_expanded = r#"
17309 - a
17310 + ˇA
17311 b
17312 - c
17313 + C
17314 "#
17315 .unindent();
17316
17317 cx.assert_state_with_diff(both_hunks_expanded.clone());
17318
17319 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17320 let snapshot = editor.snapshot(window, cx);
17321 let hunks = editor
17322 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17323 .collect::<Vec<_>>();
17324 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17325 let buffer_id = hunks[0].buffer_id;
17326 hunks
17327 .into_iter()
17328 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17329 .collect::<Vec<_>>()
17330 });
17331 assert_eq!(hunk_ranges.len(), 2);
17332
17333 cx.update_editor(|editor, _, cx| {
17334 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17335 });
17336 executor.run_until_parked();
17337
17338 let second_hunk_expanded = r#"
17339 ˇA
17340 b
17341 - c
17342 + C
17343 "#
17344 .unindent();
17345
17346 cx.assert_state_with_diff(second_hunk_expanded);
17347
17348 cx.update_editor(|editor, _, cx| {
17349 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17350 });
17351 executor.run_until_parked();
17352
17353 cx.assert_state_with_diff(both_hunks_expanded.clone());
17354
17355 cx.update_editor(|editor, _, cx| {
17356 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17357 });
17358 executor.run_until_parked();
17359
17360 let first_hunk_expanded = r#"
17361 - a
17362 + ˇA
17363 b
17364 C
17365 "#
17366 .unindent();
17367
17368 cx.assert_state_with_diff(first_hunk_expanded);
17369
17370 cx.update_editor(|editor, _, cx| {
17371 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17372 });
17373 executor.run_until_parked();
17374
17375 cx.assert_state_with_diff(both_hunks_expanded);
17376
17377 cx.set_state(
17378 &r#"
17379 ˇA
17380 b
17381 "#
17382 .unindent(),
17383 );
17384 cx.run_until_parked();
17385
17386 // TODO this cursor position seems bad
17387 cx.assert_state_with_diff(
17388 r#"
17389 - ˇa
17390 + A
17391 b
17392 "#
17393 .unindent(),
17394 );
17395
17396 cx.update_editor(|editor, window, cx| {
17397 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17398 });
17399
17400 cx.assert_state_with_diff(
17401 r#"
17402 - ˇa
17403 + A
17404 b
17405 - c
17406 "#
17407 .unindent(),
17408 );
17409
17410 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17411 let snapshot = editor.snapshot(window, cx);
17412 let hunks = editor
17413 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17414 .collect::<Vec<_>>();
17415 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17416 let buffer_id = hunks[0].buffer_id;
17417 hunks
17418 .into_iter()
17419 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17420 .collect::<Vec<_>>()
17421 });
17422 assert_eq!(hunk_ranges.len(), 2);
17423
17424 cx.update_editor(|editor, _, cx| {
17425 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17426 });
17427 executor.run_until_parked();
17428
17429 cx.assert_state_with_diff(
17430 r#"
17431 - ˇa
17432 + A
17433 b
17434 "#
17435 .unindent(),
17436 );
17437}
17438
17439#[gpui::test]
17440async fn test_toggle_deletion_hunk_at_start_of_file(
17441 executor: BackgroundExecutor,
17442 cx: &mut TestAppContext,
17443) {
17444 init_test(cx, |_| {});
17445 let mut cx = EditorTestContext::new(cx).await;
17446
17447 let diff_base = r#"
17448 a
17449 b
17450 c
17451 "#
17452 .unindent();
17453
17454 cx.set_state(
17455 &r#"
17456 ˇb
17457 c
17458 "#
17459 .unindent(),
17460 );
17461 cx.set_head_text(&diff_base);
17462 cx.update_editor(|editor, window, cx| {
17463 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17464 });
17465 executor.run_until_parked();
17466
17467 let hunk_expanded = r#"
17468 - a
17469 ˇb
17470 c
17471 "#
17472 .unindent();
17473
17474 cx.assert_state_with_diff(hunk_expanded.clone());
17475
17476 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17477 let snapshot = editor.snapshot(window, cx);
17478 let hunks = editor
17479 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17480 .collect::<Vec<_>>();
17481 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17482 let buffer_id = hunks[0].buffer_id;
17483 hunks
17484 .into_iter()
17485 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17486 .collect::<Vec<_>>()
17487 });
17488 assert_eq!(hunk_ranges.len(), 1);
17489
17490 cx.update_editor(|editor, _, cx| {
17491 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17492 });
17493 executor.run_until_parked();
17494
17495 let hunk_collapsed = r#"
17496 ˇb
17497 c
17498 "#
17499 .unindent();
17500
17501 cx.assert_state_with_diff(hunk_collapsed);
17502
17503 cx.update_editor(|editor, _, cx| {
17504 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17505 });
17506 executor.run_until_parked();
17507
17508 cx.assert_state_with_diff(hunk_expanded.clone());
17509}
17510
17511#[gpui::test]
17512async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17513 init_test(cx, |_| {});
17514
17515 let fs = FakeFs::new(cx.executor());
17516 fs.insert_tree(
17517 path!("/test"),
17518 json!({
17519 ".git": {},
17520 "file-1": "ONE\n",
17521 "file-2": "TWO\n",
17522 "file-3": "THREE\n",
17523 }),
17524 )
17525 .await;
17526
17527 fs.set_head_for_repo(
17528 path!("/test/.git").as_ref(),
17529 &[
17530 ("file-1".into(), "one\n".into()),
17531 ("file-2".into(), "two\n".into()),
17532 ("file-3".into(), "three\n".into()),
17533 ],
17534 );
17535
17536 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17537 let mut buffers = vec![];
17538 for i in 1..=3 {
17539 let buffer = project
17540 .update(cx, |project, cx| {
17541 let path = format!(path!("/test/file-{}"), i);
17542 project.open_local_buffer(path, cx)
17543 })
17544 .await
17545 .unwrap();
17546 buffers.push(buffer);
17547 }
17548
17549 let multibuffer = cx.new(|cx| {
17550 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17551 multibuffer.set_all_diff_hunks_expanded(cx);
17552 for buffer in &buffers {
17553 let snapshot = buffer.read(cx).snapshot();
17554 multibuffer.set_excerpts_for_path(
17555 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17556 buffer.clone(),
17557 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17558 DEFAULT_MULTIBUFFER_CONTEXT,
17559 cx,
17560 );
17561 }
17562 multibuffer
17563 });
17564
17565 let editor = cx.add_window(|window, cx| {
17566 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17567 });
17568 cx.run_until_parked();
17569
17570 let snapshot = editor
17571 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17572 .unwrap();
17573 let hunks = snapshot
17574 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17575 .map(|hunk| match hunk {
17576 DisplayDiffHunk::Unfolded {
17577 display_row_range, ..
17578 } => display_row_range,
17579 DisplayDiffHunk::Folded { .. } => unreachable!(),
17580 })
17581 .collect::<Vec<_>>();
17582 assert_eq!(
17583 hunks,
17584 [
17585 DisplayRow(2)..DisplayRow(4),
17586 DisplayRow(7)..DisplayRow(9),
17587 DisplayRow(12)..DisplayRow(14),
17588 ]
17589 );
17590}
17591
17592#[gpui::test]
17593async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17594 init_test(cx, |_| {});
17595
17596 let mut cx = EditorTestContext::new(cx).await;
17597 cx.set_head_text(indoc! { "
17598 one
17599 two
17600 three
17601 four
17602 five
17603 "
17604 });
17605 cx.set_index_text(indoc! { "
17606 one
17607 two
17608 three
17609 four
17610 five
17611 "
17612 });
17613 cx.set_state(indoc! {"
17614 one
17615 TWO
17616 ˇTHREE
17617 FOUR
17618 five
17619 "});
17620 cx.run_until_parked();
17621 cx.update_editor(|editor, window, cx| {
17622 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17623 });
17624 cx.run_until_parked();
17625 cx.assert_index_text(Some(indoc! {"
17626 one
17627 TWO
17628 THREE
17629 FOUR
17630 five
17631 "}));
17632 cx.set_state(indoc! { "
17633 one
17634 TWO
17635 ˇTHREE-HUNDRED
17636 FOUR
17637 five
17638 "});
17639 cx.run_until_parked();
17640 cx.update_editor(|editor, window, cx| {
17641 let snapshot = editor.snapshot(window, cx);
17642 let hunks = editor
17643 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17644 .collect::<Vec<_>>();
17645 assert_eq!(hunks.len(), 1);
17646 assert_eq!(
17647 hunks[0].status(),
17648 DiffHunkStatus {
17649 kind: DiffHunkStatusKind::Modified,
17650 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17651 }
17652 );
17653
17654 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17655 });
17656 cx.run_until_parked();
17657 cx.assert_index_text(Some(indoc! {"
17658 one
17659 TWO
17660 THREE-HUNDRED
17661 FOUR
17662 five
17663 "}));
17664}
17665
17666#[gpui::test]
17667fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17668 init_test(cx, |_| {});
17669
17670 let editor = cx.add_window(|window, cx| {
17671 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17672 build_editor(buffer, window, cx)
17673 });
17674
17675 let render_args = Arc::new(Mutex::new(None));
17676 let snapshot = editor
17677 .update(cx, |editor, window, cx| {
17678 let snapshot = editor.buffer().read(cx).snapshot(cx);
17679 let range =
17680 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17681
17682 struct RenderArgs {
17683 row: MultiBufferRow,
17684 folded: bool,
17685 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17686 }
17687
17688 let crease = Crease::inline(
17689 range,
17690 FoldPlaceholder::test(),
17691 {
17692 let toggle_callback = render_args.clone();
17693 move |row, folded, callback, _window, _cx| {
17694 *toggle_callback.lock() = Some(RenderArgs {
17695 row,
17696 folded,
17697 callback,
17698 });
17699 div()
17700 }
17701 },
17702 |_row, _folded, _window, _cx| div(),
17703 );
17704
17705 editor.insert_creases(Some(crease), cx);
17706 let snapshot = editor.snapshot(window, cx);
17707 let _div = snapshot.render_crease_toggle(
17708 MultiBufferRow(1),
17709 false,
17710 cx.entity().clone(),
17711 window,
17712 cx,
17713 );
17714 snapshot
17715 })
17716 .unwrap();
17717
17718 let render_args = render_args.lock().take().unwrap();
17719 assert_eq!(render_args.row, MultiBufferRow(1));
17720 assert!(!render_args.folded);
17721 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17722
17723 cx.update_window(*editor, |_, window, cx| {
17724 (render_args.callback)(true, window, cx)
17725 })
17726 .unwrap();
17727 let snapshot = editor
17728 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17729 .unwrap();
17730 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17731
17732 cx.update_window(*editor, |_, window, cx| {
17733 (render_args.callback)(false, window, cx)
17734 })
17735 .unwrap();
17736 let snapshot = editor
17737 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17738 .unwrap();
17739 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17740}
17741
17742#[gpui::test]
17743async fn test_input_text(cx: &mut TestAppContext) {
17744 init_test(cx, |_| {});
17745 let mut cx = EditorTestContext::new(cx).await;
17746
17747 cx.set_state(
17748 &r#"ˇone
17749 two
17750
17751 three
17752 fourˇ
17753 five
17754
17755 siˇx"#
17756 .unindent(),
17757 );
17758
17759 cx.dispatch_action(HandleInput(String::new()));
17760 cx.assert_editor_state(
17761 &r#"ˇone
17762 two
17763
17764 three
17765 fourˇ
17766 five
17767
17768 siˇx"#
17769 .unindent(),
17770 );
17771
17772 cx.dispatch_action(HandleInput("AAAA".to_string()));
17773 cx.assert_editor_state(
17774 &r#"AAAAˇone
17775 two
17776
17777 three
17778 fourAAAAˇ
17779 five
17780
17781 siAAAAˇx"#
17782 .unindent(),
17783 );
17784}
17785
17786#[gpui::test]
17787async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17788 init_test(cx, |_| {});
17789
17790 let mut cx = EditorTestContext::new(cx).await;
17791 cx.set_state(
17792 r#"let foo = 1;
17793let foo = 2;
17794let foo = 3;
17795let fooˇ = 4;
17796let foo = 5;
17797let foo = 6;
17798let foo = 7;
17799let foo = 8;
17800let foo = 9;
17801let foo = 10;
17802let foo = 11;
17803let foo = 12;
17804let foo = 13;
17805let foo = 14;
17806let foo = 15;"#,
17807 );
17808
17809 cx.update_editor(|e, window, cx| {
17810 assert_eq!(
17811 e.next_scroll_position,
17812 NextScrollCursorCenterTopBottom::Center,
17813 "Default next scroll direction is center",
17814 );
17815
17816 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17817 assert_eq!(
17818 e.next_scroll_position,
17819 NextScrollCursorCenterTopBottom::Top,
17820 "After center, next scroll direction should be top",
17821 );
17822
17823 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17824 assert_eq!(
17825 e.next_scroll_position,
17826 NextScrollCursorCenterTopBottom::Bottom,
17827 "After top, next scroll direction should be bottom",
17828 );
17829
17830 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17831 assert_eq!(
17832 e.next_scroll_position,
17833 NextScrollCursorCenterTopBottom::Center,
17834 "After bottom, scrolling should start over",
17835 );
17836
17837 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17838 assert_eq!(
17839 e.next_scroll_position,
17840 NextScrollCursorCenterTopBottom::Top,
17841 "Scrolling continues if retriggered fast enough"
17842 );
17843 });
17844
17845 cx.executor()
17846 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17847 cx.executor().run_until_parked();
17848 cx.update_editor(|e, _, _| {
17849 assert_eq!(
17850 e.next_scroll_position,
17851 NextScrollCursorCenterTopBottom::Center,
17852 "If scrolling is not triggered fast enough, it should reset"
17853 );
17854 });
17855}
17856
17857#[gpui::test]
17858async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17859 init_test(cx, |_| {});
17860 let mut cx = EditorLspTestContext::new_rust(
17861 lsp::ServerCapabilities {
17862 definition_provider: Some(lsp::OneOf::Left(true)),
17863 references_provider: Some(lsp::OneOf::Left(true)),
17864 ..lsp::ServerCapabilities::default()
17865 },
17866 cx,
17867 )
17868 .await;
17869
17870 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17871 let go_to_definition = cx
17872 .lsp
17873 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17874 move |params, _| async move {
17875 if empty_go_to_definition {
17876 Ok(None)
17877 } else {
17878 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17879 uri: params.text_document_position_params.text_document.uri,
17880 range: lsp::Range::new(
17881 lsp::Position::new(4, 3),
17882 lsp::Position::new(4, 6),
17883 ),
17884 })))
17885 }
17886 },
17887 );
17888 let references = cx
17889 .lsp
17890 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17891 Ok(Some(vec![lsp::Location {
17892 uri: params.text_document_position.text_document.uri,
17893 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17894 }]))
17895 });
17896 (go_to_definition, references)
17897 };
17898
17899 cx.set_state(
17900 &r#"fn one() {
17901 let mut a = ˇtwo();
17902 }
17903
17904 fn two() {}"#
17905 .unindent(),
17906 );
17907 set_up_lsp_handlers(false, &mut cx);
17908 let navigated = cx
17909 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17910 .await
17911 .expect("Failed to navigate to definition");
17912 assert_eq!(
17913 navigated,
17914 Navigated::Yes,
17915 "Should have navigated to definition from the GetDefinition response"
17916 );
17917 cx.assert_editor_state(
17918 &r#"fn one() {
17919 let mut a = two();
17920 }
17921
17922 fn «twoˇ»() {}"#
17923 .unindent(),
17924 );
17925
17926 let editors = cx.update_workspace(|workspace, _, cx| {
17927 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17928 });
17929 cx.update_editor(|_, _, test_editor_cx| {
17930 assert_eq!(
17931 editors.len(),
17932 1,
17933 "Initially, only one, test, editor should be open in the workspace"
17934 );
17935 assert_eq!(
17936 test_editor_cx.entity(),
17937 editors.last().expect("Asserted len is 1").clone()
17938 );
17939 });
17940
17941 set_up_lsp_handlers(true, &mut cx);
17942 let navigated = cx
17943 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17944 .await
17945 .expect("Failed to navigate to lookup references");
17946 assert_eq!(
17947 navigated,
17948 Navigated::Yes,
17949 "Should have navigated to references as a fallback after empty GoToDefinition response"
17950 );
17951 // We should not change the selections in the existing file,
17952 // if opening another milti buffer with the references
17953 cx.assert_editor_state(
17954 &r#"fn one() {
17955 let mut a = two();
17956 }
17957
17958 fn «twoˇ»() {}"#
17959 .unindent(),
17960 );
17961 let editors = cx.update_workspace(|workspace, _, cx| {
17962 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17963 });
17964 cx.update_editor(|_, _, test_editor_cx| {
17965 assert_eq!(
17966 editors.len(),
17967 2,
17968 "After falling back to references search, we open a new editor with the results"
17969 );
17970 let references_fallback_text = editors
17971 .into_iter()
17972 .find(|new_editor| *new_editor != test_editor_cx.entity())
17973 .expect("Should have one non-test editor now")
17974 .read(test_editor_cx)
17975 .text(test_editor_cx);
17976 assert_eq!(
17977 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17978 "Should use the range from the references response and not the GoToDefinition one"
17979 );
17980 });
17981}
17982
17983#[gpui::test]
17984async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17985 init_test(cx, |_| {});
17986 cx.update(|cx| {
17987 let mut editor_settings = EditorSettings::get_global(cx).clone();
17988 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17989 EditorSettings::override_global(editor_settings, cx);
17990 });
17991 let mut cx = EditorLspTestContext::new_rust(
17992 lsp::ServerCapabilities {
17993 definition_provider: Some(lsp::OneOf::Left(true)),
17994 references_provider: Some(lsp::OneOf::Left(true)),
17995 ..lsp::ServerCapabilities::default()
17996 },
17997 cx,
17998 )
17999 .await;
18000 let original_state = r#"fn one() {
18001 let mut a = ˇtwo();
18002 }
18003
18004 fn two() {}"#
18005 .unindent();
18006 cx.set_state(&original_state);
18007
18008 let mut go_to_definition = cx
18009 .lsp
18010 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18011 move |_, _| async move { Ok(None) },
18012 );
18013 let _references = cx
18014 .lsp
18015 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18016 panic!("Should not call for references with no go to definition fallback")
18017 });
18018
18019 let navigated = cx
18020 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18021 .await
18022 .expect("Failed to navigate to lookup references");
18023 go_to_definition
18024 .next()
18025 .await
18026 .expect("Should have called the go_to_definition handler");
18027
18028 assert_eq!(
18029 navigated,
18030 Navigated::No,
18031 "Should have navigated to references as a fallback after empty GoToDefinition response"
18032 );
18033 cx.assert_editor_state(&original_state);
18034 let editors = cx.update_workspace(|workspace, _, cx| {
18035 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18036 });
18037 cx.update_editor(|_, _, _| {
18038 assert_eq!(
18039 editors.len(),
18040 1,
18041 "After unsuccessful fallback, no other editor should have been opened"
18042 );
18043 });
18044}
18045
18046#[gpui::test]
18047async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18048 init_test(cx, |_| {});
18049
18050 let language = Arc::new(Language::new(
18051 LanguageConfig::default(),
18052 Some(tree_sitter_rust::LANGUAGE.into()),
18053 ));
18054
18055 let text = r#"
18056 #[cfg(test)]
18057 mod tests() {
18058 #[test]
18059 fn runnable_1() {
18060 let a = 1;
18061 }
18062
18063 #[test]
18064 fn runnable_2() {
18065 let a = 1;
18066 let b = 2;
18067 }
18068 }
18069 "#
18070 .unindent();
18071
18072 let fs = FakeFs::new(cx.executor());
18073 fs.insert_file("/file.rs", Default::default()).await;
18074
18075 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18076 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18077 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18078 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18079 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18080
18081 let editor = cx.new_window_entity(|window, cx| {
18082 Editor::new(
18083 EditorMode::full(),
18084 multi_buffer,
18085 Some(project.clone()),
18086 window,
18087 cx,
18088 )
18089 });
18090
18091 editor.update_in(cx, |editor, window, cx| {
18092 let snapshot = editor.buffer().read(cx).snapshot(cx);
18093 editor.tasks.insert(
18094 (buffer.read(cx).remote_id(), 3),
18095 RunnableTasks {
18096 templates: vec![],
18097 offset: snapshot.anchor_before(43),
18098 column: 0,
18099 extra_variables: HashMap::default(),
18100 context_range: BufferOffset(43)..BufferOffset(85),
18101 },
18102 );
18103 editor.tasks.insert(
18104 (buffer.read(cx).remote_id(), 8),
18105 RunnableTasks {
18106 templates: vec![],
18107 offset: snapshot.anchor_before(86),
18108 column: 0,
18109 extra_variables: HashMap::default(),
18110 context_range: BufferOffset(86)..BufferOffset(191),
18111 },
18112 );
18113
18114 // Test finding task when cursor is inside function body
18115 editor.change_selections(None, window, cx, |s| {
18116 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18117 });
18118 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18119 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18120
18121 // Test finding task when cursor is on function name
18122 editor.change_selections(None, window, cx, |s| {
18123 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18124 });
18125 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18126 assert_eq!(row, 8, "Should find task when cursor is on function name");
18127 });
18128}
18129
18130#[gpui::test]
18131async fn test_folding_buffers(cx: &mut TestAppContext) {
18132 init_test(cx, |_| {});
18133
18134 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18135 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18136 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18137
18138 let fs = FakeFs::new(cx.executor());
18139 fs.insert_tree(
18140 path!("/a"),
18141 json!({
18142 "first.rs": sample_text_1,
18143 "second.rs": sample_text_2,
18144 "third.rs": sample_text_3,
18145 }),
18146 )
18147 .await;
18148 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18149 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18150 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18151 let worktree = project.update(cx, |project, cx| {
18152 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18153 assert_eq!(worktrees.len(), 1);
18154 worktrees.pop().unwrap()
18155 });
18156 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18157
18158 let buffer_1 = project
18159 .update(cx, |project, cx| {
18160 project.open_buffer((worktree_id, "first.rs"), cx)
18161 })
18162 .await
18163 .unwrap();
18164 let buffer_2 = project
18165 .update(cx, |project, cx| {
18166 project.open_buffer((worktree_id, "second.rs"), cx)
18167 })
18168 .await
18169 .unwrap();
18170 let buffer_3 = project
18171 .update(cx, |project, cx| {
18172 project.open_buffer((worktree_id, "third.rs"), cx)
18173 })
18174 .await
18175 .unwrap();
18176
18177 let multi_buffer = cx.new(|cx| {
18178 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18179 multi_buffer.push_excerpts(
18180 buffer_1.clone(),
18181 [
18182 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18183 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18184 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18185 ],
18186 cx,
18187 );
18188 multi_buffer.push_excerpts(
18189 buffer_2.clone(),
18190 [
18191 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18192 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18193 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18194 ],
18195 cx,
18196 );
18197 multi_buffer.push_excerpts(
18198 buffer_3.clone(),
18199 [
18200 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18201 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18202 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18203 ],
18204 cx,
18205 );
18206 multi_buffer
18207 });
18208 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18209 Editor::new(
18210 EditorMode::full(),
18211 multi_buffer.clone(),
18212 Some(project.clone()),
18213 window,
18214 cx,
18215 )
18216 });
18217
18218 assert_eq!(
18219 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18220 "\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",
18221 );
18222
18223 multi_buffer_editor.update(cx, |editor, cx| {
18224 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18225 });
18226 assert_eq!(
18227 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18228 "\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",
18229 "After folding the first buffer, its text should not be displayed"
18230 );
18231
18232 multi_buffer_editor.update(cx, |editor, cx| {
18233 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18234 });
18235 assert_eq!(
18236 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18237 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18238 "After folding the second buffer, its text should not be displayed"
18239 );
18240
18241 multi_buffer_editor.update(cx, |editor, cx| {
18242 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18243 });
18244 assert_eq!(
18245 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18246 "\n\n\n\n\n",
18247 "After folding the third buffer, its text should not be displayed"
18248 );
18249
18250 // Emulate selection inside the fold logic, that should work
18251 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18252 editor
18253 .snapshot(window, cx)
18254 .next_line_boundary(Point::new(0, 4));
18255 });
18256
18257 multi_buffer_editor.update(cx, |editor, cx| {
18258 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18259 });
18260 assert_eq!(
18261 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18262 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18263 "After unfolding the second buffer, its text should be displayed"
18264 );
18265
18266 // Typing inside of buffer 1 causes that buffer to be unfolded.
18267 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18268 assert_eq!(
18269 multi_buffer
18270 .read(cx)
18271 .snapshot(cx)
18272 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18273 .collect::<String>(),
18274 "bbbb"
18275 );
18276 editor.change_selections(None, window, cx, |selections| {
18277 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18278 });
18279 editor.handle_input("B", window, cx);
18280 });
18281
18282 assert_eq!(
18283 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18284 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18285 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18286 );
18287
18288 multi_buffer_editor.update(cx, |editor, cx| {
18289 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18290 });
18291 assert_eq!(
18292 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18293 "\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",
18294 "After unfolding the all buffers, all original text should be displayed"
18295 );
18296}
18297
18298#[gpui::test]
18299async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18300 init_test(cx, |_| {});
18301
18302 let sample_text_1 = "1111\n2222\n3333".to_string();
18303 let sample_text_2 = "4444\n5555\n6666".to_string();
18304 let sample_text_3 = "7777\n8888\n9999".to_string();
18305
18306 let fs = FakeFs::new(cx.executor());
18307 fs.insert_tree(
18308 path!("/a"),
18309 json!({
18310 "first.rs": sample_text_1,
18311 "second.rs": sample_text_2,
18312 "third.rs": sample_text_3,
18313 }),
18314 )
18315 .await;
18316 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18317 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18318 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18319 let worktree = project.update(cx, |project, cx| {
18320 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18321 assert_eq!(worktrees.len(), 1);
18322 worktrees.pop().unwrap()
18323 });
18324 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18325
18326 let buffer_1 = project
18327 .update(cx, |project, cx| {
18328 project.open_buffer((worktree_id, "first.rs"), cx)
18329 })
18330 .await
18331 .unwrap();
18332 let buffer_2 = project
18333 .update(cx, |project, cx| {
18334 project.open_buffer((worktree_id, "second.rs"), cx)
18335 })
18336 .await
18337 .unwrap();
18338 let buffer_3 = project
18339 .update(cx, |project, cx| {
18340 project.open_buffer((worktree_id, "third.rs"), cx)
18341 })
18342 .await
18343 .unwrap();
18344
18345 let multi_buffer = cx.new(|cx| {
18346 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18347 multi_buffer.push_excerpts(
18348 buffer_1.clone(),
18349 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18350 cx,
18351 );
18352 multi_buffer.push_excerpts(
18353 buffer_2.clone(),
18354 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18355 cx,
18356 );
18357 multi_buffer.push_excerpts(
18358 buffer_3.clone(),
18359 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18360 cx,
18361 );
18362 multi_buffer
18363 });
18364
18365 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18366 Editor::new(
18367 EditorMode::full(),
18368 multi_buffer,
18369 Some(project.clone()),
18370 window,
18371 cx,
18372 )
18373 });
18374
18375 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18376 assert_eq!(
18377 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18378 full_text,
18379 );
18380
18381 multi_buffer_editor.update(cx, |editor, cx| {
18382 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18383 });
18384 assert_eq!(
18385 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18386 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18387 "After folding the first buffer, its text should not be displayed"
18388 );
18389
18390 multi_buffer_editor.update(cx, |editor, cx| {
18391 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18392 });
18393
18394 assert_eq!(
18395 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18396 "\n\n\n\n\n\n7777\n8888\n9999",
18397 "After folding the second buffer, its text should not be displayed"
18398 );
18399
18400 multi_buffer_editor.update(cx, |editor, cx| {
18401 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18402 });
18403 assert_eq!(
18404 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18405 "\n\n\n\n\n",
18406 "After folding the third buffer, its text should not be displayed"
18407 );
18408
18409 multi_buffer_editor.update(cx, |editor, cx| {
18410 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18411 });
18412 assert_eq!(
18413 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18414 "\n\n\n\n4444\n5555\n6666\n\n",
18415 "After unfolding the second buffer, its text should be displayed"
18416 );
18417
18418 multi_buffer_editor.update(cx, |editor, cx| {
18419 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18420 });
18421 assert_eq!(
18422 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18423 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18424 "After unfolding the first buffer, its text should be displayed"
18425 );
18426
18427 multi_buffer_editor.update(cx, |editor, cx| {
18428 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18429 });
18430 assert_eq!(
18431 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18432 full_text,
18433 "After unfolding all buffers, all original text should be displayed"
18434 );
18435}
18436
18437#[gpui::test]
18438async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18439 init_test(cx, |_| {});
18440
18441 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18442
18443 let fs = FakeFs::new(cx.executor());
18444 fs.insert_tree(
18445 path!("/a"),
18446 json!({
18447 "main.rs": sample_text,
18448 }),
18449 )
18450 .await;
18451 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18452 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18453 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18454 let worktree = project.update(cx, |project, cx| {
18455 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18456 assert_eq!(worktrees.len(), 1);
18457 worktrees.pop().unwrap()
18458 });
18459 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18460
18461 let buffer_1 = project
18462 .update(cx, |project, cx| {
18463 project.open_buffer((worktree_id, "main.rs"), cx)
18464 })
18465 .await
18466 .unwrap();
18467
18468 let multi_buffer = cx.new(|cx| {
18469 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18470 multi_buffer.push_excerpts(
18471 buffer_1.clone(),
18472 [ExcerptRange::new(
18473 Point::new(0, 0)
18474 ..Point::new(
18475 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18476 0,
18477 ),
18478 )],
18479 cx,
18480 );
18481 multi_buffer
18482 });
18483 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18484 Editor::new(
18485 EditorMode::full(),
18486 multi_buffer,
18487 Some(project.clone()),
18488 window,
18489 cx,
18490 )
18491 });
18492
18493 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18494 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18495 enum TestHighlight {}
18496 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18497 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18498 editor.highlight_text::<TestHighlight>(
18499 vec![highlight_range.clone()],
18500 HighlightStyle::color(Hsla::green()),
18501 cx,
18502 );
18503 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18504 });
18505
18506 let full_text = format!("\n\n{sample_text}");
18507 assert_eq!(
18508 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18509 full_text,
18510 );
18511}
18512
18513#[gpui::test]
18514async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18515 init_test(cx, |_| {});
18516 cx.update(|cx| {
18517 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18518 "keymaps/default-linux.json",
18519 cx,
18520 )
18521 .unwrap();
18522 cx.bind_keys(default_key_bindings);
18523 });
18524
18525 let (editor, cx) = cx.add_window_view(|window, cx| {
18526 let multi_buffer = MultiBuffer::build_multi(
18527 [
18528 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18529 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18530 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18531 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18532 ],
18533 cx,
18534 );
18535 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18536
18537 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18538 // fold all but the second buffer, so that we test navigating between two
18539 // adjacent folded buffers, as well as folded buffers at the start and
18540 // end the multibuffer
18541 editor.fold_buffer(buffer_ids[0], cx);
18542 editor.fold_buffer(buffer_ids[2], cx);
18543 editor.fold_buffer(buffer_ids[3], cx);
18544
18545 editor
18546 });
18547 cx.simulate_resize(size(px(1000.), px(1000.)));
18548
18549 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18550 cx.assert_excerpts_with_selections(indoc! {"
18551 [EXCERPT]
18552 ˇ[FOLDED]
18553 [EXCERPT]
18554 a1
18555 b1
18556 [EXCERPT]
18557 [FOLDED]
18558 [EXCERPT]
18559 [FOLDED]
18560 "
18561 });
18562 cx.simulate_keystroke("down");
18563 cx.assert_excerpts_with_selections(indoc! {"
18564 [EXCERPT]
18565 [FOLDED]
18566 [EXCERPT]
18567 ˇa1
18568 b1
18569 [EXCERPT]
18570 [FOLDED]
18571 [EXCERPT]
18572 [FOLDED]
18573 "
18574 });
18575 cx.simulate_keystroke("down");
18576 cx.assert_excerpts_with_selections(indoc! {"
18577 [EXCERPT]
18578 [FOLDED]
18579 [EXCERPT]
18580 a1
18581 ˇb1
18582 [EXCERPT]
18583 [FOLDED]
18584 [EXCERPT]
18585 [FOLDED]
18586 "
18587 });
18588 cx.simulate_keystroke("down");
18589 cx.assert_excerpts_with_selections(indoc! {"
18590 [EXCERPT]
18591 [FOLDED]
18592 [EXCERPT]
18593 a1
18594 b1
18595 ˇ[EXCERPT]
18596 [FOLDED]
18597 [EXCERPT]
18598 [FOLDED]
18599 "
18600 });
18601 cx.simulate_keystroke("down");
18602 cx.assert_excerpts_with_selections(indoc! {"
18603 [EXCERPT]
18604 [FOLDED]
18605 [EXCERPT]
18606 a1
18607 b1
18608 [EXCERPT]
18609 ˇ[FOLDED]
18610 [EXCERPT]
18611 [FOLDED]
18612 "
18613 });
18614 for _ in 0..5 {
18615 cx.simulate_keystroke("down");
18616 cx.assert_excerpts_with_selections(indoc! {"
18617 [EXCERPT]
18618 [FOLDED]
18619 [EXCERPT]
18620 a1
18621 b1
18622 [EXCERPT]
18623 [FOLDED]
18624 [EXCERPT]
18625 ˇ[FOLDED]
18626 "
18627 });
18628 }
18629
18630 cx.simulate_keystroke("up");
18631 cx.assert_excerpts_with_selections(indoc! {"
18632 [EXCERPT]
18633 [FOLDED]
18634 [EXCERPT]
18635 a1
18636 b1
18637 [EXCERPT]
18638 ˇ[FOLDED]
18639 [EXCERPT]
18640 [FOLDED]
18641 "
18642 });
18643 cx.simulate_keystroke("up");
18644 cx.assert_excerpts_with_selections(indoc! {"
18645 [EXCERPT]
18646 [FOLDED]
18647 [EXCERPT]
18648 a1
18649 b1
18650 ˇ[EXCERPT]
18651 [FOLDED]
18652 [EXCERPT]
18653 [FOLDED]
18654 "
18655 });
18656 cx.simulate_keystroke("up");
18657 cx.assert_excerpts_with_selections(indoc! {"
18658 [EXCERPT]
18659 [FOLDED]
18660 [EXCERPT]
18661 a1
18662 ˇb1
18663 [EXCERPT]
18664 [FOLDED]
18665 [EXCERPT]
18666 [FOLDED]
18667 "
18668 });
18669 cx.simulate_keystroke("up");
18670 cx.assert_excerpts_with_selections(indoc! {"
18671 [EXCERPT]
18672 [FOLDED]
18673 [EXCERPT]
18674 ˇa1
18675 b1
18676 [EXCERPT]
18677 [FOLDED]
18678 [EXCERPT]
18679 [FOLDED]
18680 "
18681 });
18682 for _ in 0..5 {
18683 cx.simulate_keystroke("up");
18684 cx.assert_excerpts_with_selections(indoc! {"
18685 [EXCERPT]
18686 ˇ[FOLDED]
18687 [EXCERPT]
18688 a1
18689 b1
18690 [EXCERPT]
18691 [FOLDED]
18692 [EXCERPT]
18693 [FOLDED]
18694 "
18695 });
18696 }
18697}
18698
18699#[gpui::test]
18700async fn test_inline_completion_text(cx: &mut TestAppContext) {
18701 init_test(cx, |_| {});
18702
18703 // Simple insertion
18704 assert_highlighted_edits(
18705 "Hello, world!",
18706 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18707 true,
18708 cx,
18709 |highlighted_edits, cx| {
18710 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18711 assert_eq!(highlighted_edits.highlights.len(), 1);
18712 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18713 assert_eq!(
18714 highlighted_edits.highlights[0].1.background_color,
18715 Some(cx.theme().status().created_background)
18716 );
18717 },
18718 )
18719 .await;
18720
18721 // Replacement
18722 assert_highlighted_edits(
18723 "This is a test.",
18724 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18725 false,
18726 cx,
18727 |highlighted_edits, cx| {
18728 assert_eq!(highlighted_edits.text, "That is a test.");
18729 assert_eq!(highlighted_edits.highlights.len(), 1);
18730 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18731 assert_eq!(
18732 highlighted_edits.highlights[0].1.background_color,
18733 Some(cx.theme().status().created_background)
18734 );
18735 },
18736 )
18737 .await;
18738
18739 // Multiple edits
18740 assert_highlighted_edits(
18741 "Hello, world!",
18742 vec![
18743 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18744 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18745 ],
18746 false,
18747 cx,
18748 |highlighted_edits, cx| {
18749 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18750 assert_eq!(highlighted_edits.highlights.len(), 2);
18751 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18752 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18753 assert_eq!(
18754 highlighted_edits.highlights[0].1.background_color,
18755 Some(cx.theme().status().created_background)
18756 );
18757 assert_eq!(
18758 highlighted_edits.highlights[1].1.background_color,
18759 Some(cx.theme().status().created_background)
18760 );
18761 },
18762 )
18763 .await;
18764
18765 // Multiple lines with edits
18766 assert_highlighted_edits(
18767 "First line\nSecond line\nThird line\nFourth line",
18768 vec![
18769 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18770 (
18771 Point::new(2, 0)..Point::new(2, 10),
18772 "New third line".to_string(),
18773 ),
18774 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18775 ],
18776 false,
18777 cx,
18778 |highlighted_edits, cx| {
18779 assert_eq!(
18780 highlighted_edits.text,
18781 "Second modified\nNew third line\nFourth updated line"
18782 );
18783 assert_eq!(highlighted_edits.highlights.len(), 3);
18784 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18785 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18786 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18787 for highlight in &highlighted_edits.highlights {
18788 assert_eq!(
18789 highlight.1.background_color,
18790 Some(cx.theme().status().created_background)
18791 );
18792 }
18793 },
18794 )
18795 .await;
18796}
18797
18798#[gpui::test]
18799async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18800 init_test(cx, |_| {});
18801
18802 // Deletion
18803 assert_highlighted_edits(
18804 "Hello, world!",
18805 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18806 true,
18807 cx,
18808 |highlighted_edits, cx| {
18809 assert_eq!(highlighted_edits.text, "Hello, world!");
18810 assert_eq!(highlighted_edits.highlights.len(), 1);
18811 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18812 assert_eq!(
18813 highlighted_edits.highlights[0].1.background_color,
18814 Some(cx.theme().status().deleted_background)
18815 );
18816 },
18817 )
18818 .await;
18819
18820 // Insertion
18821 assert_highlighted_edits(
18822 "Hello, world!",
18823 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18824 true,
18825 cx,
18826 |highlighted_edits, cx| {
18827 assert_eq!(highlighted_edits.highlights.len(), 1);
18828 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18829 assert_eq!(
18830 highlighted_edits.highlights[0].1.background_color,
18831 Some(cx.theme().status().created_background)
18832 );
18833 },
18834 )
18835 .await;
18836}
18837
18838async fn assert_highlighted_edits(
18839 text: &str,
18840 edits: Vec<(Range<Point>, String)>,
18841 include_deletions: bool,
18842 cx: &mut TestAppContext,
18843 assertion_fn: impl Fn(HighlightedText, &App),
18844) {
18845 let window = cx.add_window(|window, cx| {
18846 let buffer = MultiBuffer::build_simple(text, cx);
18847 Editor::new(EditorMode::full(), buffer, None, window, cx)
18848 });
18849 let cx = &mut VisualTestContext::from_window(*window, cx);
18850
18851 let (buffer, snapshot) = window
18852 .update(cx, |editor, _window, cx| {
18853 (
18854 editor.buffer().clone(),
18855 editor.buffer().read(cx).snapshot(cx),
18856 )
18857 })
18858 .unwrap();
18859
18860 let edits = edits
18861 .into_iter()
18862 .map(|(range, edit)| {
18863 (
18864 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18865 edit,
18866 )
18867 })
18868 .collect::<Vec<_>>();
18869
18870 let text_anchor_edits = edits
18871 .clone()
18872 .into_iter()
18873 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18874 .collect::<Vec<_>>();
18875
18876 let edit_preview = window
18877 .update(cx, |_, _window, cx| {
18878 buffer
18879 .read(cx)
18880 .as_singleton()
18881 .unwrap()
18882 .read(cx)
18883 .preview_edits(text_anchor_edits.into(), cx)
18884 })
18885 .unwrap()
18886 .await;
18887
18888 cx.update(|_window, cx| {
18889 let highlighted_edits = inline_completion_edit_text(
18890 &snapshot.as_singleton().unwrap().2,
18891 &edits,
18892 &edit_preview,
18893 include_deletions,
18894 cx,
18895 );
18896 assertion_fn(highlighted_edits, cx)
18897 });
18898}
18899
18900#[track_caller]
18901fn assert_breakpoint(
18902 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18903 path: &Arc<Path>,
18904 expected: Vec<(u32, Breakpoint)>,
18905) {
18906 if expected.len() == 0usize {
18907 assert!(!breakpoints.contains_key(path), "{}", path.display());
18908 } else {
18909 let mut breakpoint = breakpoints
18910 .get(path)
18911 .unwrap()
18912 .into_iter()
18913 .map(|breakpoint| {
18914 (
18915 breakpoint.row,
18916 Breakpoint {
18917 message: breakpoint.message.clone(),
18918 state: breakpoint.state,
18919 condition: breakpoint.condition.clone(),
18920 hit_condition: breakpoint.hit_condition.clone(),
18921 },
18922 )
18923 })
18924 .collect::<Vec<_>>();
18925
18926 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18927
18928 assert_eq!(expected, breakpoint);
18929 }
18930}
18931
18932fn add_log_breakpoint_at_cursor(
18933 editor: &mut Editor,
18934 log_message: &str,
18935 window: &mut Window,
18936 cx: &mut Context<Editor>,
18937) {
18938 let (anchor, bp) = editor
18939 .breakpoints_at_cursors(window, cx)
18940 .first()
18941 .and_then(|(anchor, bp)| {
18942 if let Some(bp) = bp {
18943 Some((*anchor, bp.clone()))
18944 } else {
18945 None
18946 }
18947 })
18948 .unwrap_or_else(|| {
18949 let cursor_position: Point = editor.selections.newest(cx).head();
18950
18951 let breakpoint_position = editor
18952 .snapshot(window, cx)
18953 .display_snapshot
18954 .buffer_snapshot
18955 .anchor_before(Point::new(cursor_position.row, 0));
18956
18957 (breakpoint_position, Breakpoint::new_log(&log_message))
18958 });
18959
18960 editor.edit_breakpoint_at_anchor(
18961 anchor,
18962 bp,
18963 BreakpointEditAction::EditLogMessage(log_message.into()),
18964 cx,
18965 );
18966}
18967
18968#[gpui::test]
18969async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18970 init_test(cx, |_| {});
18971
18972 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18973 let fs = FakeFs::new(cx.executor());
18974 fs.insert_tree(
18975 path!("/a"),
18976 json!({
18977 "main.rs": sample_text,
18978 }),
18979 )
18980 .await;
18981 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18982 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18983 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18984
18985 let fs = FakeFs::new(cx.executor());
18986 fs.insert_tree(
18987 path!("/a"),
18988 json!({
18989 "main.rs": sample_text,
18990 }),
18991 )
18992 .await;
18993 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18994 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18995 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18996 let worktree_id = workspace
18997 .update(cx, |workspace, _window, cx| {
18998 workspace.project().update(cx, |project, cx| {
18999 project.worktrees(cx).next().unwrap().read(cx).id()
19000 })
19001 })
19002 .unwrap();
19003
19004 let buffer = project
19005 .update(cx, |project, cx| {
19006 project.open_buffer((worktree_id, "main.rs"), cx)
19007 })
19008 .await
19009 .unwrap();
19010
19011 let (editor, cx) = cx.add_window_view(|window, cx| {
19012 Editor::new(
19013 EditorMode::full(),
19014 MultiBuffer::build_from_buffer(buffer, cx),
19015 Some(project.clone()),
19016 window,
19017 cx,
19018 )
19019 });
19020
19021 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19022 let abs_path = project.read_with(cx, |project, cx| {
19023 project
19024 .absolute_path(&project_path, cx)
19025 .map(|path_buf| Arc::from(path_buf.to_owned()))
19026 .unwrap()
19027 });
19028
19029 // assert we can add breakpoint on the first line
19030 editor.update_in(cx, |editor, window, cx| {
19031 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19032 editor.move_to_end(&MoveToEnd, window, cx);
19033 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19034 });
19035
19036 let breakpoints = editor.update(cx, |editor, cx| {
19037 editor
19038 .breakpoint_store()
19039 .as_ref()
19040 .unwrap()
19041 .read(cx)
19042 .all_source_breakpoints(cx)
19043 .clone()
19044 });
19045
19046 assert_eq!(1, breakpoints.len());
19047 assert_breakpoint(
19048 &breakpoints,
19049 &abs_path,
19050 vec![
19051 (0, Breakpoint::new_standard()),
19052 (3, Breakpoint::new_standard()),
19053 ],
19054 );
19055
19056 editor.update_in(cx, |editor, window, cx| {
19057 editor.move_to_beginning(&MoveToBeginning, window, cx);
19058 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19059 });
19060
19061 let breakpoints = editor.update(cx, |editor, cx| {
19062 editor
19063 .breakpoint_store()
19064 .as_ref()
19065 .unwrap()
19066 .read(cx)
19067 .all_source_breakpoints(cx)
19068 .clone()
19069 });
19070
19071 assert_eq!(1, breakpoints.len());
19072 assert_breakpoint(
19073 &breakpoints,
19074 &abs_path,
19075 vec![(3, Breakpoint::new_standard())],
19076 );
19077
19078 editor.update_in(cx, |editor, window, cx| {
19079 editor.move_to_end(&MoveToEnd, window, cx);
19080 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19081 });
19082
19083 let breakpoints = editor.update(cx, |editor, cx| {
19084 editor
19085 .breakpoint_store()
19086 .as_ref()
19087 .unwrap()
19088 .read(cx)
19089 .all_source_breakpoints(cx)
19090 .clone()
19091 });
19092
19093 assert_eq!(0, breakpoints.len());
19094 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19095}
19096
19097#[gpui::test]
19098async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19099 init_test(cx, |_| {});
19100
19101 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19102
19103 let fs = FakeFs::new(cx.executor());
19104 fs.insert_tree(
19105 path!("/a"),
19106 json!({
19107 "main.rs": sample_text,
19108 }),
19109 )
19110 .await;
19111 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19112 let (workspace, cx) =
19113 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19114
19115 let worktree_id = workspace.update(cx, |workspace, cx| {
19116 workspace.project().update(cx, |project, cx| {
19117 project.worktrees(cx).next().unwrap().read(cx).id()
19118 })
19119 });
19120
19121 let buffer = project
19122 .update(cx, |project, cx| {
19123 project.open_buffer((worktree_id, "main.rs"), cx)
19124 })
19125 .await
19126 .unwrap();
19127
19128 let (editor, cx) = cx.add_window_view(|window, cx| {
19129 Editor::new(
19130 EditorMode::full(),
19131 MultiBuffer::build_from_buffer(buffer, cx),
19132 Some(project.clone()),
19133 window,
19134 cx,
19135 )
19136 });
19137
19138 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19139 let abs_path = project.read_with(cx, |project, cx| {
19140 project
19141 .absolute_path(&project_path, cx)
19142 .map(|path_buf| Arc::from(path_buf.to_owned()))
19143 .unwrap()
19144 });
19145
19146 editor.update_in(cx, |editor, window, cx| {
19147 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19148 });
19149
19150 let breakpoints = editor.update(cx, |editor, cx| {
19151 editor
19152 .breakpoint_store()
19153 .as_ref()
19154 .unwrap()
19155 .read(cx)
19156 .all_source_breakpoints(cx)
19157 .clone()
19158 });
19159
19160 assert_breakpoint(
19161 &breakpoints,
19162 &abs_path,
19163 vec![(0, Breakpoint::new_log("hello world"))],
19164 );
19165
19166 // Removing a log message from a log breakpoint should remove it
19167 editor.update_in(cx, |editor, window, cx| {
19168 add_log_breakpoint_at_cursor(editor, "", window, cx);
19169 });
19170
19171 let breakpoints = editor.update(cx, |editor, cx| {
19172 editor
19173 .breakpoint_store()
19174 .as_ref()
19175 .unwrap()
19176 .read(cx)
19177 .all_source_breakpoints(cx)
19178 .clone()
19179 });
19180
19181 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19182
19183 editor.update_in(cx, |editor, window, cx| {
19184 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19185 editor.move_to_end(&MoveToEnd, window, cx);
19186 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19187 // Not adding a log message to a standard breakpoint shouldn't remove it
19188 add_log_breakpoint_at_cursor(editor, "", window, cx);
19189 });
19190
19191 let breakpoints = editor.update(cx, |editor, cx| {
19192 editor
19193 .breakpoint_store()
19194 .as_ref()
19195 .unwrap()
19196 .read(cx)
19197 .all_source_breakpoints(cx)
19198 .clone()
19199 });
19200
19201 assert_breakpoint(
19202 &breakpoints,
19203 &abs_path,
19204 vec![
19205 (0, Breakpoint::new_standard()),
19206 (3, Breakpoint::new_standard()),
19207 ],
19208 );
19209
19210 editor.update_in(cx, |editor, window, cx| {
19211 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19212 });
19213
19214 let breakpoints = editor.update(cx, |editor, cx| {
19215 editor
19216 .breakpoint_store()
19217 .as_ref()
19218 .unwrap()
19219 .read(cx)
19220 .all_source_breakpoints(cx)
19221 .clone()
19222 });
19223
19224 assert_breakpoint(
19225 &breakpoints,
19226 &abs_path,
19227 vec![
19228 (0, Breakpoint::new_standard()),
19229 (3, Breakpoint::new_log("hello world")),
19230 ],
19231 );
19232
19233 editor.update_in(cx, |editor, window, cx| {
19234 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19235 });
19236
19237 let breakpoints = editor.update(cx, |editor, cx| {
19238 editor
19239 .breakpoint_store()
19240 .as_ref()
19241 .unwrap()
19242 .read(cx)
19243 .all_source_breakpoints(cx)
19244 .clone()
19245 });
19246
19247 assert_breakpoint(
19248 &breakpoints,
19249 &abs_path,
19250 vec![
19251 (0, Breakpoint::new_standard()),
19252 (3, Breakpoint::new_log("hello Earth!!")),
19253 ],
19254 );
19255}
19256
19257/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19258/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19259/// or when breakpoints were placed out of order. This tests for a regression too
19260#[gpui::test]
19261async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19262 init_test(cx, |_| {});
19263
19264 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19265 let fs = FakeFs::new(cx.executor());
19266 fs.insert_tree(
19267 path!("/a"),
19268 json!({
19269 "main.rs": sample_text,
19270 }),
19271 )
19272 .await;
19273 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19274 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19275 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19276
19277 let fs = FakeFs::new(cx.executor());
19278 fs.insert_tree(
19279 path!("/a"),
19280 json!({
19281 "main.rs": sample_text,
19282 }),
19283 )
19284 .await;
19285 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19286 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19287 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19288 let worktree_id = workspace
19289 .update(cx, |workspace, _window, cx| {
19290 workspace.project().update(cx, |project, cx| {
19291 project.worktrees(cx).next().unwrap().read(cx).id()
19292 })
19293 })
19294 .unwrap();
19295
19296 let buffer = project
19297 .update(cx, |project, cx| {
19298 project.open_buffer((worktree_id, "main.rs"), cx)
19299 })
19300 .await
19301 .unwrap();
19302
19303 let (editor, cx) = cx.add_window_view(|window, cx| {
19304 Editor::new(
19305 EditorMode::full(),
19306 MultiBuffer::build_from_buffer(buffer, cx),
19307 Some(project.clone()),
19308 window,
19309 cx,
19310 )
19311 });
19312
19313 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19314 let abs_path = project.read_with(cx, |project, cx| {
19315 project
19316 .absolute_path(&project_path, cx)
19317 .map(|path_buf| Arc::from(path_buf.to_owned()))
19318 .unwrap()
19319 });
19320
19321 // assert we can add breakpoint on the first line
19322 editor.update_in(cx, |editor, window, cx| {
19323 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19324 editor.move_to_end(&MoveToEnd, window, cx);
19325 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19326 editor.move_up(&MoveUp, window, cx);
19327 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19328 });
19329
19330 let breakpoints = editor.update(cx, |editor, cx| {
19331 editor
19332 .breakpoint_store()
19333 .as_ref()
19334 .unwrap()
19335 .read(cx)
19336 .all_source_breakpoints(cx)
19337 .clone()
19338 });
19339
19340 assert_eq!(1, breakpoints.len());
19341 assert_breakpoint(
19342 &breakpoints,
19343 &abs_path,
19344 vec![
19345 (0, Breakpoint::new_standard()),
19346 (2, Breakpoint::new_standard()),
19347 (3, Breakpoint::new_standard()),
19348 ],
19349 );
19350
19351 editor.update_in(cx, |editor, window, cx| {
19352 editor.move_to_beginning(&MoveToBeginning, window, cx);
19353 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19354 editor.move_to_end(&MoveToEnd, window, cx);
19355 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19356 // Disabling a breakpoint that doesn't exist should do nothing
19357 editor.move_up(&MoveUp, window, cx);
19358 editor.move_up(&MoveUp, window, cx);
19359 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19360 });
19361
19362 let breakpoints = editor.update(cx, |editor, cx| {
19363 editor
19364 .breakpoint_store()
19365 .as_ref()
19366 .unwrap()
19367 .read(cx)
19368 .all_source_breakpoints(cx)
19369 .clone()
19370 });
19371
19372 let disable_breakpoint = {
19373 let mut bp = Breakpoint::new_standard();
19374 bp.state = BreakpointState::Disabled;
19375 bp
19376 };
19377
19378 assert_eq!(1, breakpoints.len());
19379 assert_breakpoint(
19380 &breakpoints,
19381 &abs_path,
19382 vec![
19383 (0, disable_breakpoint.clone()),
19384 (2, Breakpoint::new_standard()),
19385 (3, disable_breakpoint.clone()),
19386 ],
19387 );
19388
19389 editor.update_in(cx, |editor, window, cx| {
19390 editor.move_to_beginning(&MoveToBeginning, window, cx);
19391 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19392 editor.move_to_end(&MoveToEnd, window, cx);
19393 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19394 editor.move_up(&MoveUp, window, cx);
19395 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19396 });
19397
19398 let breakpoints = editor.update(cx, |editor, cx| {
19399 editor
19400 .breakpoint_store()
19401 .as_ref()
19402 .unwrap()
19403 .read(cx)
19404 .all_source_breakpoints(cx)
19405 .clone()
19406 });
19407
19408 assert_eq!(1, breakpoints.len());
19409 assert_breakpoint(
19410 &breakpoints,
19411 &abs_path,
19412 vec![
19413 (0, Breakpoint::new_standard()),
19414 (2, disable_breakpoint),
19415 (3, Breakpoint::new_standard()),
19416 ],
19417 );
19418}
19419
19420#[gpui::test]
19421async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19422 init_test(cx, |_| {});
19423 let capabilities = lsp::ServerCapabilities {
19424 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19425 prepare_provider: Some(true),
19426 work_done_progress_options: Default::default(),
19427 })),
19428 ..Default::default()
19429 };
19430 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19431
19432 cx.set_state(indoc! {"
19433 struct Fˇoo {}
19434 "});
19435
19436 cx.update_editor(|editor, _, cx| {
19437 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19438 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19439 editor.highlight_background::<DocumentHighlightRead>(
19440 &[highlight_range],
19441 |c| c.editor_document_highlight_read_background,
19442 cx,
19443 );
19444 });
19445
19446 let mut prepare_rename_handler = cx
19447 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19448 move |_, _, _| async move {
19449 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19450 start: lsp::Position {
19451 line: 0,
19452 character: 7,
19453 },
19454 end: lsp::Position {
19455 line: 0,
19456 character: 10,
19457 },
19458 })))
19459 },
19460 );
19461 let prepare_rename_task = cx
19462 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19463 .expect("Prepare rename was not started");
19464 prepare_rename_handler.next().await.unwrap();
19465 prepare_rename_task.await.expect("Prepare rename failed");
19466
19467 let mut rename_handler =
19468 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19469 let edit = lsp::TextEdit {
19470 range: lsp::Range {
19471 start: lsp::Position {
19472 line: 0,
19473 character: 7,
19474 },
19475 end: lsp::Position {
19476 line: 0,
19477 character: 10,
19478 },
19479 },
19480 new_text: "FooRenamed".to_string(),
19481 };
19482 Ok(Some(lsp::WorkspaceEdit::new(
19483 // Specify the same edit twice
19484 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19485 )))
19486 });
19487 let rename_task = cx
19488 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19489 .expect("Confirm rename was not started");
19490 rename_handler.next().await.unwrap();
19491 rename_task.await.expect("Confirm rename failed");
19492 cx.run_until_parked();
19493
19494 // Despite two edits, only one is actually applied as those are identical
19495 cx.assert_editor_state(indoc! {"
19496 struct FooRenamedˇ {}
19497 "});
19498}
19499
19500#[gpui::test]
19501async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19502 init_test(cx, |_| {});
19503 // These capabilities indicate that the server does not support prepare rename.
19504 let capabilities = lsp::ServerCapabilities {
19505 rename_provider: Some(lsp::OneOf::Left(true)),
19506 ..Default::default()
19507 };
19508 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19509
19510 cx.set_state(indoc! {"
19511 struct Fˇoo {}
19512 "});
19513
19514 cx.update_editor(|editor, _window, cx| {
19515 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19516 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19517 editor.highlight_background::<DocumentHighlightRead>(
19518 &[highlight_range],
19519 |c| c.editor_document_highlight_read_background,
19520 cx,
19521 );
19522 });
19523
19524 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19525 .expect("Prepare rename was not started")
19526 .await
19527 .expect("Prepare rename failed");
19528
19529 let mut rename_handler =
19530 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19531 let edit = lsp::TextEdit {
19532 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 new_text: "FooRenamed".to_string(),
19543 };
19544 Ok(Some(lsp::WorkspaceEdit::new(
19545 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19546 )))
19547 });
19548 let rename_task = cx
19549 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19550 .expect("Confirm rename was not started");
19551 rename_handler.next().await.unwrap();
19552 rename_task.await.expect("Confirm rename failed");
19553 cx.run_until_parked();
19554
19555 // Correct range is renamed, as `surrounding_word` is used to find it.
19556 cx.assert_editor_state(indoc! {"
19557 struct FooRenamedˇ {}
19558 "});
19559}
19560
19561#[gpui::test]
19562async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19563 init_test(cx, |_| {});
19564 let mut cx = EditorTestContext::new(cx).await;
19565
19566 let language = Arc::new(
19567 Language::new(
19568 LanguageConfig::default(),
19569 Some(tree_sitter_html::LANGUAGE.into()),
19570 )
19571 .with_brackets_query(
19572 r#"
19573 ("<" @open "/>" @close)
19574 ("</" @open ">" @close)
19575 ("<" @open ">" @close)
19576 ("\"" @open "\"" @close)
19577 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19578 "#,
19579 )
19580 .unwrap(),
19581 );
19582 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19583
19584 cx.set_state(indoc! {"
19585 <span>ˇ</span>
19586 "});
19587 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19588 cx.assert_editor_state(indoc! {"
19589 <span>
19590 ˇ
19591 </span>
19592 "});
19593
19594 cx.set_state(indoc! {"
19595 <span><span></span>ˇ</span>
19596 "});
19597 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19598 cx.assert_editor_state(indoc! {"
19599 <span><span></span>
19600 ˇ</span>
19601 "});
19602
19603 cx.set_state(indoc! {"
19604 <span>ˇ
19605 </span>
19606 "});
19607 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19608 cx.assert_editor_state(indoc! {"
19609 <span>
19610 ˇ
19611 </span>
19612 "});
19613}
19614
19615#[gpui::test(iterations = 10)]
19616async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19617 init_test(cx, |_| {});
19618
19619 let fs = FakeFs::new(cx.executor());
19620 fs.insert_tree(
19621 path!("/dir"),
19622 json!({
19623 "a.ts": "a",
19624 }),
19625 )
19626 .await;
19627
19628 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19629 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19630 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19631
19632 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19633 language_registry.add(Arc::new(Language::new(
19634 LanguageConfig {
19635 name: "TypeScript".into(),
19636 matcher: LanguageMatcher {
19637 path_suffixes: vec!["ts".to_string()],
19638 ..Default::default()
19639 },
19640 ..Default::default()
19641 },
19642 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19643 )));
19644 let mut fake_language_servers = language_registry.register_fake_lsp(
19645 "TypeScript",
19646 FakeLspAdapter {
19647 capabilities: lsp::ServerCapabilities {
19648 code_lens_provider: Some(lsp::CodeLensOptions {
19649 resolve_provider: Some(true),
19650 }),
19651 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19652 commands: vec!["_the/command".to_string()],
19653 ..lsp::ExecuteCommandOptions::default()
19654 }),
19655 ..lsp::ServerCapabilities::default()
19656 },
19657 ..FakeLspAdapter::default()
19658 },
19659 );
19660
19661 let (buffer, _handle) = project
19662 .update(cx, |p, cx| {
19663 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19664 })
19665 .await
19666 .unwrap();
19667 cx.executor().run_until_parked();
19668
19669 let fake_server = fake_language_servers.next().await.unwrap();
19670
19671 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19672 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19673 drop(buffer_snapshot);
19674 let actions = cx
19675 .update_window(*workspace, |_, window, cx| {
19676 project.code_actions(&buffer, anchor..anchor, window, cx)
19677 })
19678 .unwrap();
19679
19680 fake_server
19681 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19682 Ok(Some(vec![
19683 lsp::CodeLens {
19684 range: lsp::Range::default(),
19685 command: Some(lsp::Command {
19686 title: "Code lens command".to_owned(),
19687 command: "_the/command".to_owned(),
19688 arguments: None,
19689 }),
19690 data: None,
19691 },
19692 lsp::CodeLens {
19693 range: lsp::Range::default(),
19694 command: Some(lsp::Command {
19695 title: "Command not in capabilities".to_owned(),
19696 command: "not in capabilities".to_owned(),
19697 arguments: None,
19698 }),
19699 data: None,
19700 },
19701 lsp::CodeLens {
19702 range: lsp::Range {
19703 start: lsp::Position {
19704 line: 1,
19705 character: 1,
19706 },
19707 end: lsp::Position {
19708 line: 1,
19709 character: 1,
19710 },
19711 },
19712 command: Some(lsp::Command {
19713 title: "Command not in range".to_owned(),
19714 command: "_the/command".to_owned(),
19715 arguments: None,
19716 }),
19717 data: None,
19718 },
19719 ]))
19720 })
19721 .next()
19722 .await;
19723
19724 let actions = actions.await.unwrap();
19725 assert_eq!(
19726 actions.len(),
19727 1,
19728 "Should have only one valid action for the 0..0 range"
19729 );
19730 let action = actions[0].clone();
19731 let apply = project.update(cx, |project, cx| {
19732 project.apply_code_action(buffer.clone(), action, true, cx)
19733 });
19734
19735 // Resolving the code action does not populate its edits. In absence of
19736 // edits, we must execute the given command.
19737 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19738 |mut lens, _| async move {
19739 let lens_command = lens.command.as_mut().expect("should have a command");
19740 assert_eq!(lens_command.title, "Code lens command");
19741 lens_command.arguments = Some(vec![json!("the-argument")]);
19742 Ok(lens)
19743 },
19744 );
19745
19746 // While executing the command, the language server sends the editor
19747 // a `workspaceEdit` request.
19748 fake_server
19749 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19750 let fake = fake_server.clone();
19751 move |params, _| {
19752 assert_eq!(params.command, "_the/command");
19753 let fake = fake.clone();
19754 async move {
19755 fake.server
19756 .request::<lsp::request::ApplyWorkspaceEdit>(
19757 lsp::ApplyWorkspaceEditParams {
19758 label: None,
19759 edit: lsp::WorkspaceEdit {
19760 changes: Some(
19761 [(
19762 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19763 vec![lsp::TextEdit {
19764 range: lsp::Range::new(
19765 lsp::Position::new(0, 0),
19766 lsp::Position::new(0, 0),
19767 ),
19768 new_text: "X".into(),
19769 }],
19770 )]
19771 .into_iter()
19772 .collect(),
19773 ),
19774 ..Default::default()
19775 },
19776 },
19777 )
19778 .await
19779 .into_response()
19780 .unwrap();
19781 Ok(Some(json!(null)))
19782 }
19783 }
19784 })
19785 .next()
19786 .await;
19787
19788 // Applying the code lens command returns a project transaction containing the edits
19789 // sent by the language server in its `workspaceEdit` request.
19790 let transaction = apply.await.unwrap();
19791 assert!(transaction.0.contains_key(&buffer));
19792 buffer.update(cx, |buffer, cx| {
19793 assert_eq!(buffer.text(), "Xa");
19794 buffer.undo(cx);
19795 assert_eq!(buffer.text(), "a");
19796 });
19797}
19798
19799#[gpui::test]
19800async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19801 init_test(cx, |_| {});
19802
19803 let fs = FakeFs::new(cx.executor());
19804 let main_text = r#"fn main() {
19805println!("1");
19806println!("2");
19807println!("3");
19808println!("4");
19809println!("5");
19810}"#;
19811 let lib_text = "mod foo {}";
19812 fs.insert_tree(
19813 path!("/a"),
19814 json!({
19815 "lib.rs": lib_text,
19816 "main.rs": main_text,
19817 }),
19818 )
19819 .await;
19820
19821 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19822 let (workspace, cx) =
19823 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19824 let worktree_id = workspace.update(cx, |workspace, cx| {
19825 workspace.project().update(cx, |project, cx| {
19826 project.worktrees(cx).next().unwrap().read(cx).id()
19827 })
19828 });
19829
19830 let expected_ranges = vec![
19831 Point::new(0, 0)..Point::new(0, 0),
19832 Point::new(1, 0)..Point::new(1, 1),
19833 Point::new(2, 0)..Point::new(2, 2),
19834 Point::new(3, 0)..Point::new(3, 3),
19835 ];
19836
19837 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19838 let editor_1 = workspace
19839 .update_in(cx, |workspace, window, cx| {
19840 workspace.open_path(
19841 (worktree_id, "main.rs"),
19842 Some(pane_1.downgrade()),
19843 true,
19844 window,
19845 cx,
19846 )
19847 })
19848 .unwrap()
19849 .await
19850 .downcast::<Editor>()
19851 .unwrap();
19852 pane_1.update(cx, |pane, cx| {
19853 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19854 open_editor.update(cx, |editor, cx| {
19855 assert_eq!(
19856 editor.display_text(cx),
19857 main_text,
19858 "Original main.rs text on initial open",
19859 );
19860 assert_eq!(
19861 editor
19862 .selections
19863 .all::<Point>(cx)
19864 .into_iter()
19865 .map(|s| s.range())
19866 .collect::<Vec<_>>(),
19867 vec![Point::zero()..Point::zero()],
19868 "Default selections on initial open",
19869 );
19870 })
19871 });
19872 editor_1.update_in(cx, |editor, window, cx| {
19873 editor.change_selections(None, window, cx, |s| {
19874 s.select_ranges(expected_ranges.clone());
19875 });
19876 });
19877
19878 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19879 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19880 });
19881 let editor_2 = workspace
19882 .update_in(cx, |workspace, window, cx| {
19883 workspace.open_path(
19884 (worktree_id, "main.rs"),
19885 Some(pane_2.downgrade()),
19886 true,
19887 window,
19888 cx,
19889 )
19890 })
19891 .unwrap()
19892 .await
19893 .downcast::<Editor>()
19894 .unwrap();
19895 pane_2.update(cx, |pane, cx| {
19896 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19897 open_editor.update(cx, |editor, cx| {
19898 assert_eq!(
19899 editor.display_text(cx),
19900 main_text,
19901 "Original main.rs text on initial open in another panel",
19902 );
19903 assert_eq!(
19904 editor
19905 .selections
19906 .all::<Point>(cx)
19907 .into_iter()
19908 .map(|s| s.range())
19909 .collect::<Vec<_>>(),
19910 vec![Point::zero()..Point::zero()],
19911 "Default selections on initial open in another panel",
19912 );
19913 })
19914 });
19915
19916 editor_2.update_in(cx, |editor, window, cx| {
19917 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19918 });
19919
19920 let _other_editor_1 = workspace
19921 .update_in(cx, |workspace, window, cx| {
19922 workspace.open_path(
19923 (worktree_id, "lib.rs"),
19924 Some(pane_1.downgrade()),
19925 true,
19926 window,
19927 cx,
19928 )
19929 })
19930 .unwrap()
19931 .await
19932 .downcast::<Editor>()
19933 .unwrap();
19934 pane_1
19935 .update_in(cx, |pane, window, cx| {
19936 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19937 .unwrap()
19938 })
19939 .await
19940 .unwrap();
19941 drop(editor_1);
19942 pane_1.update(cx, |pane, cx| {
19943 pane.active_item()
19944 .unwrap()
19945 .downcast::<Editor>()
19946 .unwrap()
19947 .update(cx, |editor, cx| {
19948 assert_eq!(
19949 editor.display_text(cx),
19950 lib_text,
19951 "Other file should be open and active",
19952 );
19953 });
19954 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19955 });
19956
19957 let _other_editor_2 = workspace
19958 .update_in(cx, |workspace, window, cx| {
19959 workspace.open_path(
19960 (worktree_id, "lib.rs"),
19961 Some(pane_2.downgrade()),
19962 true,
19963 window,
19964 cx,
19965 )
19966 })
19967 .unwrap()
19968 .await
19969 .downcast::<Editor>()
19970 .unwrap();
19971 pane_2
19972 .update_in(cx, |pane, window, cx| {
19973 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19974 .unwrap()
19975 })
19976 .await
19977 .unwrap();
19978 drop(editor_2);
19979 pane_2.update(cx, |pane, cx| {
19980 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19981 open_editor.update(cx, |editor, cx| {
19982 assert_eq!(
19983 editor.display_text(cx),
19984 lib_text,
19985 "Other file should be open and active in another panel too",
19986 );
19987 });
19988 assert_eq!(
19989 pane.items().count(),
19990 1,
19991 "No other editors should be open in another pane",
19992 );
19993 });
19994
19995 let _editor_1_reopened = workspace
19996 .update_in(cx, |workspace, window, cx| {
19997 workspace.open_path(
19998 (worktree_id, "main.rs"),
19999 Some(pane_1.downgrade()),
20000 true,
20001 window,
20002 cx,
20003 )
20004 })
20005 .unwrap()
20006 .await
20007 .downcast::<Editor>()
20008 .unwrap();
20009 let _editor_2_reopened = workspace
20010 .update_in(cx, |workspace, window, cx| {
20011 workspace.open_path(
20012 (worktree_id, "main.rs"),
20013 Some(pane_2.downgrade()),
20014 true,
20015 window,
20016 cx,
20017 )
20018 })
20019 .unwrap()
20020 .await
20021 .downcast::<Editor>()
20022 .unwrap();
20023 pane_1.update(cx, |pane, cx| {
20024 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20025 open_editor.update(cx, |editor, cx| {
20026 assert_eq!(
20027 editor.display_text(cx),
20028 main_text,
20029 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20030 );
20031 assert_eq!(
20032 editor
20033 .selections
20034 .all::<Point>(cx)
20035 .into_iter()
20036 .map(|s| s.range())
20037 .collect::<Vec<_>>(),
20038 expected_ranges,
20039 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20040 );
20041 })
20042 });
20043 pane_2.update(cx, |pane, cx| {
20044 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20045 open_editor.update(cx, |editor, cx| {
20046 assert_eq!(
20047 editor.display_text(cx),
20048 r#"fn main() {
20049⋯rintln!("1");
20050⋯intln!("2");
20051⋯ntln!("3");
20052println!("4");
20053println!("5");
20054}"#,
20055 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20056 );
20057 assert_eq!(
20058 editor
20059 .selections
20060 .all::<Point>(cx)
20061 .into_iter()
20062 .map(|s| s.range())
20063 .collect::<Vec<_>>(),
20064 vec![Point::zero()..Point::zero()],
20065 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20066 );
20067 })
20068 });
20069}
20070
20071#[gpui::test]
20072async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20073 init_test(cx, |_| {});
20074
20075 let fs = FakeFs::new(cx.executor());
20076 let main_text = r#"fn main() {
20077println!("1");
20078println!("2");
20079println!("3");
20080println!("4");
20081println!("5");
20082}"#;
20083 let lib_text = "mod foo {}";
20084 fs.insert_tree(
20085 path!("/a"),
20086 json!({
20087 "lib.rs": lib_text,
20088 "main.rs": main_text,
20089 }),
20090 )
20091 .await;
20092
20093 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20094 let (workspace, cx) =
20095 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20096 let worktree_id = workspace.update(cx, |workspace, cx| {
20097 workspace.project().update(cx, |project, cx| {
20098 project.worktrees(cx).next().unwrap().read(cx).id()
20099 })
20100 });
20101
20102 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20103 let editor = workspace
20104 .update_in(cx, |workspace, window, cx| {
20105 workspace.open_path(
20106 (worktree_id, "main.rs"),
20107 Some(pane.downgrade()),
20108 true,
20109 window,
20110 cx,
20111 )
20112 })
20113 .unwrap()
20114 .await
20115 .downcast::<Editor>()
20116 .unwrap();
20117 pane.update(cx, |pane, cx| {
20118 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20119 open_editor.update(cx, |editor, cx| {
20120 assert_eq!(
20121 editor.display_text(cx),
20122 main_text,
20123 "Original main.rs text on initial open",
20124 );
20125 })
20126 });
20127 editor.update_in(cx, |editor, window, cx| {
20128 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20129 });
20130
20131 cx.update_global(|store: &mut SettingsStore, cx| {
20132 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20133 s.restore_on_file_reopen = Some(false);
20134 });
20135 });
20136 editor.update_in(cx, |editor, window, cx| {
20137 editor.fold_ranges(
20138 vec![
20139 Point::new(1, 0)..Point::new(1, 1),
20140 Point::new(2, 0)..Point::new(2, 2),
20141 Point::new(3, 0)..Point::new(3, 3),
20142 ],
20143 false,
20144 window,
20145 cx,
20146 );
20147 });
20148 pane.update_in(cx, |pane, window, cx| {
20149 pane.close_all_items(&CloseAllItems::default(), window, cx)
20150 .unwrap()
20151 })
20152 .await
20153 .unwrap();
20154 pane.update(cx, |pane, _| {
20155 assert!(pane.active_item().is_none());
20156 });
20157 cx.update_global(|store: &mut SettingsStore, cx| {
20158 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20159 s.restore_on_file_reopen = Some(true);
20160 });
20161 });
20162
20163 let _editor_reopened = workspace
20164 .update_in(cx, |workspace, window, cx| {
20165 workspace.open_path(
20166 (worktree_id, "main.rs"),
20167 Some(pane.downgrade()),
20168 true,
20169 window,
20170 cx,
20171 )
20172 })
20173 .unwrap()
20174 .await
20175 .downcast::<Editor>()
20176 .unwrap();
20177 pane.update(cx, |pane, cx| {
20178 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20179 open_editor.update(cx, |editor, cx| {
20180 assert_eq!(
20181 editor.display_text(cx),
20182 main_text,
20183 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20184 );
20185 })
20186 });
20187}
20188
20189#[gpui::test]
20190async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20191 struct EmptyModalView {
20192 focus_handle: gpui::FocusHandle,
20193 }
20194 impl EventEmitter<DismissEvent> for EmptyModalView {}
20195 impl Render for EmptyModalView {
20196 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20197 div()
20198 }
20199 }
20200 impl Focusable for EmptyModalView {
20201 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20202 self.focus_handle.clone()
20203 }
20204 }
20205 impl workspace::ModalView for EmptyModalView {}
20206 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20207 EmptyModalView {
20208 focus_handle: cx.focus_handle(),
20209 }
20210 }
20211
20212 init_test(cx, |_| {});
20213
20214 let fs = FakeFs::new(cx.executor());
20215 let project = Project::test(fs, [], cx).await;
20216 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20217 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20218 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20219 let editor = cx.new_window_entity(|window, cx| {
20220 Editor::new(
20221 EditorMode::full(),
20222 buffer,
20223 Some(project.clone()),
20224 window,
20225 cx,
20226 )
20227 });
20228 workspace
20229 .update(cx, |workspace, window, cx| {
20230 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20231 })
20232 .unwrap();
20233 editor.update_in(cx, |editor, window, cx| {
20234 editor.open_context_menu(&OpenContextMenu, window, cx);
20235 assert!(editor.mouse_context_menu.is_some());
20236 });
20237 workspace
20238 .update(cx, |workspace, window, cx| {
20239 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20240 })
20241 .unwrap();
20242 cx.read(|cx| {
20243 assert!(editor.read(cx).mouse_context_menu.is_none());
20244 });
20245}
20246
20247#[gpui::test]
20248async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20249 init_test(cx, |_| {});
20250
20251 let fs = FakeFs::new(cx.executor());
20252 fs.insert_file(path!("/file.html"), Default::default())
20253 .await;
20254
20255 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20256
20257 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20258 let html_language = Arc::new(Language::new(
20259 LanguageConfig {
20260 name: "HTML".into(),
20261 matcher: LanguageMatcher {
20262 path_suffixes: vec!["html".to_string()],
20263 ..LanguageMatcher::default()
20264 },
20265 brackets: BracketPairConfig {
20266 pairs: vec![BracketPair {
20267 start: "<".into(),
20268 end: ">".into(),
20269 close: true,
20270 ..Default::default()
20271 }],
20272 ..Default::default()
20273 },
20274 ..Default::default()
20275 },
20276 Some(tree_sitter_html::LANGUAGE.into()),
20277 ));
20278 language_registry.add(html_language);
20279 let mut fake_servers = language_registry.register_fake_lsp(
20280 "HTML",
20281 FakeLspAdapter {
20282 capabilities: lsp::ServerCapabilities {
20283 completion_provider: Some(lsp::CompletionOptions {
20284 resolve_provider: Some(true),
20285 ..Default::default()
20286 }),
20287 ..Default::default()
20288 },
20289 ..Default::default()
20290 },
20291 );
20292
20293 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20294 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20295
20296 let worktree_id = workspace
20297 .update(cx, |workspace, _window, cx| {
20298 workspace.project().update(cx, |project, cx| {
20299 project.worktrees(cx).next().unwrap().read(cx).id()
20300 })
20301 })
20302 .unwrap();
20303 project
20304 .update(cx, |project, cx| {
20305 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20306 })
20307 .await
20308 .unwrap();
20309 let editor = workspace
20310 .update(cx, |workspace, window, cx| {
20311 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20312 })
20313 .unwrap()
20314 .await
20315 .unwrap()
20316 .downcast::<Editor>()
20317 .unwrap();
20318
20319 let fake_server = fake_servers.next().await.unwrap();
20320 editor.update_in(cx, |editor, window, cx| {
20321 editor.set_text("<ad></ad>", window, cx);
20322 editor.change_selections(None, window, cx, |selections| {
20323 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20324 });
20325 let Some((buffer, _)) = editor
20326 .buffer
20327 .read(cx)
20328 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20329 else {
20330 panic!("Failed to get buffer for selection position");
20331 };
20332 let buffer = buffer.read(cx);
20333 let buffer_id = buffer.remote_id();
20334 let opening_range =
20335 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20336 let closing_range =
20337 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20338 let mut linked_ranges = HashMap::default();
20339 linked_ranges.insert(
20340 buffer_id,
20341 vec![(opening_range.clone(), vec![closing_range.clone()])],
20342 );
20343 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20344 });
20345 let mut completion_handle =
20346 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20347 Ok(Some(lsp::CompletionResponse::Array(vec![
20348 lsp::CompletionItem {
20349 label: "head".to_string(),
20350 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20351 lsp::InsertReplaceEdit {
20352 new_text: "head".to_string(),
20353 insert: lsp::Range::new(
20354 lsp::Position::new(0, 1),
20355 lsp::Position::new(0, 3),
20356 ),
20357 replace: lsp::Range::new(
20358 lsp::Position::new(0, 1),
20359 lsp::Position::new(0, 3),
20360 ),
20361 },
20362 )),
20363 ..Default::default()
20364 },
20365 ])))
20366 });
20367 editor.update_in(cx, |editor, window, cx| {
20368 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20369 });
20370 cx.run_until_parked();
20371 completion_handle.next().await.unwrap();
20372 editor.update(cx, |editor, _| {
20373 assert!(
20374 editor.context_menu_visible(),
20375 "Completion menu should be visible"
20376 );
20377 });
20378 editor.update_in(cx, |editor, window, cx| {
20379 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20380 });
20381 cx.executor().run_until_parked();
20382 editor.update(cx, |editor, cx| {
20383 assert_eq!(editor.text(cx), "<head></head>");
20384 });
20385}
20386
20387#[gpui::test]
20388async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20389 init_test(cx, |_| {});
20390
20391 let fs = FakeFs::new(cx.executor());
20392 fs.insert_tree(
20393 path!("/root"),
20394 json!({
20395 "a": {
20396 "main.rs": "fn main() {}",
20397 },
20398 "foo": {
20399 "bar": {
20400 "external_file.rs": "pub mod external {}",
20401 }
20402 }
20403 }),
20404 )
20405 .await;
20406
20407 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20408 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20409 language_registry.add(rust_lang());
20410 let _fake_servers = language_registry.register_fake_lsp(
20411 "Rust",
20412 FakeLspAdapter {
20413 ..FakeLspAdapter::default()
20414 },
20415 );
20416 let (workspace, cx) =
20417 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20418 let worktree_id = workspace.update(cx, |workspace, cx| {
20419 workspace.project().update(cx, |project, cx| {
20420 project.worktrees(cx).next().unwrap().read(cx).id()
20421 })
20422 });
20423
20424 let assert_language_servers_count =
20425 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20426 project.update(cx, |project, cx| {
20427 let current = project
20428 .lsp_store()
20429 .read(cx)
20430 .as_local()
20431 .unwrap()
20432 .language_servers
20433 .len();
20434 assert_eq!(expected, current, "{context}");
20435 });
20436 };
20437
20438 assert_language_servers_count(
20439 0,
20440 "No servers should be running before any file is open",
20441 cx,
20442 );
20443 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20444 let main_editor = workspace
20445 .update_in(cx, |workspace, window, cx| {
20446 workspace.open_path(
20447 (worktree_id, "main.rs"),
20448 Some(pane.downgrade()),
20449 true,
20450 window,
20451 cx,
20452 )
20453 })
20454 .unwrap()
20455 .await
20456 .downcast::<Editor>()
20457 .unwrap();
20458 pane.update(cx, |pane, cx| {
20459 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20460 open_editor.update(cx, |editor, cx| {
20461 assert_eq!(
20462 editor.display_text(cx),
20463 "fn main() {}",
20464 "Original main.rs text on initial open",
20465 );
20466 });
20467 assert_eq!(open_editor, main_editor);
20468 });
20469 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20470
20471 let external_editor = workspace
20472 .update_in(cx, |workspace, window, cx| {
20473 workspace.open_abs_path(
20474 PathBuf::from("/root/foo/bar/external_file.rs"),
20475 OpenOptions::default(),
20476 window,
20477 cx,
20478 )
20479 })
20480 .await
20481 .expect("opening external file")
20482 .downcast::<Editor>()
20483 .expect("downcasted external file's open element to editor");
20484 pane.update(cx, |pane, cx| {
20485 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20486 open_editor.update(cx, |editor, cx| {
20487 assert_eq!(
20488 editor.display_text(cx),
20489 "pub mod external {}",
20490 "External file is open now",
20491 );
20492 });
20493 assert_eq!(open_editor, external_editor);
20494 });
20495 assert_language_servers_count(
20496 1,
20497 "Second, external, *.rs file should join the existing server",
20498 cx,
20499 );
20500
20501 pane.update_in(cx, |pane, window, cx| {
20502 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20503 })
20504 .unwrap()
20505 .await
20506 .unwrap();
20507 pane.update_in(cx, |pane, window, cx| {
20508 pane.navigate_backward(window, cx);
20509 });
20510 cx.run_until_parked();
20511 pane.update(cx, |pane, cx| {
20512 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20513 open_editor.update(cx, |editor, cx| {
20514 assert_eq!(
20515 editor.display_text(cx),
20516 "pub mod external {}",
20517 "External file is open now",
20518 );
20519 });
20520 });
20521 assert_language_servers_count(
20522 1,
20523 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20524 cx,
20525 );
20526
20527 cx.update(|_, cx| {
20528 workspace::reload(&workspace::Reload::default(), cx);
20529 });
20530 assert_language_servers_count(
20531 1,
20532 "After reloading the worktree with local and external files opened, only one project should be started",
20533 cx,
20534 );
20535}
20536
20537#[gpui::test]
20538async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20539 init_test(cx, |_| {});
20540
20541 let mut cx = EditorTestContext::new(cx).await;
20542 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20543 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20544
20545 // test cursor move to start of each line on tab
20546 // for `if`, `elif`, `else`, `while`, `with` and `for`
20547 cx.set_state(indoc! {"
20548 def main():
20549 ˇ for item in items:
20550 ˇ while item.active:
20551 ˇ if item.value > 10:
20552 ˇ continue
20553 ˇ elif item.value < 0:
20554 ˇ break
20555 ˇ else:
20556 ˇ with item.context() as ctx:
20557 ˇ yield count
20558 ˇ else:
20559 ˇ log('while else')
20560 ˇ else:
20561 ˇ log('for else')
20562 "});
20563 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20564 cx.assert_editor_state(indoc! {"
20565 def main():
20566 ˇfor item in items:
20567 ˇwhile item.active:
20568 ˇif item.value > 10:
20569 ˇcontinue
20570 ˇelif item.value < 0:
20571 ˇbreak
20572 ˇelse:
20573 ˇwith item.context() as ctx:
20574 ˇyield count
20575 ˇelse:
20576 ˇlog('while else')
20577 ˇelse:
20578 ˇlog('for else')
20579 "});
20580 // test relative indent is preserved when tab
20581 // for `if`, `elif`, `else`, `while`, `with` and `for`
20582 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20583 cx.assert_editor_state(indoc! {"
20584 def main():
20585 ˇfor item in items:
20586 ˇwhile item.active:
20587 ˇif item.value > 10:
20588 ˇcontinue
20589 ˇelif item.value < 0:
20590 ˇbreak
20591 ˇelse:
20592 ˇwith item.context() as ctx:
20593 ˇyield count
20594 ˇelse:
20595 ˇlog('while else')
20596 ˇelse:
20597 ˇlog('for else')
20598 "});
20599
20600 // test cursor move to start of each line on tab
20601 // for `try`, `except`, `else`, `finally`, `match` and `def`
20602 cx.set_state(indoc! {"
20603 def main():
20604 ˇ try:
20605 ˇ fetch()
20606 ˇ except ValueError:
20607 ˇ handle_error()
20608 ˇ else:
20609 ˇ match value:
20610 ˇ case _:
20611 ˇ finally:
20612 ˇ def status():
20613 ˇ return 0
20614 "});
20615 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20616 cx.assert_editor_state(indoc! {"
20617 def main():
20618 ˇtry:
20619 ˇfetch()
20620 ˇexcept ValueError:
20621 ˇhandle_error()
20622 ˇelse:
20623 ˇmatch value:
20624 ˇcase _:
20625 ˇfinally:
20626 ˇdef status():
20627 ˇreturn 0
20628 "});
20629 // test relative indent is preserved when tab
20630 // for `try`, `except`, `else`, `finally`, `match` and `def`
20631 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20632 cx.assert_editor_state(indoc! {"
20633 def main():
20634 ˇtry:
20635 ˇfetch()
20636 ˇexcept ValueError:
20637 ˇhandle_error()
20638 ˇelse:
20639 ˇmatch value:
20640 ˇcase _:
20641 ˇfinally:
20642 ˇdef status():
20643 ˇreturn 0
20644 "});
20645}
20646
20647#[gpui::test]
20648async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
20649 init_test(cx, |_| {});
20650
20651 let mut cx = EditorTestContext::new(cx).await;
20652 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20653 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20654
20655 // test `else` auto outdents when typed inside `if` block
20656 cx.set_state(indoc! {"
20657 def main():
20658 if i == 2:
20659 return
20660 ˇ
20661 "});
20662 cx.update_editor(|editor, window, cx| {
20663 editor.handle_input("else:", window, cx);
20664 });
20665 cx.assert_editor_state(indoc! {"
20666 def main():
20667 if i == 2:
20668 return
20669 else:ˇ
20670 "});
20671
20672 // test `except` auto outdents when typed inside `try` block
20673 cx.set_state(indoc! {"
20674 def main():
20675 try:
20676 i = 2
20677 ˇ
20678 "});
20679 cx.update_editor(|editor, window, cx| {
20680 editor.handle_input("except:", window, cx);
20681 });
20682 cx.assert_editor_state(indoc! {"
20683 def main():
20684 try:
20685 i = 2
20686 except:ˇ
20687 "});
20688
20689 // test `else` auto outdents when typed inside `except` block
20690 cx.set_state(indoc! {"
20691 def main():
20692 try:
20693 i = 2
20694 except:
20695 j = 2
20696 ˇ
20697 "});
20698 cx.update_editor(|editor, window, cx| {
20699 editor.handle_input("else:", window, cx);
20700 });
20701 cx.assert_editor_state(indoc! {"
20702 def main():
20703 try:
20704 i = 2
20705 except:
20706 j = 2
20707 else:ˇ
20708 "});
20709
20710 // test `finally` auto outdents when typed inside `else` block
20711 cx.set_state(indoc! {"
20712 def main():
20713 try:
20714 i = 2
20715 except:
20716 j = 2
20717 else:
20718 k = 2
20719 ˇ
20720 "});
20721 cx.update_editor(|editor, window, cx| {
20722 editor.handle_input("finally:", window, cx);
20723 });
20724 cx.assert_editor_state(indoc! {"
20725 def main():
20726 try:
20727 i = 2
20728 except:
20729 j = 2
20730 else:
20731 k = 2
20732 finally:ˇ
20733 "});
20734
20735 // TODO: test `except` auto outdents when typed inside `try` block right after for block
20736 // cx.set_state(indoc! {"
20737 // def main():
20738 // try:
20739 // for i in range(n):
20740 // pass
20741 // ˇ
20742 // "});
20743 // cx.update_editor(|editor, window, cx| {
20744 // editor.handle_input("except:", window, cx);
20745 // });
20746 // cx.assert_editor_state(indoc! {"
20747 // def main():
20748 // try:
20749 // for i in range(n):
20750 // pass
20751 // except:ˇ
20752 // "});
20753
20754 // TODO: test `else` auto outdents when typed inside `except` block right after for block
20755 // cx.set_state(indoc! {"
20756 // def main():
20757 // try:
20758 // i = 2
20759 // except:
20760 // for i in range(n):
20761 // pass
20762 // ˇ
20763 // "});
20764 // cx.update_editor(|editor, window, cx| {
20765 // editor.handle_input("else:", window, cx);
20766 // });
20767 // cx.assert_editor_state(indoc! {"
20768 // def main():
20769 // try:
20770 // i = 2
20771 // except:
20772 // for i in range(n):
20773 // pass
20774 // else:ˇ
20775 // "});
20776
20777 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
20778 // cx.set_state(indoc! {"
20779 // def main():
20780 // try:
20781 // i = 2
20782 // except:
20783 // j = 2
20784 // else:
20785 // for i in range(n):
20786 // pass
20787 // ˇ
20788 // "});
20789 // cx.update_editor(|editor, window, cx| {
20790 // editor.handle_input("finally:", window, cx);
20791 // });
20792 // cx.assert_editor_state(indoc! {"
20793 // def main():
20794 // try:
20795 // i = 2
20796 // except:
20797 // j = 2
20798 // else:
20799 // for i in range(n):
20800 // pass
20801 // finally:ˇ
20802 // "});
20803
20804 // test `else` stays at correct indent when typed after `for` block
20805 cx.set_state(indoc! {"
20806 def main():
20807 for i in range(10):
20808 if i == 3:
20809 break
20810 ˇ
20811 "});
20812 cx.update_editor(|editor, window, cx| {
20813 editor.handle_input("else:", window, cx);
20814 });
20815 cx.assert_editor_state(indoc! {"
20816 def main():
20817 for i in range(10):
20818 if i == 3:
20819 break
20820 else:ˇ
20821 "});
20822}
20823
20824#[gpui::test]
20825async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
20826 init_test(cx, |_| {});
20827 update_test_language_settings(cx, |settings| {
20828 settings.defaults.extend_comment_on_newline = Some(false);
20829 });
20830 let mut cx = EditorTestContext::new(cx).await;
20831 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20832 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20833
20834 // test correct indent after newline on comment
20835 cx.set_state(indoc! {"
20836 # COMMENT:ˇ
20837 "});
20838 cx.update_editor(|editor, window, cx| {
20839 editor.newline(&Newline, window, cx);
20840 });
20841 cx.assert_editor_state(indoc! {"
20842 # COMMENT:
20843 ˇ
20844 "});
20845
20846 // test correct indent after newline in curly brackets
20847 cx.set_state(indoc! {"
20848 {ˇ}
20849 "});
20850 cx.update_editor(|editor, window, cx| {
20851 editor.newline(&Newline, window, cx);
20852 });
20853 cx.run_until_parked();
20854 cx.assert_editor_state(indoc! {"
20855 {
20856 ˇ
20857 }
20858 "});
20859}
20860
20861fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20862 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20863 point..point
20864}
20865
20866fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20867 let (text, ranges) = marked_text_ranges(marked_text, true);
20868 assert_eq!(editor.text(cx), text);
20869 assert_eq!(
20870 editor.selections.ranges(cx),
20871 ranges,
20872 "Assert selections are {}",
20873 marked_text
20874 );
20875}
20876
20877pub fn handle_signature_help_request(
20878 cx: &mut EditorLspTestContext,
20879 mocked_response: lsp::SignatureHelp,
20880) -> impl Future<Output = ()> + use<> {
20881 let mut request =
20882 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
20883 let mocked_response = mocked_response.clone();
20884 async move { Ok(Some(mocked_response)) }
20885 });
20886
20887 async move {
20888 request.next().await;
20889 }
20890}
20891
20892/// Handle completion request passing a marked string specifying where the completion
20893/// should be triggered from using '|' character, what range should be replaced, and what completions
20894/// should be returned using '<' and '>' to delimit the range.
20895///
20896/// Also see `handle_completion_request_with_insert_and_replace`.
20897#[track_caller]
20898pub fn handle_completion_request(
20899 cx: &mut EditorLspTestContext,
20900 marked_string: &str,
20901 completions: Vec<&'static str>,
20902 counter: Arc<AtomicUsize>,
20903) -> impl Future<Output = ()> {
20904 let complete_from_marker: TextRangeMarker = '|'.into();
20905 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20906 let (_, mut marked_ranges) = marked_text_ranges_by(
20907 marked_string,
20908 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20909 );
20910
20911 let complete_from_position =
20912 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20913 let replace_range =
20914 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20915
20916 let mut request =
20917 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20918 let completions = completions.clone();
20919 counter.fetch_add(1, atomic::Ordering::Release);
20920 async move {
20921 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20922 assert_eq!(
20923 params.text_document_position.position,
20924 complete_from_position
20925 );
20926 Ok(Some(lsp::CompletionResponse::Array(
20927 completions
20928 .iter()
20929 .map(|completion_text| lsp::CompletionItem {
20930 label: completion_text.to_string(),
20931 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20932 range: replace_range,
20933 new_text: completion_text.to_string(),
20934 })),
20935 ..Default::default()
20936 })
20937 .collect(),
20938 )))
20939 }
20940 });
20941
20942 async move {
20943 request.next().await;
20944 }
20945}
20946
20947/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
20948/// given instead, which also contains an `insert` range.
20949///
20950/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
20951/// that is, `replace_range.start..cursor_pos`.
20952pub fn handle_completion_request_with_insert_and_replace(
20953 cx: &mut EditorLspTestContext,
20954 marked_string: &str,
20955 completions: Vec<&'static str>,
20956 counter: Arc<AtomicUsize>,
20957) -> impl Future<Output = ()> {
20958 let complete_from_marker: TextRangeMarker = '|'.into();
20959 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20960 let (_, mut marked_ranges) = marked_text_ranges_by(
20961 marked_string,
20962 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20963 );
20964
20965 let complete_from_position =
20966 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20967 let replace_range =
20968 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20969
20970 let mut request =
20971 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20972 let completions = completions.clone();
20973 counter.fetch_add(1, atomic::Ordering::Release);
20974 async move {
20975 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20976 assert_eq!(
20977 params.text_document_position.position, complete_from_position,
20978 "marker `|` position doesn't match",
20979 );
20980 Ok(Some(lsp::CompletionResponse::Array(
20981 completions
20982 .iter()
20983 .map(|completion_text| lsp::CompletionItem {
20984 label: completion_text.to_string(),
20985 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20986 lsp::InsertReplaceEdit {
20987 insert: lsp::Range {
20988 start: replace_range.start,
20989 end: complete_from_position,
20990 },
20991 replace: replace_range,
20992 new_text: completion_text.to_string(),
20993 },
20994 )),
20995 ..Default::default()
20996 })
20997 .collect(),
20998 )))
20999 }
21000 });
21001
21002 async move {
21003 request.next().await;
21004 }
21005}
21006
21007fn handle_resolve_completion_request(
21008 cx: &mut EditorLspTestContext,
21009 edits: Option<Vec<(&'static str, &'static str)>>,
21010) -> impl Future<Output = ()> {
21011 let edits = edits.map(|edits| {
21012 edits
21013 .iter()
21014 .map(|(marked_string, new_text)| {
21015 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21016 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21017 lsp::TextEdit::new(replace_range, new_text.to_string())
21018 })
21019 .collect::<Vec<_>>()
21020 });
21021
21022 let mut request =
21023 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21024 let edits = edits.clone();
21025 async move {
21026 Ok(lsp::CompletionItem {
21027 additional_text_edits: edits,
21028 ..Default::default()
21029 })
21030 }
21031 });
21032
21033 async move {
21034 request.next().await;
21035 }
21036}
21037
21038pub(crate) fn update_test_language_settings(
21039 cx: &mut TestAppContext,
21040 f: impl Fn(&mut AllLanguageSettingsContent),
21041) {
21042 cx.update(|cx| {
21043 SettingsStore::update_global(cx, |store, cx| {
21044 store.update_user_settings::<AllLanguageSettings>(cx, f);
21045 });
21046 });
21047}
21048
21049pub(crate) fn update_test_project_settings(
21050 cx: &mut TestAppContext,
21051 f: impl Fn(&mut ProjectSettings),
21052) {
21053 cx.update(|cx| {
21054 SettingsStore::update_global(cx, |store, cx| {
21055 store.update_user_settings::<ProjectSettings>(cx, f);
21056 });
21057 });
21058}
21059
21060pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
21061 cx.update(|cx| {
21062 assets::Assets.load_test_fonts(cx);
21063 let store = SettingsStore::test(cx);
21064 cx.set_global(store);
21065 theme::init(theme::LoadThemes::JustBase, cx);
21066 release_channel::init(SemanticVersion::default(), cx);
21067 client::init_settings(cx);
21068 language::init(cx);
21069 Project::init_settings(cx);
21070 workspace::init_settings(cx);
21071 crate::init(cx);
21072 });
21073
21074 update_test_language_settings(cx, f);
21075}
21076
21077#[track_caller]
21078fn assert_hunk_revert(
21079 not_reverted_text_with_selections: &str,
21080 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21081 expected_reverted_text_with_selections: &str,
21082 base_text: &str,
21083 cx: &mut EditorLspTestContext,
21084) {
21085 cx.set_state(not_reverted_text_with_selections);
21086 cx.set_head_text(base_text);
21087 cx.executor().run_until_parked();
21088
21089 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21090 let snapshot = editor.snapshot(window, cx);
21091 let reverted_hunk_statuses = snapshot
21092 .buffer_snapshot
21093 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21094 .map(|hunk| hunk.status().kind)
21095 .collect::<Vec<_>>();
21096
21097 editor.git_restore(&Default::default(), window, cx);
21098 reverted_hunk_statuses
21099 });
21100 cx.executor().run_until_parked();
21101 cx.assert_editor_state(expected_reverted_text_with_selections);
21102 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21103}