1use super::*;
2use crate::{
3 JoinLines,
4 inline_completion_tests::FakeInlineCompletionProvider,
5 linked_editing_ranges::LinkedEditingRanges,
6 scroll::scroll_amount::ScrollAmount,
7 test::{
8 assert_text_with_selections, build_editor,
9 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
10 editor_test_context::EditorTestContext,
11 select_ranges,
12 },
13};
14use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
15use futures::StreamExt;
16use gpui::{
17 BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
18 VisualTestContext, WindowBounds, WindowOptions, div,
19};
20use indoc::indoc;
21use language::{
22 BracketPairConfig,
23 Capability::ReadWrite,
24 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
25 Override, Point,
26 language_settings::{
27 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
28 LanguageSettingsContent, LspInsertMode, PrettierSettings,
29 },
30 tree_sitter_python,
31};
32use language_settings::{Formatter, FormatterList, IndentGuideSettings};
33use lsp::CompletionParams;
34use multi_buffer::{IndentGuide, PathKey};
35use parking_lot::Mutex;
36use pretty_assertions::{assert_eq, assert_ne};
37use project::{
38 FakeFs,
39 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
40 project_settings::{LspSettings, ProjectSettings},
41};
42use serde_json::{self, json};
43use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
44use std::{
45 iter,
46 sync::atomic::{self, AtomicUsize},
47};
48use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
49use text::ToPoint as _;
50use unindent::Unindent;
51use util::{
52 assert_set_eq, path,
53 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
54 uri,
55};
56use workspace::{
57 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
58 item::{FollowEvent, FollowableItem, Item, ItemHandle},
59};
60
61#[gpui::test]
62fn test_edit_events(cx: &mut TestAppContext) {
63 init_test(cx, |_| {});
64
65 let buffer = cx.new(|cx| {
66 let mut buffer = language::Buffer::local("123456", cx);
67 buffer.set_group_interval(Duration::from_secs(1));
68 buffer
69 });
70
71 let events = Rc::new(RefCell::new(Vec::new()));
72 let editor1 = cx.add_window({
73 let events = events.clone();
74 |window, cx| {
75 let entity = cx.entity().clone();
76 cx.subscribe_in(
77 &entity,
78 window,
79 move |_, _, event: &EditorEvent, _, _| match event {
80 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
81 EditorEvent::BufferEdited => {
82 events.borrow_mut().push(("editor1", "buffer edited"))
83 }
84 _ => {}
85 },
86 )
87 .detach();
88 Editor::for_buffer(buffer.clone(), None, window, cx)
89 }
90 });
91
92 let editor2 = cx.add_window({
93 let events = events.clone();
94 |window, cx| {
95 cx.subscribe_in(
96 &cx.entity().clone(),
97 window,
98 move |_, _, event: &EditorEvent, _, _| match event {
99 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
100 EditorEvent::BufferEdited => {
101 events.borrow_mut().push(("editor2", "buffer edited"))
102 }
103 _ => {}
104 },
105 )
106 .detach();
107 Editor::for_buffer(buffer.clone(), None, window, cx)
108 }
109 });
110
111 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
112
113 // Mutating editor 1 will emit an `Edited` event only for that editor.
114 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
115 assert_eq!(
116 mem::take(&mut *events.borrow_mut()),
117 [
118 ("editor1", "edited"),
119 ("editor1", "buffer edited"),
120 ("editor2", "buffer edited"),
121 ]
122 );
123
124 // Mutating editor 2 will emit an `Edited` event only for that editor.
125 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
126 assert_eq!(
127 mem::take(&mut *events.borrow_mut()),
128 [
129 ("editor2", "edited"),
130 ("editor1", "buffer edited"),
131 ("editor2", "buffer edited"),
132 ]
133 );
134
135 // Undoing on editor 1 will emit an `Edited` event only for that editor.
136 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
137 assert_eq!(
138 mem::take(&mut *events.borrow_mut()),
139 [
140 ("editor1", "edited"),
141 ("editor1", "buffer edited"),
142 ("editor2", "buffer edited"),
143 ]
144 );
145
146 // Redoing on editor 1 will emit an `Edited` event only for that editor.
147 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
148 assert_eq!(
149 mem::take(&mut *events.borrow_mut()),
150 [
151 ("editor1", "edited"),
152 ("editor1", "buffer edited"),
153 ("editor2", "buffer edited"),
154 ]
155 );
156
157 // Undoing on editor 2 will emit an `Edited` event only for that editor.
158 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
159 assert_eq!(
160 mem::take(&mut *events.borrow_mut()),
161 [
162 ("editor2", "edited"),
163 ("editor1", "buffer edited"),
164 ("editor2", "buffer edited"),
165 ]
166 );
167
168 // Redoing on editor 2 will emit an `Edited` event only for that editor.
169 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
170 assert_eq!(
171 mem::take(&mut *events.borrow_mut()),
172 [
173 ("editor2", "edited"),
174 ("editor1", "buffer edited"),
175 ("editor2", "buffer edited"),
176 ]
177 );
178
179 // No event is emitted when the mutation is a no-op.
180 _ = editor2.update(cx, |editor, window, cx| {
181 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
182
183 editor.backspace(&Backspace, window, cx);
184 });
185 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
186}
187
188#[gpui::test]
189fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
190 init_test(cx, |_| {});
191
192 let mut now = Instant::now();
193 let group_interval = Duration::from_millis(1);
194 let buffer = cx.new(|cx| {
195 let mut buf = language::Buffer::local("123456", cx);
196 buf.set_group_interval(group_interval);
197 buf
198 });
199 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
200 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
201
202 _ = editor.update(cx, |editor, window, cx| {
203 editor.start_transaction_at(now, window, cx);
204 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
205
206 editor.insert("cd", window, cx);
207 editor.end_transaction_at(now, cx);
208 assert_eq!(editor.text(cx), "12cd56");
209 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
210
211 editor.start_transaction_at(now, window, cx);
212 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
213 editor.insert("e", window, cx);
214 editor.end_transaction_at(now, cx);
215 assert_eq!(editor.text(cx), "12cde6");
216 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
217
218 now += group_interval + Duration::from_millis(1);
219 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
220
221 // Simulate an edit in another editor
222 buffer.update(cx, |buffer, cx| {
223 buffer.start_transaction_at(now, cx);
224 buffer.edit([(0..1, "a")], None, cx);
225 buffer.edit([(1..1, "b")], None, cx);
226 buffer.end_transaction_at(now, cx);
227 });
228
229 assert_eq!(editor.text(cx), "ab2cde6");
230 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
231
232 // Last transaction happened past the group interval in a different editor.
233 // Undo it individually and don't restore selections.
234 editor.undo(&Undo, window, cx);
235 assert_eq!(editor.text(cx), "12cde6");
236 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
237
238 // First two transactions happened within the group interval in this editor.
239 // Undo them together and restore selections.
240 editor.undo(&Undo, window, cx);
241 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
242 assert_eq!(editor.text(cx), "123456");
243 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
244
245 // Redo the first two transactions together.
246 editor.redo(&Redo, window, cx);
247 assert_eq!(editor.text(cx), "12cde6");
248 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
249
250 // Redo the last transaction on its own.
251 editor.redo(&Redo, window, cx);
252 assert_eq!(editor.text(cx), "ab2cde6");
253 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
254
255 // Test empty transactions.
256 editor.start_transaction_at(now, window, cx);
257 editor.end_transaction_at(now, cx);
258 editor.undo(&Undo, window, cx);
259 assert_eq!(editor.text(cx), "12cde6");
260 });
261}
262
263#[gpui::test]
264fn test_ime_composition(cx: &mut TestAppContext) {
265 init_test(cx, |_| {});
266
267 let buffer = cx.new(|cx| {
268 let mut buffer = language::Buffer::local("abcde", cx);
269 // Ensure automatic grouping doesn't occur.
270 buffer.set_group_interval(Duration::ZERO);
271 buffer
272 });
273
274 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
275 cx.add_window(|window, cx| {
276 let mut editor = build_editor(buffer.clone(), window, cx);
277
278 // Start a new IME composition.
279 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
280 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
281 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
282 assert_eq!(editor.text(cx), "äbcde");
283 assert_eq!(
284 editor.marked_text_ranges(cx),
285 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
286 );
287
288 // Finalize IME composition.
289 editor.replace_text_in_range(None, "ā", window, cx);
290 assert_eq!(editor.text(cx), "ābcde");
291 assert_eq!(editor.marked_text_ranges(cx), None);
292
293 // IME composition edits are grouped and are undone/redone at once.
294 editor.undo(&Default::default(), window, cx);
295 assert_eq!(editor.text(cx), "abcde");
296 assert_eq!(editor.marked_text_ranges(cx), None);
297 editor.redo(&Default::default(), window, cx);
298 assert_eq!(editor.text(cx), "ābcde");
299 assert_eq!(editor.marked_text_ranges(cx), None);
300
301 // Start a new IME composition.
302 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
303 assert_eq!(
304 editor.marked_text_ranges(cx),
305 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
306 );
307
308 // Undoing during an IME composition cancels it.
309 editor.undo(&Default::default(), window, cx);
310 assert_eq!(editor.text(cx), "ābcde");
311 assert_eq!(editor.marked_text_ranges(cx), None);
312
313 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
314 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
315 assert_eq!(editor.text(cx), "ābcdè");
316 assert_eq!(
317 editor.marked_text_ranges(cx),
318 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
319 );
320
321 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
322 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
323 assert_eq!(editor.text(cx), "ābcdę");
324 assert_eq!(editor.marked_text_ranges(cx), None);
325
326 // Start a new IME composition with multiple cursors.
327 editor.change_selections(None, window, cx, |s| {
328 s.select_ranges([
329 OffsetUtf16(1)..OffsetUtf16(1),
330 OffsetUtf16(3)..OffsetUtf16(3),
331 OffsetUtf16(5)..OffsetUtf16(5),
332 ])
333 });
334 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
335 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
336 assert_eq!(
337 editor.marked_text_ranges(cx),
338 Some(vec![
339 OffsetUtf16(0)..OffsetUtf16(3),
340 OffsetUtf16(4)..OffsetUtf16(7),
341 OffsetUtf16(8)..OffsetUtf16(11)
342 ])
343 );
344
345 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
346 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
347 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
348 assert_eq!(
349 editor.marked_text_ranges(cx),
350 Some(vec![
351 OffsetUtf16(1)..OffsetUtf16(2),
352 OffsetUtf16(5)..OffsetUtf16(6),
353 OffsetUtf16(9)..OffsetUtf16(10)
354 ])
355 );
356
357 // Finalize IME composition with multiple cursors.
358 editor.replace_text_in_range(Some(9..10), "2", window, cx);
359 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
360 assert_eq!(editor.marked_text_ranges(cx), None);
361
362 editor
363 });
364}
365
366#[gpui::test]
367fn test_selection_with_mouse(cx: &mut TestAppContext) {
368 init_test(cx, |_| {});
369
370 let editor = cx.add_window(|window, cx| {
371 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
372 build_editor(buffer, window, cx)
373 });
374
375 _ = editor.update(cx, |editor, window, cx| {
376 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
377 });
378 assert_eq!(
379 editor
380 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
381 .unwrap(),
382 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
383 );
384
385 _ = editor.update(cx, |editor, window, cx| {
386 editor.update_selection(
387 DisplayPoint::new(DisplayRow(3), 3),
388 0,
389 gpui::Point::<f32>::default(),
390 window,
391 cx,
392 );
393 });
394
395 assert_eq!(
396 editor
397 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
398 .unwrap(),
399 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
400 );
401
402 _ = editor.update(cx, |editor, window, cx| {
403 editor.update_selection(
404 DisplayPoint::new(DisplayRow(1), 1),
405 0,
406 gpui::Point::<f32>::default(),
407 window,
408 cx,
409 );
410 });
411
412 assert_eq!(
413 editor
414 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
415 .unwrap(),
416 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
417 );
418
419 _ = editor.update(cx, |editor, window, cx| {
420 editor.end_selection(window, cx);
421 editor.update_selection(
422 DisplayPoint::new(DisplayRow(3), 3),
423 0,
424 gpui::Point::<f32>::default(),
425 window,
426 cx,
427 );
428 });
429
430 assert_eq!(
431 editor
432 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
433 .unwrap(),
434 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
435 );
436
437 _ = editor.update(cx, |editor, window, cx| {
438 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
439 editor.update_selection(
440 DisplayPoint::new(DisplayRow(0), 0),
441 0,
442 gpui::Point::<f32>::default(),
443 window,
444 cx,
445 );
446 });
447
448 assert_eq!(
449 editor
450 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
451 .unwrap(),
452 [
453 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
454 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
455 ]
456 );
457
458 _ = editor.update(cx, |editor, window, cx| {
459 editor.end_selection(window, cx);
460 });
461
462 assert_eq!(
463 editor
464 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
465 .unwrap(),
466 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
467 );
468}
469
470#[gpui::test]
471fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
472 init_test(cx, |_| {});
473
474 let editor = cx.add_window(|window, cx| {
475 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
476 build_editor(buffer, window, cx)
477 });
478
479 _ = editor.update(cx, |editor, window, cx| {
480 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
481 });
482
483 _ = editor.update(cx, |editor, window, cx| {
484 editor.end_selection(window, cx);
485 });
486
487 _ = editor.update(cx, |editor, window, cx| {
488 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
489 });
490
491 _ = editor.update(cx, |editor, window, cx| {
492 editor.end_selection(window, cx);
493 });
494
495 assert_eq!(
496 editor
497 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
498 .unwrap(),
499 [
500 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
501 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
502 ]
503 );
504
505 _ = editor.update(cx, |editor, window, cx| {
506 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
507 });
508
509 _ = editor.update(cx, |editor, window, cx| {
510 editor.end_selection(window, cx);
511 });
512
513 assert_eq!(
514 editor
515 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
516 .unwrap(),
517 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
518 );
519}
520
521#[gpui::test]
522fn test_canceling_pending_selection(cx: &mut TestAppContext) {
523 init_test(cx, |_| {});
524
525 let editor = cx.add_window(|window, cx| {
526 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
527 build_editor(buffer, window, cx)
528 });
529
530 _ = editor.update(cx, |editor, window, cx| {
531 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
532 assert_eq!(
533 editor.selections.display_ranges(cx),
534 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
535 );
536 });
537
538 _ = editor.update(cx, |editor, window, cx| {
539 editor.update_selection(
540 DisplayPoint::new(DisplayRow(3), 3),
541 0,
542 gpui::Point::<f32>::default(),
543 window,
544 cx,
545 );
546 assert_eq!(
547 editor.selections.display_ranges(cx),
548 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
549 );
550 });
551
552 _ = editor.update(cx, |editor, window, cx| {
553 editor.cancel(&Cancel, window, cx);
554 editor.update_selection(
555 DisplayPoint::new(DisplayRow(1), 1),
556 0,
557 gpui::Point::<f32>::default(),
558 window,
559 cx,
560 );
561 assert_eq!(
562 editor.selections.display_ranges(cx),
563 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
564 );
565 });
566}
567
568#[gpui::test]
569fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
570 init_test(cx, |_| {});
571
572 let editor = cx.add_window(|window, cx| {
573 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
574 build_editor(buffer, window, cx)
575 });
576
577 _ = editor.update(cx, |editor, window, cx| {
578 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
579 assert_eq!(
580 editor.selections.display_ranges(cx),
581 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
582 );
583
584 editor.move_down(&Default::default(), window, cx);
585 assert_eq!(
586 editor.selections.display_ranges(cx),
587 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
588 );
589
590 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
591 assert_eq!(
592 editor.selections.display_ranges(cx),
593 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
594 );
595
596 editor.move_up(&Default::default(), window, cx);
597 assert_eq!(
598 editor.selections.display_ranges(cx),
599 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
600 );
601 });
602}
603
604#[gpui::test]
605fn test_clone(cx: &mut TestAppContext) {
606 init_test(cx, |_| {});
607
608 let (text, selection_ranges) = marked_text_ranges(
609 indoc! {"
610 one
611 two
612 threeˇ
613 four
614 fiveˇ
615 "},
616 true,
617 );
618
619 let editor = cx.add_window(|window, cx| {
620 let buffer = MultiBuffer::build_simple(&text, cx);
621 build_editor(buffer, window, cx)
622 });
623
624 _ = editor.update(cx, |editor, window, cx| {
625 editor.change_selections(None, window, cx, |s| {
626 s.select_ranges(selection_ranges.clone())
627 });
628 editor.fold_creases(
629 vec![
630 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
631 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
632 ],
633 true,
634 window,
635 cx,
636 );
637 });
638
639 let cloned_editor = editor
640 .update(cx, |editor, _, cx| {
641 cx.open_window(Default::default(), |window, cx| {
642 cx.new(|cx| editor.clone(window, cx))
643 })
644 })
645 .unwrap()
646 .unwrap();
647
648 let snapshot = editor
649 .update(cx, |e, window, cx| e.snapshot(window, cx))
650 .unwrap();
651 let cloned_snapshot = cloned_editor
652 .update(cx, |e, window, cx| e.snapshot(window, cx))
653 .unwrap();
654
655 assert_eq!(
656 cloned_editor
657 .update(cx, |e, _, cx| e.display_text(cx))
658 .unwrap(),
659 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
660 );
661 assert_eq!(
662 cloned_snapshot
663 .folds_in_range(0..text.len())
664 .collect::<Vec<_>>(),
665 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
666 );
667 assert_set_eq!(
668 cloned_editor
669 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
670 .unwrap(),
671 editor
672 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
673 .unwrap()
674 );
675 assert_set_eq!(
676 cloned_editor
677 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
678 .unwrap(),
679 editor
680 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
681 .unwrap()
682 );
683}
684
685#[gpui::test]
686async fn test_navigation_history(cx: &mut TestAppContext) {
687 init_test(cx, |_| {});
688
689 use workspace::item::Item;
690
691 let fs = FakeFs::new(cx.executor());
692 let project = Project::test(fs, [], cx).await;
693 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
694 let pane = workspace
695 .update(cx, |workspace, _, _| workspace.active_pane().clone())
696 .unwrap();
697
698 _ = workspace.update(cx, |_v, window, cx| {
699 cx.new(|cx| {
700 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
701 let mut editor = build_editor(buffer.clone(), window, cx);
702 let handle = cx.entity();
703 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
704
705 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
706 editor.nav_history.as_mut().unwrap().pop_backward(cx)
707 }
708
709 // Move the cursor a small distance.
710 // Nothing is added to the navigation history.
711 editor.change_selections(None, window, cx, |s| {
712 s.select_display_ranges([
713 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
714 ])
715 });
716 editor.change_selections(None, window, cx, |s| {
717 s.select_display_ranges([
718 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
719 ])
720 });
721 assert!(pop_history(&mut editor, cx).is_none());
722
723 // Move the cursor a large distance.
724 // The history can jump back to the previous position.
725 editor.change_selections(None, window, cx, |s| {
726 s.select_display_ranges([
727 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
728 ])
729 });
730 let nav_entry = pop_history(&mut editor, cx).unwrap();
731 editor.navigate(nav_entry.data.unwrap(), window, cx);
732 assert_eq!(nav_entry.item.id(), cx.entity_id());
733 assert_eq!(
734 editor.selections.display_ranges(cx),
735 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
736 );
737 assert!(pop_history(&mut editor, cx).is_none());
738
739 // Move the cursor a small distance via the mouse.
740 // Nothing is added to the navigation history.
741 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
742 editor.end_selection(window, cx);
743 assert_eq!(
744 editor.selections.display_ranges(cx),
745 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
746 );
747 assert!(pop_history(&mut editor, cx).is_none());
748
749 // Move the cursor a large distance via the mouse.
750 // The history can jump back to the previous position.
751 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
752 editor.end_selection(window, cx);
753 assert_eq!(
754 editor.selections.display_ranges(cx),
755 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
756 );
757 let nav_entry = pop_history(&mut editor, cx).unwrap();
758 editor.navigate(nav_entry.data.unwrap(), window, cx);
759 assert_eq!(nav_entry.item.id(), cx.entity_id());
760 assert_eq!(
761 editor.selections.display_ranges(cx),
762 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
763 );
764 assert!(pop_history(&mut editor, cx).is_none());
765
766 // Set scroll position to check later
767 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
768 let original_scroll_position = editor.scroll_manager.anchor();
769
770 // Jump to the end of the document and adjust scroll
771 editor.move_to_end(&MoveToEnd, window, cx);
772 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
773 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
774
775 let nav_entry = pop_history(&mut editor, cx).unwrap();
776 editor.navigate(nav_entry.data.unwrap(), window, cx);
777 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
778
779 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
780 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
781 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
782 let invalid_point = Point::new(9999, 0);
783 editor.navigate(
784 Box::new(NavigationData {
785 cursor_anchor: invalid_anchor,
786 cursor_position: invalid_point,
787 scroll_anchor: ScrollAnchor {
788 anchor: invalid_anchor,
789 offset: Default::default(),
790 },
791 scroll_top_row: invalid_point.row,
792 }),
793 window,
794 cx,
795 );
796 assert_eq!(
797 editor.selections.display_ranges(cx),
798 &[editor.max_point(cx)..editor.max_point(cx)]
799 );
800 assert_eq!(
801 editor.scroll_position(cx),
802 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
803 );
804
805 editor
806 })
807 });
808}
809
810#[gpui::test]
811fn test_cancel(cx: &mut TestAppContext) {
812 init_test(cx, |_| {});
813
814 let editor = cx.add_window(|window, cx| {
815 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
816 build_editor(buffer, window, cx)
817 });
818
819 _ = editor.update(cx, |editor, window, cx| {
820 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
821 editor.update_selection(
822 DisplayPoint::new(DisplayRow(1), 1),
823 0,
824 gpui::Point::<f32>::default(),
825 window,
826 cx,
827 );
828 editor.end_selection(window, cx);
829
830 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
831 editor.update_selection(
832 DisplayPoint::new(DisplayRow(0), 3),
833 0,
834 gpui::Point::<f32>::default(),
835 window,
836 cx,
837 );
838 editor.end_selection(window, cx);
839 assert_eq!(
840 editor.selections.display_ranges(cx),
841 [
842 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
843 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
844 ]
845 );
846 });
847
848 _ = editor.update(cx, |editor, window, cx| {
849 editor.cancel(&Cancel, window, cx);
850 assert_eq!(
851 editor.selections.display_ranges(cx),
852 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
853 );
854 });
855
856 _ = editor.update(cx, |editor, window, cx| {
857 editor.cancel(&Cancel, window, cx);
858 assert_eq!(
859 editor.selections.display_ranges(cx),
860 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
861 );
862 });
863}
864
865#[gpui::test]
866fn test_fold_action(cx: &mut TestAppContext) {
867 init_test(cx, |_| {});
868
869 let editor = cx.add_window(|window, cx| {
870 let buffer = MultiBuffer::build_simple(
871 &"
872 impl Foo {
873 // Hello!
874
875 fn a() {
876 1
877 }
878
879 fn b() {
880 2
881 }
882
883 fn c() {
884 3
885 }
886 }
887 "
888 .unindent(),
889 cx,
890 );
891 build_editor(buffer.clone(), window, cx)
892 });
893
894 _ = editor.update(cx, |editor, window, cx| {
895 editor.change_selections(None, window, cx, |s| {
896 s.select_display_ranges([
897 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
898 ]);
899 });
900 editor.fold(&Fold, window, cx);
901 assert_eq!(
902 editor.display_text(cx),
903 "
904 impl Foo {
905 // Hello!
906
907 fn a() {
908 1
909 }
910
911 fn b() {⋯
912 }
913
914 fn c() {⋯
915 }
916 }
917 "
918 .unindent(),
919 );
920
921 editor.fold(&Fold, window, cx);
922 assert_eq!(
923 editor.display_text(cx),
924 "
925 impl Foo {⋯
926 }
927 "
928 .unindent(),
929 );
930
931 editor.unfold_lines(&UnfoldLines, window, cx);
932 assert_eq!(
933 editor.display_text(cx),
934 "
935 impl Foo {
936 // Hello!
937
938 fn a() {
939 1
940 }
941
942 fn b() {⋯
943 }
944
945 fn c() {⋯
946 }
947 }
948 "
949 .unindent(),
950 );
951
952 editor.unfold_lines(&UnfoldLines, window, cx);
953 assert_eq!(
954 editor.display_text(cx),
955 editor.buffer.read(cx).read(cx).text()
956 );
957 });
958}
959
960#[gpui::test]
961fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
962 init_test(cx, |_| {});
963
964 let editor = cx.add_window(|window, cx| {
965 let buffer = MultiBuffer::build_simple(
966 &"
967 class Foo:
968 # Hello!
969
970 def a():
971 print(1)
972
973 def b():
974 print(2)
975
976 def c():
977 print(3)
978 "
979 .unindent(),
980 cx,
981 );
982 build_editor(buffer.clone(), window, cx)
983 });
984
985 _ = editor.update(cx, |editor, window, cx| {
986 editor.change_selections(None, window, cx, |s| {
987 s.select_display_ranges([
988 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
989 ]);
990 });
991 editor.fold(&Fold, window, cx);
992 assert_eq!(
993 editor.display_text(cx),
994 "
995 class Foo:
996 # Hello!
997
998 def a():
999 print(1)
1000
1001 def b():⋯
1002
1003 def c():⋯
1004 "
1005 .unindent(),
1006 );
1007
1008 editor.fold(&Fold, window, cx);
1009 assert_eq!(
1010 editor.display_text(cx),
1011 "
1012 class Foo:⋯
1013 "
1014 .unindent(),
1015 );
1016
1017 editor.unfold_lines(&UnfoldLines, window, cx);
1018 assert_eq!(
1019 editor.display_text(cx),
1020 "
1021 class Foo:
1022 # Hello!
1023
1024 def a():
1025 print(1)
1026
1027 def b():⋯
1028
1029 def c():⋯
1030 "
1031 .unindent(),
1032 );
1033
1034 editor.unfold_lines(&UnfoldLines, window, cx);
1035 assert_eq!(
1036 editor.display_text(cx),
1037 editor.buffer.read(cx).read(cx).text()
1038 );
1039 });
1040}
1041
1042#[gpui::test]
1043fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1044 init_test(cx, |_| {});
1045
1046 let editor = cx.add_window(|window, cx| {
1047 let buffer = MultiBuffer::build_simple(
1048 &"
1049 class Foo:
1050 # Hello!
1051
1052 def a():
1053 print(1)
1054
1055 def b():
1056 print(2)
1057
1058
1059 def c():
1060 print(3)
1061
1062
1063 "
1064 .unindent(),
1065 cx,
1066 );
1067 build_editor(buffer.clone(), window, cx)
1068 });
1069
1070 _ = editor.update(cx, |editor, window, cx| {
1071 editor.change_selections(None, window, cx, |s| {
1072 s.select_display_ranges([
1073 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1074 ]);
1075 });
1076 editor.fold(&Fold, window, cx);
1077 assert_eq!(
1078 editor.display_text(cx),
1079 "
1080 class Foo:
1081 # Hello!
1082
1083 def a():
1084 print(1)
1085
1086 def b():⋯
1087
1088
1089 def c():⋯
1090
1091
1092 "
1093 .unindent(),
1094 );
1095
1096 editor.fold(&Fold, window, cx);
1097 assert_eq!(
1098 editor.display_text(cx),
1099 "
1100 class Foo:⋯
1101
1102
1103 "
1104 .unindent(),
1105 );
1106
1107 editor.unfold_lines(&UnfoldLines, window, cx);
1108 assert_eq!(
1109 editor.display_text(cx),
1110 "
1111 class Foo:
1112 # Hello!
1113
1114 def a():
1115 print(1)
1116
1117 def b():⋯
1118
1119
1120 def c():⋯
1121
1122
1123 "
1124 .unindent(),
1125 );
1126
1127 editor.unfold_lines(&UnfoldLines, window, cx);
1128 assert_eq!(
1129 editor.display_text(cx),
1130 editor.buffer.read(cx).read(cx).text()
1131 );
1132 });
1133}
1134
1135#[gpui::test]
1136fn test_fold_at_level(cx: &mut TestAppContext) {
1137 init_test(cx, |_| {});
1138
1139 let editor = cx.add_window(|window, cx| {
1140 let buffer = MultiBuffer::build_simple(
1141 &"
1142 class Foo:
1143 # Hello!
1144
1145 def a():
1146 print(1)
1147
1148 def b():
1149 print(2)
1150
1151
1152 class Bar:
1153 # World!
1154
1155 def a():
1156 print(1)
1157
1158 def b():
1159 print(2)
1160
1161
1162 "
1163 .unindent(),
1164 cx,
1165 );
1166 build_editor(buffer.clone(), window, cx)
1167 });
1168
1169 _ = editor.update(cx, |editor, window, cx| {
1170 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1171 assert_eq!(
1172 editor.display_text(cx),
1173 "
1174 class Foo:
1175 # Hello!
1176
1177 def a():⋯
1178
1179 def b():⋯
1180
1181
1182 class Bar:
1183 # World!
1184
1185 def a():⋯
1186
1187 def b():⋯
1188
1189
1190 "
1191 .unindent(),
1192 );
1193
1194 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1195 assert_eq!(
1196 editor.display_text(cx),
1197 "
1198 class Foo:⋯
1199
1200
1201 class Bar:⋯
1202
1203
1204 "
1205 .unindent(),
1206 );
1207
1208 editor.unfold_all(&UnfoldAll, window, cx);
1209 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1210 assert_eq!(
1211 editor.display_text(cx),
1212 "
1213 class Foo:
1214 # Hello!
1215
1216 def a():
1217 print(1)
1218
1219 def b():
1220 print(2)
1221
1222
1223 class Bar:
1224 # World!
1225
1226 def a():
1227 print(1)
1228
1229 def b():
1230 print(2)
1231
1232
1233 "
1234 .unindent(),
1235 );
1236
1237 assert_eq!(
1238 editor.display_text(cx),
1239 editor.buffer.read(cx).read(cx).text()
1240 );
1241 });
1242}
1243
1244#[gpui::test]
1245fn test_move_cursor(cx: &mut TestAppContext) {
1246 init_test(cx, |_| {});
1247
1248 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1249 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1250
1251 buffer.update(cx, |buffer, cx| {
1252 buffer.edit(
1253 vec![
1254 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1255 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1256 ],
1257 None,
1258 cx,
1259 );
1260 });
1261 _ = editor.update(cx, |editor, window, cx| {
1262 assert_eq!(
1263 editor.selections.display_ranges(cx),
1264 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1265 );
1266
1267 editor.move_down(&MoveDown, window, cx);
1268 assert_eq!(
1269 editor.selections.display_ranges(cx),
1270 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1271 );
1272
1273 editor.move_right(&MoveRight, window, cx);
1274 assert_eq!(
1275 editor.selections.display_ranges(cx),
1276 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1277 );
1278
1279 editor.move_left(&MoveLeft, window, cx);
1280 assert_eq!(
1281 editor.selections.display_ranges(cx),
1282 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1283 );
1284
1285 editor.move_up(&MoveUp, window, cx);
1286 assert_eq!(
1287 editor.selections.display_ranges(cx),
1288 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1289 );
1290
1291 editor.move_to_end(&MoveToEnd, window, cx);
1292 assert_eq!(
1293 editor.selections.display_ranges(cx),
1294 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1295 );
1296
1297 editor.move_to_beginning(&MoveToBeginning, window, cx);
1298 assert_eq!(
1299 editor.selections.display_ranges(cx),
1300 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1301 );
1302
1303 editor.change_selections(None, window, cx, |s| {
1304 s.select_display_ranges([
1305 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1306 ]);
1307 });
1308 editor.select_to_beginning(&SelectToBeginning, window, cx);
1309 assert_eq!(
1310 editor.selections.display_ranges(cx),
1311 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1312 );
1313
1314 editor.select_to_end(&SelectToEnd, window, cx);
1315 assert_eq!(
1316 editor.selections.display_ranges(cx),
1317 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1318 );
1319 });
1320}
1321
1322#[gpui::test]
1323fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1324 init_test(cx, |_| {});
1325
1326 let editor = cx.add_window(|window, cx| {
1327 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1328 build_editor(buffer.clone(), window, cx)
1329 });
1330
1331 assert_eq!('🟥'.len_utf8(), 4);
1332 assert_eq!('α'.len_utf8(), 2);
1333
1334 _ = editor.update(cx, |editor, window, cx| {
1335 editor.fold_creases(
1336 vec![
1337 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1338 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1339 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1340 ],
1341 true,
1342 window,
1343 cx,
1344 );
1345 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1346
1347 editor.move_right(&MoveRight, window, cx);
1348 assert_eq!(
1349 editor.selections.display_ranges(cx),
1350 &[empty_range(0, "🟥".len())]
1351 );
1352 editor.move_right(&MoveRight, window, cx);
1353 assert_eq!(
1354 editor.selections.display_ranges(cx),
1355 &[empty_range(0, "🟥🟧".len())]
1356 );
1357 editor.move_right(&MoveRight, window, cx);
1358 assert_eq!(
1359 editor.selections.display_ranges(cx),
1360 &[empty_range(0, "🟥🟧⋯".len())]
1361 );
1362
1363 editor.move_down(&MoveDown, window, cx);
1364 assert_eq!(
1365 editor.selections.display_ranges(cx),
1366 &[empty_range(1, "ab⋯e".len())]
1367 );
1368 editor.move_left(&MoveLeft, window, cx);
1369 assert_eq!(
1370 editor.selections.display_ranges(cx),
1371 &[empty_range(1, "ab⋯".len())]
1372 );
1373 editor.move_left(&MoveLeft, window, cx);
1374 assert_eq!(
1375 editor.selections.display_ranges(cx),
1376 &[empty_range(1, "ab".len())]
1377 );
1378 editor.move_left(&MoveLeft, window, cx);
1379 assert_eq!(
1380 editor.selections.display_ranges(cx),
1381 &[empty_range(1, "a".len())]
1382 );
1383
1384 editor.move_down(&MoveDown, window, cx);
1385 assert_eq!(
1386 editor.selections.display_ranges(cx),
1387 &[empty_range(2, "α".len())]
1388 );
1389 editor.move_right(&MoveRight, window, cx);
1390 assert_eq!(
1391 editor.selections.display_ranges(cx),
1392 &[empty_range(2, "αβ".len())]
1393 );
1394 editor.move_right(&MoveRight, window, cx);
1395 assert_eq!(
1396 editor.selections.display_ranges(cx),
1397 &[empty_range(2, "αβ⋯".len())]
1398 );
1399 editor.move_right(&MoveRight, window, cx);
1400 assert_eq!(
1401 editor.selections.display_ranges(cx),
1402 &[empty_range(2, "αβ⋯ε".len())]
1403 );
1404
1405 editor.move_up(&MoveUp, window, cx);
1406 assert_eq!(
1407 editor.selections.display_ranges(cx),
1408 &[empty_range(1, "ab⋯e".len())]
1409 );
1410 editor.move_down(&MoveDown, window, cx);
1411 assert_eq!(
1412 editor.selections.display_ranges(cx),
1413 &[empty_range(2, "αβ⋯ε".len())]
1414 );
1415 editor.move_up(&MoveUp, window, cx);
1416 assert_eq!(
1417 editor.selections.display_ranges(cx),
1418 &[empty_range(1, "ab⋯e".len())]
1419 );
1420
1421 editor.move_up(&MoveUp, window, cx);
1422 assert_eq!(
1423 editor.selections.display_ranges(cx),
1424 &[empty_range(0, "🟥🟧".len())]
1425 );
1426 editor.move_left(&MoveLeft, window, cx);
1427 assert_eq!(
1428 editor.selections.display_ranges(cx),
1429 &[empty_range(0, "🟥".len())]
1430 );
1431 editor.move_left(&MoveLeft, window, cx);
1432 assert_eq!(
1433 editor.selections.display_ranges(cx),
1434 &[empty_range(0, "".len())]
1435 );
1436 });
1437}
1438
1439#[gpui::test]
1440fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1441 init_test(cx, |_| {});
1442
1443 let editor = cx.add_window(|window, cx| {
1444 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1445 build_editor(buffer.clone(), window, cx)
1446 });
1447 _ = editor.update(cx, |editor, window, cx| {
1448 editor.change_selections(None, window, cx, |s| {
1449 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1450 });
1451
1452 // moving above start of document should move selection to start of document,
1453 // but the next move down should still be at the original goal_x
1454 editor.move_up(&MoveUp, window, cx);
1455 assert_eq!(
1456 editor.selections.display_ranges(cx),
1457 &[empty_range(0, "".len())]
1458 );
1459
1460 editor.move_down(&MoveDown, window, cx);
1461 assert_eq!(
1462 editor.selections.display_ranges(cx),
1463 &[empty_range(1, "abcd".len())]
1464 );
1465
1466 editor.move_down(&MoveDown, window, cx);
1467 assert_eq!(
1468 editor.selections.display_ranges(cx),
1469 &[empty_range(2, "αβγ".len())]
1470 );
1471
1472 editor.move_down(&MoveDown, window, cx);
1473 assert_eq!(
1474 editor.selections.display_ranges(cx),
1475 &[empty_range(3, "abcd".len())]
1476 );
1477
1478 editor.move_down(&MoveDown, window, cx);
1479 assert_eq!(
1480 editor.selections.display_ranges(cx),
1481 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1482 );
1483
1484 // moving past end of document should not change goal_x
1485 editor.move_down(&MoveDown, window, cx);
1486 assert_eq!(
1487 editor.selections.display_ranges(cx),
1488 &[empty_range(5, "".len())]
1489 );
1490
1491 editor.move_down(&MoveDown, window, cx);
1492 assert_eq!(
1493 editor.selections.display_ranges(cx),
1494 &[empty_range(5, "".len())]
1495 );
1496
1497 editor.move_up(&MoveUp, window, cx);
1498 assert_eq!(
1499 editor.selections.display_ranges(cx),
1500 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1501 );
1502
1503 editor.move_up(&MoveUp, window, cx);
1504 assert_eq!(
1505 editor.selections.display_ranges(cx),
1506 &[empty_range(3, "abcd".len())]
1507 );
1508
1509 editor.move_up(&MoveUp, window, cx);
1510 assert_eq!(
1511 editor.selections.display_ranges(cx),
1512 &[empty_range(2, "αβγ".len())]
1513 );
1514 });
1515}
1516
1517#[gpui::test]
1518fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1519 init_test(cx, |_| {});
1520 let move_to_beg = MoveToBeginningOfLine {
1521 stop_at_soft_wraps: true,
1522 stop_at_indent: true,
1523 };
1524
1525 let delete_to_beg = DeleteToBeginningOfLine {
1526 stop_at_indent: false,
1527 };
1528
1529 let move_to_end = MoveToEndOfLine {
1530 stop_at_soft_wraps: true,
1531 };
1532
1533 let editor = cx.add_window(|window, cx| {
1534 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1535 build_editor(buffer, window, cx)
1536 });
1537 _ = editor.update(cx, |editor, window, cx| {
1538 editor.change_selections(None, window, cx, |s| {
1539 s.select_display_ranges([
1540 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1541 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1542 ]);
1543 });
1544 });
1545
1546 _ = editor.update(cx, |editor, window, cx| {
1547 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1548 assert_eq!(
1549 editor.selections.display_ranges(cx),
1550 &[
1551 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1552 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1553 ]
1554 );
1555 });
1556
1557 _ = editor.update(cx, |editor, window, cx| {
1558 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1559 assert_eq!(
1560 editor.selections.display_ranges(cx),
1561 &[
1562 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1563 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1564 ]
1565 );
1566 });
1567
1568 _ = editor.update(cx, |editor, window, cx| {
1569 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1570 assert_eq!(
1571 editor.selections.display_ranges(cx),
1572 &[
1573 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1574 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1575 ]
1576 );
1577 });
1578
1579 _ = editor.update(cx, |editor, window, cx| {
1580 editor.move_to_end_of_line(&move_to_end, window, cx);
1581 assert_eq!(
1582 editor.selections.display_ranges(cx),
1583 &[
1584 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1585 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1586 ]
1587 );
1588 });
1589
1590 // Moving to the end of line again is a no-op.
1591 _ = editor.update(cx, |editor, window, cx| {
1592 editor.move_to_end_of_line(&move_to_end, window, cx);
1593 assert_eq!(
1594 editor.selections.display_ranges(cx),
1595 &[
1596 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1597 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1598 ]
1599 );
1600 });
1601
1602 _ = editor.update(cx, |editor, window, cx| {
1603 editor.move_left(&MoveLeft, window, cx);
1604 editor.select_to_beginning_of_line(
1605 &SelectToBeginningOfLine {
1606 stop_at_soft_wraps: true,
1607 stop_at_indent: true,
1608 },
1609 window,
1610 cx,
1611 );
1612 assert_eq!(
1613 editor.selections.display_ranges(cx),
1614 &[
1615 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1616 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1617 ]
1618 );
1619 });
1620
1621 _ = editor.update(cx, |editor, window, cx| {
1622 editor.select_to_beginning_of_line(
1623 &SelectToBeginningOfLine {
1624 stop_at_soft_wraps: true,
1625 stop_at_indent: true,
1626 },
1627 window,
1628 cx,
1629 );
1630 assert_eq!(
1631 editor.selections.display_ranges(cx),
1632 &[
1633 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1634 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1635 ]
1636 );
1637 });
1638
1639 _ = editor.update(cx, |editor, window, cx| {
1640 editor.select_to_beginning_of_line(
1641 &SelectToBeginningOfLine {
1642 stop_at_soft_wraps: true,
1643 stop_at_indent: true,
1644 },
1645 window,
1646 cx,
1647 );
1648 assert_eq!(
1649 editor.selections.display_ranges(cx),
1650 &[
1651 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1652 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1653 ]
1654 );
1655 });
1656
1657 _ = editor.update(cx, |editor, window, cx| {
1658 editor.select_to_end_of_line(
1659 &SelectToEndOfLine {
1660 stop_at_soft_wraps: true,
1661 },
1662 window,
1663 cx,
1664 );
1665 assert_eq!(
1666 editor.selections.display_ranges(cx),
1667 &[
1668 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1669 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1670 ]
1671 );
1672 });
1673
1674 _ = editor.update(cx, |editor, window, cx| {
1675 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1676 assert_eq!(editor.display_text(cx), "ab\n de");
1677 assert_eq!(
1678 editor.selections.display_ranges(cx),
1679 &[
1680 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1681 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1682 ]
1683 );
1684 });
1685
1686 _ = editor.update(cx, |editor, window, cx| {
1687 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1688 assert_eq!(editor.display_text(cx), "\n");
1689 assert_eq!(
1690 editor.selections.display_ranges(cx),
1691 &[
1692 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1693 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1694 ]
1695 );
1696 });
1697}
1698
1699#[gpui::test]
1700fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1701 init_test(cx, |_| {});
1702 let move_to_beg = MoveToBeginningOfLine {
1703 stop_at_soft_wraps: false,
1704 stop_at_indent: false,
1705 };
1706
1707 let move_to_end = MoveToEndOfLine {
1708 stop_at_soft_wraps: false,
1709 };
1710
1711 let editor = cx.add_window(|window, cx| {
1712 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1713 build_editor(buffer, window, cx)
1714 });
1715
1716 _ = editor.update(cx, |editor, window, cx| {
1717 editor.set_wrap_width(Some(140.0.into()), cx);
1718
1719 // We expect the following lines after wrapping
1720 // ```
1721 // thequickbrownfox
1722 // jumpedoverthelazydo
1723 // gs
1724 // ```
1725 // The final `gs` was soft-wrapped onto a new line.
1726 assert_eq!(
1727 "thequickbrownfox\njumpedoverthelaz\nydogs",
1728 editor.display_text(cx),
1729 );
1730
1731 // First, let's assert behavior on the first line, that was not soft-wrapped.
1732 // Start the cursor at the `k` on the first line
1733 editor.change_selections(None, window, cx, |s| {
1734 s.select_display_ranges([
1735 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1736 ]);
1737 });
1738
1739 // Moving to the beginning of the line should put us at the beginning of the line.
1740 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1741 assert_eq!(
1742 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1743 editor.selections.display_ranges(cx)
1744 );
1745
1746 // Moving to the end of the line should put us at the end of the line.
1747 editor.move_to_end_of_line(&move_to_end, window, cx);
1748 assert_eq!(
1749 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1750 editor.selections.display_ranges(cx)
1751 );
1752
1753 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1754 // Start the cursor at the last line (`y` that was wrapped to a new line)
1755 editor.change_selections(None, window, cx, |s| {
1756 s.select_display_ranges([
1757 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1758 ]);
1759 });
1760
1761 // Moving to the beginning of the line should put us at the start of the second line of
1762 // display text, i.e., the `j`.
1763 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1764 assert_eq!(
1765 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1766 editor.selections.display_ranges(cx)
1767 );
1768
1769 // Moving to the beginning of the line again should be a no-op.
1770 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1771 assert_eq!(
1772 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1773 editor.selections.display_ranges(cx)
1774 );
1775
1776 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1777 // next display line.
1778 editor.move_to_end_of_line(&move_to_end, window, cx);
1779 assert_eq!(
1780 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1781 editor.selections.display_ranges(cx)
1782 );
1783
1784 // Moving to the end of the line again should be a no-op.
1785 editor.move_to_end_of_line(&move_to_end, window, cx);
1786 assert_eq!(
1787 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1788 editor.selections.display_ranges(cx)
1789 );
1790 });
1791}
1792
1793#[gpui::test]
1794fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1795 init_test(cx, |_| {});
1796
1797 let move_to_beg = MoveToBeginningOfLine {
1798 stop_at_soft_wraps: true,
1799 stop_at_indent: true,
1800 };
1801
1802 let select_to_beg = SelectToBeginningOfLine {
1803 stop_at_soft_wraps: true,
1804 stop_at_indent: true,
1805 };
1806
1807 let delete_to_beg = DeleteToBeginningOfLine {
1808 stop_at_indent: true,
1809 };
1810
1811 let move_to_end = MoveToEndOfLine {
1812 stop_at_soft_wraps: false,
1813 };
1814
1815 let editor = cx.add_window(|window, cx| {
1816 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1817 build_editor(buffer, window, cx)
1818 });
1819
1820 _ = editor.update(cx, |editor, window, cx| {
1821 editor.change_selections(None, window, cx, |s| {
1822 s.select_display_ranges([
1823 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1824 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1825 ]);
1826 });
1827
1828 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1829 // and the second cursor at the first non-whitespace character in the line.
1830 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1831 assert_eq!(
1832 editor.selections.display_ranges(cx),
1833 &[
1834 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1835 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1836 ]
1837 );
1838
1839 // Moving to the beginning of the line again should be a no-op for the first cursor,
1840 // and should move the second cursor to the beginning of the line.
1841 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1842 assert_eq!(
1843 editor.selections.display_ranges(cx),
1844 &[
1845 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1846 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1847 ]
1848 );
1849
1850 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1851 // and should move the second cursor back to the first non-whitespace character in the line.
1852 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1853 assert_eq!(
1854 editor.selections.display_ranges(cx),
1855 &[
1856 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1857 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1858 ]
1859 );
1860
1861 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1862 // and to the first non-whitespace character in the line for the second cursor.
1863 editor.move_to_end_of_line(&move_to_end, window, cx);
1864 editor.move_left(&MoveLeft, window, cx);
1865 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1866 assert_eq!(
1867 editor.selections.display_ranges(cx),
1868 &[
1869 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1870 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1871 ]
1872 );
1873
1874 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1875 // and should select to the beginning of the line for the second cursor.
1876 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1877 assert_eq!(
1878 editor.selections.display_ranges(cx),
1879 &[
1880 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1881 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1882 ]
1883 );
1884
1885 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1886 // and should delete to the first non-whitespace character in the line for the second cursor.
1887 editor.move_to_end_of_line(&move_to_end, window, cx);
1888 editor.move_left(&MoveLeft, window, cx);
1889 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1890 assert_eq!(editor.text(cx), "c\n f");
1891 });
1892}
1893
1894#[gpui::test]
1895fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1896 init_test(cx, |_| {});
1897
1898 let editor = cx.add_window(|window, cx| {
1899 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1900 build_editor(buffer, window, cx)
1901 });
1902 _ = editor.update(cx, |editor, window, cx| {
1903 editor.change_selections(None, window, cx, |s| {
1904 s.select_display_ranges([
1905 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1906 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1907 ])
1908 });
1909
1910 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1911 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1912
1913 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1914 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1915
1916 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1917 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1918
1919 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1920 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1921
1922 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1923 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1924
1925 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1926 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1927
1928 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1929 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1930
1931 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1932 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1933
1934 editor.move_right(&MoveRight, window, cx);
1935 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1936 assert_selection_ranges(
1937 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1938 editor,
1939 cx,
1940 );
1941
1942 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1943 assert_selection_ranges(
1944 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1945 editor,
1946 cx,
1947 );
1948
1949 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1950 assert_selection_ranges(
1951 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1952 editor,
1953 cx,
1954 );
1955 });
1956}
1957
1958#[gpui::test]
1959fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1960 init_test(cx, |_| {});
1961
1962 let editor = cx.add_window(|window, cx| {
1963 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1964 build_editor(buffer, window, cx)
1965 });
1966
1967 _ = editor.update(cx, |editor, window, cx| {
1968 editor.set_wrap_width(Some(140.0.into()), cx);
1969 assert_eq!(
1970 editor.display_text(cx),
1971 "use one::{\n two::three::\n four::five\n};"
1972 );
1973
1974 editor.change_selections(None, window, cx, |s| {
1975 s.select_display_ranges([
1976 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1977 ]);
1978 });
1979
1980 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1981 assert_eq!(
1982 editor.selections.display_ranges(cx),
1983 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1984 );
1985
1986 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1987 assert_eq!(
1988 editor.selections.display_ranges(cx),
1989 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1990 );
1991
1992 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1993 assert_eq!(
1994 editor.selections.display_ranges(cx),
1995 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1996 );
1997
1998 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1999 assert_eq!(
2000 editor.selections.display_ranges(cx),
2001 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2002 );
2003
2004 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2005 assert_eq!(
2006 editor.selections.display_ranges(cx),
2007 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2008 );
2009
2010 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2011 assert_eq!(
2012 editor.selections.display_ranges(cx),
2013 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2014 );
2015 });
2016}
2017
2018#[gpui::test]
2019async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2020 init_test(cx, |_| {});
2021 let mut cx = EditorTestContext::new(cx).await;
2022
2023 let line_height = cx.editor(|editor, window, _| {
2024 editor
2025 .style()
2026 .unwrap()
2027 .text
2028 .line_height_in_pixels(window.rem_size())
2029 });
2030 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2031
2032 cx.set_state(
2033 &r#"ˇone
2034 two
2035
2036 three
2037 fourˇ
2038 five
2039
2040 six"#
2041 .unindent(),
2042 );
2043
2044 cx.update_editor(|editor, window, cx| {
2045 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2046 });
2047 cx.assert_editor_state(
2048 &r#"one
2049 two
2050 ˇ
2051 three
2052 four
2053 five
2054 ˇ
2055 six"#
2056 .unindent(),
2057 );
2058
2059 cx.update_editor(|editor, window, cx| {
2060 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2061 });
2062 cx.assert_editor_state(
2063 &r#"one
2064 two
2065
2066 three
2067 four
2068 five
2069 ˇ
2070 sixˇ"#
2071 .unindent(),
2072 );
2073
2074 cx.update_editor(|editor, window, cx| {
2075 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2076 });
2077 cx.assert_editor_state(
2078 &r#"one
2079 two
2080
2081 three
2082 four
2083 five
2084
2085 sixˇ"#
2086 .unindent(),
2087 );
2088
2089 cx.update_editor(|editor, window, cx| {
2090 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2091 });
2092 cx.assert_editor_state(
2093 &r#"one
2094 two
2095
2096 three
2097 four
2098 five
2099 ˇ
2100 six"#
2101 .unindent(),
2102 );
2103
2104 cx.update_editor(|editor, window, cx| {
2105 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2106 });
2107 cx.assert_editor_state(
2108 &r#"one
2109 two
2110 ˇ
2111 three
2112 four
2113 five
2114
2115 six"#
2116 .unindent(),
2117 );
2118
2119 cx.update_editor(|editor, window, cx| {
2120 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2121 });
2122 cx.assert_editor_state(
2123 &r#"ˇone
2124 two
2125
2126 three
2127 four
2128 five
2129
2130 six"#
2131 .unindent(),
2132 );
2133}
2134
2135#[gpui::test]
2136async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2137 init_test(cx, |_| {});
2138 let mut cx = EditorTestContext::new(cx).await;
2139 let line_height = cx.editor(|editor, window, _| {
2140 editor
2141 .style()
2142 .unwrap()
2143 .text
2144 .line_height_in_pixels(window.rem_size())
2145 });
2146 let window = cx.window;
2147 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2148
2149 cx.set_state(
2150 r#"ˇone
2151 two
2152 three
2153 four
2154 five
2155 six
2156 seven
2157 eight
2158 nine
2159 ten
2160 "#,
2161 );
2162
2163 cx.update_editor(|editor, window, cx| {
2164 assert_eq!(
2165 editor.snapshot(window, cx).scroll_position(),
2166 gpui::Point::new(0., 0.)
2167 );
2168 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2169 assert_eq!(
2170 editor.snapshot(window, cx).scroll_position(),
2171 gpui::Point::new(0., 3.)
2172 );
2173 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2174 assert_eq!(
2175 editor.snapshot(window, cx).scroll_position(),
2176 gpui::Point::new(0., 6.)
2177 );
2178 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2179 assert_eq!(
2180 editor.snapshot(window, cx).scroll_position(),
2181 gpui::Point::new(0., 3.)
2182 );
2183
2184 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2185 assert_eq!(
2186 editor.snapshot(window, cx).scroll_position(),
2187 gpui::Point::new(0., 1.)
2188 );
2189 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2190 assert_eq!(
2191 editor.snapshot(window, cx).scroll_position(),
2192 gpui::Point::new(0., 3.)
2193 );
2194 });
2195}
2196
2197#[gpui::test]
2198async fn test_autoscroll(cx: &mut TestAppContext) {
2199 init_test(cx, |_| {});
2200 let mut cx = EditorTestContext::new(cx).await;
2201
2202 let line_height = cx.update_editor(|editor, window, cx| {
2203 editor.set_vertical_scroll_margin(2, cx);
2204 editor
2205 .style()
2206 .unwrap()
2207 .text
2208 .line_height_in_pixels(window.rem_size())
2209 });
2210 let window = cx.window;
2211 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2212
2213 cx.set_state(
2214 r#"ˇone
2215 two
2216 three
2217 four
2218 five
2219 six
2220 seven
2221 eight
2222 nine
2223 ten
2224 "#,
2225 );
2226 cx.update_editor(|editor, window, cx| {
2227 assert_eq!(
2228 editor.snapshot(window, cx).scroll_position(),
2229 gpui::Point::new(0., 0.0)
2230 );
2231 });
2232
2233 // Add a cursor below the visible area. Since both cursors cannot fit
2234 // on screen, the editor autoscrolls to reveal the newest cursor, and
2235 // allows the vertical scroll margin below that cursor.
2236 cx.update_editor(|editor, window, cx| {
2237 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2238 selections.select_ranges([
2239 Point::new(0, 0)..Point::new(0, 0),
2240 Point::new(6, 0)..Point::new(6, 0),
2241 ]);
2242 })
2243 });
2244 cx.update_editor(|editor, window, cx| {
2245 assert_eq!(
2246 editor.snapshot(window, cx).scroll_position(),
2247 gpui::Point::new(0., 3.0)
2248 );
2249 });
2250
2251 // Move down. The editor cursor scrolls down to track the newest cursor.
2252 cx.update_editor(|editor, window, cx| {
2253 editor.move_down(&Default::default(), window, cx);
2254 });
2255 cx.update_editor(|editor, window, cx| {
2256 assert_eq!(
2257 editor.snapshot(window, cx).scroll_position(),
2258 gpui::Point::new(0., 4.0)
2259 );
2260 });
2261
2262 // Add a cursor above the visible area. Since both cursors fit on screen,
2263 // the editor scrolls to show both.
2264 cx.update_editor(|editor, window, cx| {
2265 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2266 selections.select_ranges([
2267 Point::new(1, 0)..Point::new(1, 0),
2268 Point::new(6, 0)..Point::new(6, 0),
2269 ]);
2270 })
2271 });
2272 cx.update_editor(|editor, window, cx| {
2273 assert_eq!(
2274 editor.snapshot(window, cx).scroll_position(),
2275 gpui::Point::new(0., 1.0)
2276 );
2277 });
2278}
2279
2280#[gpui::test]
2281async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2282 init_test(cx, |_| {});
2283 let mut cx = EditorTestContext::new(cx).await;
2284
2285 let line_height = cx.editor(|editor, window, _cx| {
2286 editor
2287 .style()
2288 .unwrap()
2289 .text
2290 .line_height_in_pixels(window.rem_size())
2291 });
2292 let window = cx.window;
2293 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2294 cx.set_state(
2295 &r#"
2296 ˇone
2297 two
2298 threeˇ
2299 four
2300 five
2301 six
2302 seven
2303 eight
2304 nine
2305 ten
2306 "#
2307 .unindent(),
2308 );
2309
2310 cx.update_editor(|editor, window, cx| {
2311 editor.move_page_down(&MovePageDown::default(), window, cx)
2312 });
2313 cx.assert_editor_state(
2314 &r#"
2315 one
2316 two
2317 three
2318 ˇfour
2319 five
2320 sixˇ
2321 seven
2322 eight
2323 nine
2324 ten
2325 "#
2326 .unindent(),
2327 );
2328
2329 cx.update_editor(|editor, window, cx| {
2330 editor.move_page_down(&MovePageDown::default(), window, cx)
2331 });
2332 cx.assert_editor_state(
2333 &r#"
2334 one
2335 two
2336 three
2337 four
2338 five
2339 six
2340 ˇseven
2341 eight
2342 nineˇ
2343 ten
2344 "#
2345 .unindent(),
2346 );
2347
2348 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2349 cx.assert_editor_state(
2350 &r#"
2351 one
2352 two
2353 three
2354 ˇfour
2355 five
2356 sixˇ
2357 seven
2358 eight
2359 nine
2360 ten
2361 "#
2362 .unindent(),
2363 );
2364
2365 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2366 cx.assert_editor_state(
2367 &r#"
2368 ˇone
2369 two
2370 threeˇ
2371 four
2372 five
2373 six
2374 seven
2375 eight
2376 nine
2377 ten
2378 "#
2379 .unindent(),
2380 );
2381
2382 // Test select collapsing
2383 cx.update_editor(|editor, window, cx| {
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 editor.move_page_down(&MovePageDown::default(), window, cx);
2386 editor.move_page_down(&MovePageDown::default(), window, cx);
2387 });
2388 cx.assert_editor_state(
2389 &r#"
2390 one
2391 two
2392 three
2393 four
2394 five
2395 six
2396 seven
2397 eight
2398 nine
2399 ˇten
2400 ˇ"#
2401 .unindent(),
2402 );
2403}
2404
2405#[gpui::test]
2406async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2407 init_test(cx, |_| {});
2408 let mut cx = EditorTestContext::new(cx).await;
2409 cx.set_state("one «two threeˇ» four");
2410 cx.update_editor(|editor, window, cx| {
2411 editor.delete_to_beginning_of_line(
2412 &DeleteToBeginningOfLine {
2413 stop_at_indent: false,
2414 },
2415 window,
2416 cx,
2417 );
2418 assert_eq!(editor.text(cx), " four");
2419 });
2420}
2421
2422#[gpui::test]
2423fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2424 init_test(cx, |_| {});
2425
2426 let editor = cx.add_window(|window, cx| {
2427 let buffer = MultiBuffer::build_simple("one two three four", cx);
2428 build_editor(buffer.clone(), window, cx)
2429 });
2430
2431 _ = editor.update(cx, |editor, window, cx| {
2432 editor.change_selections(None, window, cx, |s| {
2433 s.select_display_ranges([
2434 // an empty selection - the preceding word fragment is deleted
2435 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2436 // characters selected - they are deleted
2437 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2438 ])
2439 });
2440 editor.delete_to_previous_word_start(
2441 &DeleteToPreviousWordStart {
2442 ignore_newlines: false,
2443 },
2444 window,
2445 cx,
2446 );
2447 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2448 });
2449
2450 _ = editor.update(cx, |editor, window, cx| {
2451 editor.change_selections(None, window, cx, |s| {
2452 s.select_display_ranges([
2453 // an empty selection - the following word fragment is deleted
2454 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2455 // characters selected - they are deleted
2456 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2457 ])
2458 });
2459 editor.delete_to_next_word_end(
2460 &DeleteToNextWordEnd {
2461 ignore_newlines: false,
2462 },
2463 window,
2464 cx,
2465 );
2466 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2467 });
2468}
2469
2470#[gpui::test]
2471fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2472 init_test(cx, |_| {});
2473
2474 let editor = cx.add_window(|window, cx| {
2475 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2476 build_editor(buffer.clone(), window, cx)
2477 });
2478 let del_to_prev_word_start = DeleteToPreviousWordStart {
2479 ignore_newlines: false,
2480 };
2481 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2482 ignore_newlines: true,
2483 };
2484
2485 _ = editor.update(cx, |editor, window, cx| {
2486 editor.change_selections(None, window, cx, |s| {
2487 s.select_display_ranges([
2488 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2489 ])
2490 });
2491 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2492 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
2493 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2494 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree");
2495 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2496 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\n");
2497 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2498 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2");
2499 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2500 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
2501 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2502 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2503 });
2504}
2505
2506#[gpui::test]
2507fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2508 init_test(cx, |_| {});
2509
2510 let editor = cx.add_window(|window, cx| {
2511 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2512 build_editor(buffer.clone(), window, cx)
2513 });
2514 let del_to_next_word_end = DeleteToNextWordEnd {
2515 ignore_newlines: false,
2516 };
2517 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2518 ignore_newlines: true,
2519 };
2520
2521 _ = editor.update(cx, |editor, window, cx| {
2522 editor.change_selections(None, window, cx, |s| {
2523 s.select_display_ranges([
2524 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2525 ])
2526 });
2527 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2528 assert_eq!(
2529 editor.buffer.read(cx).read(cx).text(),
2530 "one\n two\nthree\n four"
2531 );
2532 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2533 assert_eq!(
2534 editor.buffer.read(cx).read(cx).text(),
2535 "\n two\nthree\n four"
2536 );
2537 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2538 assert_eq!(
2539 editor.buffer.read(cx).read(cx).text(),
2540 "two\nthree\n four"
2541 );
2542 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2543 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2544 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2545 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\n four");
2546 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2547 assert_eq!(editor.buffer.read(cx).read(cx).text(), "");
2548 });
2549}
2550
2551#[gpui::test]
2552fn test_newline(cx: &mut TestAppContext) {
2553 init_test(cx, |_| {});
2554
2555 let editor = cx.add_window(|window, cx| {
2556 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2557 build_editor(buffer.clone(), window, cx)
2558 });
2559
2560 _ = editor.update(cx, |editor, window, cx| {
2561 editor.change_selections(None, window, cx, |s| {
2562 s.select_display_ranges([
2563 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2565 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2566 ])
2567 });
2568
2569 editor.newline(&Newline, window, cx);
2570 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2571 });
2572}
2573
2574#[gpui::test]
2575fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2576 init_test(cx, |_| {});
2577
2578 let editor = cx.add_window(|window, cx| {
2579 let buffer = MultiBuffer::build_simple(
2580 "
2581 a
2582 b(
2583 X
2584 )
2585 c(
2586 X
2587 )
2588 "
2589 .unindent()
2590 .as_str(),
2591 cx,
2592 );
2593 let mut editor = build_editor(buffer.clone(), window, cx);
2594 editor.change_selections(None, window, cx, |s| {
2595 s.select_ranges([
2596 Point::new(2, 4)..Point::new(2, 5),
2597 Point::new(5, 4)..Point::new(5, 5),
2598 ])
2599 });
2600 editor
2601 });
2602
2603 _ = editor.update(cx, |editor, window, cx| {
2604 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2605 editor.buffer.update(cx, |buffer, cx| {
2606 buffer.edit(
2607 [
2608 (Point::new(1, 2)..Point::new(3, 0), ""),
2609 (Point::new(4, 2)..Point::new(6, 0), ""),
2610 ],
2611 None,
2612 cx,
2613 );
2614 assert_eq!(
2615 buffer.read(cx).text(),
2616 "
2617 a
2618 b()
2619 c()
2620 "
2621 .unindent()
2622 );
2623 });
2624 assert_eq!(
2625 editor.selections.ranges(cx),
2626 &[
2627 Point::new(1, 2)..Point::new(1, 2),
2628 Point::new(2, 2)..Point::new(2, 2),
2629 ],
2630 );
2631
2632 editor.newline(&Newline, window, cx);
2633 assert_eq!(
2634 editor.text(cx),
2635 "
2636 a
2637 b(
2638 )
2639 c(
2640 )
2641 "
2642 .unindent()
2643 );
2644
2645 // The selections are moved after the inserted newlines
2646 assert_eq!(
2647 editor.selections.ranges(cx),
2648 &[
2649 Point::new(2, 0)..Point::new(2, 0),
2650 Point::new(4, 0)..Point::new(4, 0),
2651 ],
2652 );
2653 });
2654}
2655
2656#[gpui::test]
2657async fn test_newline_above(cx: &mut TestAppContext) {
2658 init_test(cx, |settings| {
2659 settings.defaults.tab_size = NonZeroU32::new(4)
2660 });
2661
2662 let language = Arc::new(
2663 Language::new(
2664 LanguageConfig::default(),
2665 Some(tree_sitter_rust::LANGUAGE.into()),
2666 )
2667 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2668 .unwrap(),
2669 );
2670
2671 let mut cx = EditorTestContext::new(cx).await;
2672 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2673 cx.set_state(indoc! {"
2674 const a: ˇA = (
2675 (ˇ
2676 «const_functionˇ»(ˇ),
2677 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2678 )ˇ
2679 ˇ);ˇ
2680 "});
2681
2682 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2683 cx.assert_editor_state(indoc! {"
2684 ˇ
2685 const a: A = (
2686 ˇ
2687 (
2688 ˇ
2689 ˇ
2690 const_function(),
2691 ˇ
2692 ˇ
2693 ˇ
2694 ˇ
2695 something_else,
2696 ˇ
2697 )
2698 ˇ
2699 ˇ
2700 );
2701 "});
2702}
2703
2704#[gpui::test]
2705async fn test_newline_below(cx: &mut TestAppContext) {
2706 init_test(cx, |settings| {
2707 settings.defaults.tab_size = NonZeroU32::new(4)
2708 });
2709
2710 let language = Arc::new(
2711 Language::new(
2712 LanguageConfig::default(),
2713 Some(tree_sitter_rust::LANGUAGE.into()),
2714 )
2715 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2716 .unwrap(),
2717 );
2718
2719 let mut cx = EditorTestContext::new(cx).await;
2720 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2721 cx.set_state(indoc! {"
2722 const a: ˇA = (
2723 (ˇ
2724 «const_functionˇ»(ˇ),
2725 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2726 )ˇ
2727 ˇ);ˇ
2728 "});
2729
2730 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2731 cx.assert_editor_state(indoc! {"
2732 const a: A = (
2733 ˇ
2734 (
2735 ˇ
2736 const_function(),
2737 ˇ
2738 ˇ
2739 something_else,
2740 ˇ
2741 ˇ
2742 ˇ
2743 ˇ
2744 )
2745 ˇ
2746 );
2747 ˇ
2748 ˇ
2749 "});
2750}
2751
2752#[gpui::test]
2753async fn test_newline_comments(cx: &mut TestAppContext) {
2754 init_test(cx, |settings| {
2755 settings.defaults.tab_size = NonZeroU32::new(4)
2756 });
2757
2758 let language = Arc::new(Language::new(
2759 LanguageConfig {
2760 line_comments: vec!["// ".into()],
2761 ..LanguageConfig::default()
2762 },
2763 None,
2764 ));
2765 {
2766 let mut cx = EditorTestContext::new(cx).await;
2767 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2768 cx.set_state(indoc! {"
2769 // Fooˇ
2770 "});
2771
2772 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2773 cx.assert_editor_state(indoc! {"
2774 // Foo
2775 // ˇ
2776 "});
2777 // Ensure that we add comment prefix when existing line contains space
2778 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2779 cx.assert_editor_state(
2780 indoc! {"
2781 // Foo
2782 //s
2783 // ˇ
2784 "}
2785 .replace("s", " ") // s is used as space placeholder to prevent format on save
2786 .as_str(),
2787 );
2788 // Ensure that we add comment prefix when existing line does not contain space
2789 cx.set_state(indoc! {"
2790 // Foo
2791 //ˇ
2792 "});
2793 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2794 cx.assert_editor_state(indoc! {"
2795 // Foo
2796 //
2797 // ˇ
2798 "});
2799 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2800 cx.set_state(indoc! {"
2801 ˇ// Foo
2802 "});
2803 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2804 cx.assert_editor_state(indoc! {"
2805
2806 ˇ// Foo
2807 "});
2808 }
2809 // Ensure that comment continuations can be disabled.
2810 update_test_language_settings(cx, |settings| {
2811 settings.defaults.extend_comment_on_newline = Some(false);
2812 });
2813 let mut cx = EditorTestContext::new(cx).await;
2814 cx.set_state(indoc! {"
2815 // Fooˇ
2816 "});
2817 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2818 cx.assert_editor_state(indoc! {"
2819 // Foo
2820 ˇ
2821 "});
2822}
2823
2824#[gpui::test]
2825async fn test_newline_comments_with_multiple_delimiters(cx: &mut TestAppContext) {
2826 init_test(cx, |settings| {
2827 settings.defaults.tab_size = NonZeroU32::new(4)
2828 });
2829
2830 let language = Arc::new(Language::new(
2831 LanguageConfig {
2832 line_comments: vec!["// ".into(), "/// ".into()],
2833 ..LanguageConfig::default()
2834 },
2835 None,
2836 ));
2837 {
2838 let mut cx = EditorTestContext::new(cx).await;
2839 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2840 cx.set_state(indoc! {"
2841 //ˇ
2842 "});
2843 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2844 cx.assert_editor_state(indoc! {"
2845 //
2846 // ˇ
2847 "});
2848
2849 cx.set_state(indoc! {"
2850 ///ˇ
2851 "});
2852 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 ///
2855 /// ˇ
2856 "});
2857 }
2858}
2859
2860#[gpui::test]
2861async fn test_newline_documentation_comments(cx: &mut TestAppContext) {
2862 init_test(cx, |settings| {
2863 settings.defaults.tab_size = NonZeroU32::new(4)
2864 });
2865
2866 let language = Arc::new(
2867 Language::new(
2868 LanguageConfig {
2869 documentation: Some(language::DocumentationConfig {
2870 start: "/**".into(),
2871 end: "*/".into(),
2872 prefix: "* ".into(),
2873 tab_size: NonZeroU32::new(1).unwrap(),
2874 }),
2875
2876 ..LanguageConfig::default()
2877 },
2878 Some(tree_sitter_rust::LANGUAGE.into()),
2879 )
2880 .with_override_query("[(line_comment)(block_comment)] @comment.inclusive")
2881 .unwrap(),
2882 );
2883
2884 {
2885 let mut cx = EditorTestContext::new(cx).await;
2886 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2887 cx.set_state(indoc! {"
2888 /**ˇ
2889 "});
2890
2891 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2892 cx.assert_editor_state(indoc! {"
2893 /**
2894 * ˇ
2895 "});
2896 // Ensure that if cursor is before the comment start,
2897 // we do not actually insert a comment prefix.
2898 cx.set_state(indoc! {"
2899 ˇ/**
2900 "});
2901 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2902 cx.assert_editor_state(indoc! {"
2903
2904 ˇ/**
2905 "});
2906 // Ensure that if cursor is between it doesn't add comment prefix.
2907 cx.set_state(indoc! {"
2908 /*ˇ*
2909 "});
2910 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2911 cx.assert_editor_state(indoc! {"
2912 /*
2913 ˇ*
2914 "});
2915 // Ensure that if suffix exists on same line after cursor it adds new line.
2916 cx.set_state(indoc! {"
2917 /**ˇ*/
2918 "});
2919 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2920 cx.assert_editor_state(indoc! {"
2921 /**
2922 * ˇ
2923 */
2924 "});
2925 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2926 cx.set_state(indoc! {"
2927 /**ˇ */
2928 "});
2929 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2930 cx.assert_editor_state(indoc! {"
2931 /**
2932 * ˇ
2933 */
2934 "});
2935 // Ensure that if suffix exists on same line after cursor with space it adds new line.
2936 cx.set_state(indoc! {"
2937 /** ˇ*/
2938 "});
2939 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2940 cx.assert_editor_state(
2941 indoc! {"
2942 /**s
2943 * ˇ
2944 */
2945 "}
2946 .replace("s", " ") // s is used as space placeholder to prevent format on save
2947 .as_str(),
2948 );
2949 // Ensure that delimiter space is preserved when newline on already
2950 // spaced delimiter.
2951 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2952 cx.assert_editor_state(
2953 indoc! {"
2954 /**s
2955 *s
2956 * ˇ
2957 */
2958 "}
2959 .replace("s", " ") // s is used as space placeholder to prevent format on save
2960 .as_str(),
2961 );
2962 // Ensure that delimiter space is preserved when space is not
2963 // on existing delimiter.
2964 cx.set_state(indoc! {"
2965 /**
2966 *ˇ
2967 */
2968 "});
2969 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2970 cx.assert_editor_state(indoc! {"
2971 /**
2972 *
2973 * ˇ
2974 */
2975 "});
2976 // Ensure that if suffix exists on same line after cursor it
2977 // doesn't add extra new line if prefix is not on same line.
2978 cx.set_state(indoc! {"
2979 /**
2980 ˇ*/
2981 "});
2982 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2983 cx.assert_editor_state(indoc! {"
2984 /**
2985
2986 ˇ*/
2987 "});
2988 // Ensure that it detects suffix after existing prefix.
2989 cx.set_state(indoc! {"
2990 /**ˇ/
2991 "});
2992 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2993 cx.assert_editor_state(indoc! {"
2994 /**
2995 ˇ/
2996 "});
2997 // Ensure that if suffix exists on same line before
2998 // cursor it does not add comment prefix.
2999 cx.set_state(indoc! {"
3000 /** */ˇ
3001 "});
3002 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3003 cx.assert_editor_state(indoc! {"
3004 /** */
3005 ˇ
3006 "});
3007 // Ensure that if suffix exists on same line before
3008 // cursor it does not add comment prefix.
3009 cx.set_state(indoc! {"
3010 /**
3011 *
3012 */ˇ
3013 "});
3014 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3015 cx.assert_editor_state(indoc! {"
3016 /**
3017 *
3018 */
3019 ˇ
3020 "});
3021
3022 // Ensure that inline comment followed by code
3023 // doesn't add comment prefix on newline
3024 cx.set_state(indoc! {"
3025 /** */ textˇ
3026 "});
3027 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3028 cx.assert_editor_state(indoc! {"
3029 /** */ text
3030 ˇ
3031 "});
3032
3033 // Ensure that text after comment end tag
3034 // doesn't add comment prefix on newline
3035 cx.set_state(indoc! {"
3036 /**
3037 *
3038 */ˇtext
3039 "});
3040 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3041 cx.assert_editor_state(indoc! {"
3042 /**
3043 *
3044 */
3045 ˇtext
3046 "});
3047
3048 // Ensure if not comment block it doesn't
3049 // add comment prefix on newline
3050 cx.set_state(indoc! {"
3051 * textˇ
3052 "});
3053 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3054 cx.assert_editor_state(indoc! {"
3055 * text
3056 ˇ
3057 "});
3058 }
3059 // Ensure that comment continuations can be disabled.
3060 update_test_language_settings(cx, |settings| {
3061 settings.defaults.extend_comment_on_newline = Some(false);
3062 });
3063 let mut cx = EditorTestContext::new(cx).await;
3064 cx.set_state(indoc! {"
3065 /**ˇ
3066 "});
3067 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
3068 cx.assert_editor_state(indoc! {"
3069 /**
3070 ˇ
3071 "});
3072}
3073
3074#[gpui::test]
3075fn test_insert_with_old_selections(cx: &mut TestAppContext) {
3076 init_test(cx, |_| {});
3077
3078 let editor = cx.add_window(|window, cx| {
3079 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
3080 let mut editor = build_editor(buffer.clone(), window, cx);
3081 editor.change_selections(None, window, cx, |s| {
3082 s.select_ranges([3..4, 11..12, 19..20])
3083 });
3084 editor
3085 });
3086
3087 _ = editor.update(cx, |editor, window, cx| {
3088 // Edit the buffer directly, deleting ranges surrounding the editor's selections
3089 editor.buffer.update(cx, |buffer, cx| {
3090 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
3091 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
3092 });
3093 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
3094
3095 editor.insert("Z", window, cx);
3096 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
3097
3098 // The selections are moved after the inserted characters
3099 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
3100 });
3101}
3102
3103#[gpui::test]
3104async fn test_tab(cx: &mut TestAppContext) {
3105 init_test(cx, |settings| {
3106 settings.defaults.tab_size = NonZeroU32::new(3)
3107 });
3108
3109 let mut cx = EditorTestContext::new(cx).await;
3110 cx.set_state(indoc! {"
3111 ˇabˇc
3112 ˇ🏀ˇ🏀ˇefg
3113 dˇ
3114 "});
3115 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3116 cx.assert_editor_state(indoc! {"
3117 ˇab ˇc
3118 ˇ🏀 ˇ🏀 ˇefg
3119 d ˇ
3120 "});
3121
3122 cx.set_state(indoc! {"
3123 a
3124 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3125 "});
3126 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3127 cx.assert_editor_state(indoc! {"
3128 a
3129 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
3130 "});
3131}
3132
3133#[gpui::test]
3134async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
3135 init_test(cx, |_| {});
3136
3137 let mut cx = EditorTestContext::new(cx).await;
3138 let language = Arc::new(
3139 Language::new(
3140 LanguageConfig::default(),
3141 Some(tree_sitter_rust::LANGUAGE.into()),
3142 )
3143 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
3144 .unwrap(),
3145 );
3146 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3147
3148 // test when all cursors are not at suggested indent
3149 // then simply move to their suggested indent location
3150 cx.set_state(indoc! {"
3151 const a: B = (
3152 c(
3153 ˇ
3154 ˇ )
3155 );
3156 "});
3157 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3158 cx.assert_editor_state(indoc! {"
3159 const a: B = (
3160 c(
3161 ˇ
3162 ˇ)
3163 );
3164 "});
3165
3166 // test cursor already at suggested indent not moving when
3167 // other cursors are yet to reach their suggested indents
3168 cx.set_state(indoc! {"
3169 ˇ
3170 const a: B = (
3171 c(
3172 d(
3173 ˇ
3174 )
3175 ˇ
3176 ˇ )
3177 );
3178 "});
3179 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3180 cx.assert_editor_state(indoc! {"
3181 ˇ
3182 const a: B = (
3183 c(
3184 d(
3185 ˇ
3186 )
3187 ˇ
3188 ˇ)
3189 );
3190 "});
3191 // test when all cursors are at suggested indent then tab is inserted
3192 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3193 cx.assert_editor_state(indoc! {"
3194 ˇ
3195 const a: B = (
3196 c(
3197 d(
3198 ˇ
3199 )
3200 ˇ
3201 ˇ)
3202 );
3203 "});
3204
3205 // test when current indent is less than suggested indent,
3206 // we adjust line to match suggested indent and move cursor to it
3207 //
3208 // when no other cursor is at word boundary, all of them should move
3209 cx.set_state(indoc! {"
3210 const a: B = (
3211 c(
3212 d(
3213 ˇ
3214 ˇ )
3215 ˇ )
3216 );
3217 "});
3218 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3219 cx.assert_editor_state(indoc! {"
3220 const a: B = (
3221 c(
3222 d(
3223 ˇ
3224 ˇ)
3225 ˇ)
3226 );
3227 "});
3228
3229 // test when current indent is less than suggested indent,
3230 // we adjust line to match suggested indent and move cursor to it
3231 //
3232 // when some other cursor is at word boundary, it should not move
3233 cx.set_state(indoc! {"
3234 const a: B = (
3235 c(
3236 d(
3237 ˇ
3238 ˇ )
3239 ˇ)
3240 );
3241 "});
3242 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3243 cx.assert_editor_state(indoc! {"
3244 const a: B = (
3245 c(
3246 d(
3247 ˇ
3248 ˇ)
3249 ˇ)
3250 );
3251 "});
3252
3253 // test when current indent is more than suggested indent,
3254 // we just move cursor to current indent instead of suggested indent
3255 //
3256 // when no other cursor is at word boundary, all of them should move
3257 cx.set_state(indoc! {"
3258 const a: B = (
3259 c(
3260 d(
3261 ˇ
3262 ˇ )
3263 ˇ )
3264 );
3265 "});
3266 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3267 cx.assert_editor_state(indoc! {"
3268 const a: B = (
3269 c(
3270 d(
3271 ˇ
3272 ˇ)
3273 ˇ)
3274 );
3275 "});
3276 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3277 cx.assert_editor_state(indoc! {"
3278 const a: B = (
3279 c(
3280 d(
3281 ˇ
3282 ˇ)
3283 ˇ)
3284 );
3285 "});
3286
3287 // test when current indent is more than suggested indent,
3288 // we just move cursor to current indent instead of suggested indent
3289 //
3290 // when some other cursor is at word boundary, it doesn't move
3291 cx.set_state(indoc! {"
3292 const a: B = (
3293 c(
3294 d(
3295 ˇ
3296 ˇ )
3297 ˇ)
3298 );
3299 "});
3300 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3301 cx.assert_editor_state(indoc! {"
3302 const a: B = (
3303 c(
3304 d(
3305 ˇ
3306 ˇ)
3307 ˇ)
3308 );
3309 "});
3310
3311 // handle auto-indent when there are multiple cursors on the same line
3312 cx.set_state(indoc! {"
3313 const a: B = (
3314 c(
3315 ˇ ˇ
3316 ˇ )
3317 );
3318 "});
3319 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3320 cx.assert_editor_state(indoc! {"
3321 const a: B = (
3322 c(
3323 ˇ
3324 ˇ)
3325 );
3326 "});
3327}
3328
3329#[gpui::test]
3330async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3331 init_test(cx, |settings| {
3332 settings.defaults.tab_size = NonZeroU32::new(3)
3333 });
3334
3335 let mut cx = EditorTestContext::new(cx).await;
3336 cx.set_state(indoc! {"
3337 ˇ
3338 \t ˇ
3339 \t ˇ
3340 \t ˇ
3341 \t \t\t \t \t\t \t\t \t \t ˇ
3342 "});
3343
3344 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3345 cx.assert_editor_state(indoc! {"
3346 ˇ
3347 \t ˇ
3348 \t ˇ
3349 \t ˇ
3350 \t \t\t \t \t\t \t\t \t \t ˇ
3351 "});
3352}
3353
3354#[gpui::test]
3355async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3356 init_test(cx, |settings| {
3357 settings.defaults.tab_size = NonZeroU32::new(4)
3358 });
3359
3360 let language = Arc::new(
3361 Language::new(
3362 LanguageConfig::default(),
3363 Some(tree_sitter_rust::LANGUAGE.into()),
3364 )
3365 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3366 .unwrap(),
3367 );
3368
3369 let mut cx = EditorTestContext::new(cx).await;
3370 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3371 cx.set_state(indoc! {"
3372 fn a() {
3373 if b {
3374 \t ˇc
3375 }
3376 }
3377 "});
3378
3379 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3380 cx.assert_editor_state(indoc! {"
3381 fn a() {
3382 if b {
3383 ˇc
3384 }
3385 }
3386 "});
3387}
3388
3389#[gpui::test]
3390async fn test_indent_outdent(cx: &mut TestAppContext) {
3391 init_test(cx, |settings| {
3392 settings.defaults.tab_size = NonZeroU32::new(4);
3393 });
3394
3395 let mut cx = EditorTestContext::new(cx).await;
3396
3397 cx.set_state(indoc! {"
3398 «oneˇ» «twoˇ»
3399 three
3400 four
3401 "});
3402 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3403 cx.assert_editor_state(indoc! {"
3404 «oneˇ» «twoˇ»
3405 three
3406 four
3407 "});
3408
3409 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3410 cx.assert_editor_state(indoc! {"
3411 «oneˇ» «twoˇ»
3412 three
3413 four
3414 "});
3415
3416 // select across line ending
3417 cx.set_state(indoc! {"
3418 one two
3419 t«hree
3420 ˇ» four
3421 "});
3422 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3423 cx.assert_editor_state(indoc! {"
3424 one two
3425 t«hree
3426 ˇ» four
3427 "});
3428
3429 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3430 cx.assert_editor_state(indoc! {"
3431 one two
3432 t«hree
3433 ˇ» four
3434 "});
3435
3436 // Ensure that indenting/outdenting works when the cursor is at column 0.
3437 cx.set_state(indoc! {"
3438 one two
3439 ˇthree
3440 four
3441 "});
3442 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3443 cx.assert_editor_state(indoc! {"
3444 one two
3445 ˇthree
3446 four
3447 "});
3448
3449 cx.set_state(indoc! {"
3450 one two
3451 ˇ three
3452 four
3453 "});
3454 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3455 cx.assert_editor_state(indoc! {"
3456 one two
3457 ˇthree
3458 four
3459 "});
3460}
3461
3462#[gpui::test]
3463async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3464 init_test(cx, |settings| {
3465 settings.defaults.hard_tabs = Some(true);
3466 });
3467
3468 let mut cx = EditorTestContext::new(cx).await;
3469
3470 // select two ranges on one line
3471 cx.set_state(indoc! {"
3472 «oneˇ» «twoˇ»
3473 three
3474 four
3475 "});
3476 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3477 cx.assert_editor_state(indoc! {"
3478 \t«oneˇ» «twoˇ»
3479 three
3480 four
3481 "});
3482 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3483 cx.assert_editor_state(indoc! {"
3484 \t\t«oneˇ» «twoˇ»
3485 three
3486 four
3487 "});
3488 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3489 cx.assert_editor_state(indoc! {"
3490 \t«oneˇ» «twoˇ»
3491 three
3492 four
3493 "});
3494 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3495 cx.assert_editor_state(indoc! {"
3496 «oneˇ» «twoˇ»
3497 three
3498 four
3499 "});
3500
3501 // select across a line ending
3502 cx.set_state(indoc! {"
3503 one two
3504 t«hree
3505 ˇ»four
3506 "});
3507 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3508 cx.assert_editor_state(indoc! {"
3509 one two
3510 \tt«hree
3511 ˇ»four
3512 "});
3513 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3514 cx.assert_editor_state(indoc! {"
3515 one two
3516 \t\tt«hree
3517 ˇ»four
3518 "});
3519 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3520 cx.assert_editor_state(indoc! {"
3521 one two
3522 \tt«hree
3523 ˇ»four
3524 "});
3525 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3526 cx.assert_editor_state(indoc! {"
3527 one two
3528 t«hree
3529 ˇ»four
3530 "});
3531
3532 // Ensure that indenting/outdenting works when the cursor is at column 0.
3533 cx.set_state(indoc! {"
3534 one two
3535 ˇthree
3536 four
3537 "});
3538 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3539 cx.assert_editor_state(indoc! {"
3540 one two
3541 ˇthree
3542 four
3543 "});
3544 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3545 cx.assert_editor_state(indoc! {"
3546 one two
3547 \tˇthree
3548 four
3549 "});
3550 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3551 cx.assert_editor_state(indoc! {"
3552 one two
3553 ˇthree
3554 four
3555 "});
3556}
3557
3558#[gpui::test]
3559fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3560 init_test(cx, |settings| {
3561 settings.languages.extend([
3562 (
3563 "TOML".into(),
3564 LanguageSettingsContent {
3565 tab_size: NonZeroU32::new(2),
3566 ..Default::default()
3567 },
3568 ),
3569 (
3570 "Rust".into(),
3571 LanguageSettingsContent {
3572 tab_size: NonZeroU32::new(4),
3573 ..Default::default()
3574 },
3575 ),
3576 ]);
3577 });
3578
3579 let toml_language = Arc::new(Language::new(
3580 LanguageConfig {
3581 name: "TOML".into(),
3582 ..Default::default()
3583 },
3584 None,
3585 ));
3586 let rust_language = Arc::new(Language::new(
3587 LanguageConfig {
3588 name: "Rust".into(),
3589 ..Default::default()
3590 },
3591 None,
3592 ));
3593
3594 let toml_buffer =
3595 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3596 let rust_buffer =
3597 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3598 let multibuffer = cx.new(|cx| {
3599 let mut multibuffer = MultiBuffer::new(ReadWrite);
3600 multibuffer.push_excerpts(
3601 toml_buffer.clone(),
3602 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3603 cx,
3604 );
3605 multibuffer.push_excerpts(
3606 rust_buffer.clone(),
3607 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3608 cx,
3609 );
3610 multibuffer
3611 });
3612
3613 cx.add_window(|window, cx| {
3614 let mut editor = build_editor(multibuffer, window, cx);
3615
3616 assert_eq!(
3617 editor.text(cx),
3618 indoc! {"
3619 a = 1
3620 b = 2
3621
3622 const c: usize = 3;
3623 "}
3624 );
3625
3626 select_ranges(
3627 &mut editor,
3628 indoc! {"
3629 «aˇ» = 1
3630 b = 2
3631
3632 «const c:ˇ» usize = 3;
3633 "},
3634 window,
3635 cx,
3636 );
3637
3638 editor.tab(&Tab, window, cx);
3639 assert_text_with_selections(
3640 &mut editor,
3641 indoc! {"
3642 «aˇ» = 1
3643 b = 2
3644
3645 «const c:ˇ» usize = 3;
3646 "},
3647 cx,
3648 );
3649 editor.backtab(&Backtab, window, cx);
3650 assert_text_with_selections(
3651 &mut editor,
3652 indoc! {"
3653 «aˇ» = 1
3654 b = 2
3655
3656 «const c:ˇ» usize = 3;
3657 "},
3658 cx,
3659 );
3660
3661 editor
3662 });
3663}
3664
3665#[gpui::test]
3666async fn test_backspace(cx: &mut TestAppContext) {
3667 init_test(cx, |_| {});
3668
3669 let mut cx = EditorTestContext::new(cx).await;
3670
3671 // Basic backspace
3672 cx.set_state(indoc! {"
3673 onˇe two three
3674 fou«rˇ» five six
3675 seven «ˇeight nine
3676 »ten
3677 "});
3678 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3679 cx.assert_editor_state(indoc! {"
3680 oˇe two three
3681 fouˇ five six
3682 seven ˇten
3683 "});
3684
3685 // Test backspace inside and around indents
3686 cx.set_state(indoc! {"
3687 zero
3688 ˇone
3689 ˇtwo
3690 ˇ ˇ ˇ three
3691 ˇ ˇ four
3692 "});
3693 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3694 cx.assert_editor_state(indoc! {"
3695 zero
3696 ˇone
3697 ˇtwo
3698 ˇ threeˇ four
3699 "});
3700}
3701
3702#[gpui::test]
3703async fn test_delete(cx: &mut TestAppContext) {
3704 init_test(cx, |_| {});
3705
3706 let mut cx = EditorTestContext::new(cx).await;
3707 cx.set_state(indoc! {"
3708 onˇe two three
3709 fou«rˇ» five six
3710 seven «ˇeight nine
3711 »ten
3712 "});
3713 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3714 cx.assert_editor_state(indoc! {"
3715 onˇ two three
3716 fouˇ five six
3717 seven ˇten
3718 "});
3719}
3720
3721#[gpui::test]
3722fn test_delete_line(cx: &mut TestAppContext) {
3723 init_test(cx, |_| {});
3724
3725 let editor = cx.add_window(|window, cx| {
3726 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3727 build_editor(buffer, window, cx)
3728 });
3729 _ = editor.update(cx, |editor, window, cx| {
3730 editor.change_selections(None, window, cx, |s| {
3731 s.select_display_ranges([
3732 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3733 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3734 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3735 ])
3736 });
3737 editor.delete_line(&DeleteLine, window, cx);
3738 assert_eq!(editor.display_text(cx), "ghi");
3739 assert_eq!(
3740 editor.selections.display_ranges(cx),
3741 vec![
3742 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3743 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3744 ]
3745 );
3746 });
3747
3748 let editor = cx.add_window(|window, cx| {
3749 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3750 build_editor(buffer, window, cx)
3751 });
3752 _ = editor.update(cx, |editor, window, cx| {
3753 editor.change_selections(None, window, cx, |s| {
3754 s.select_display_ranges([
3755 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3756 ])
3757 });
3758 editor.delete_line(&DeleteLine, window, cx);
3759 assert_eq!(editor.display_text(cx), "ghi\n");
3760 assert_eq!(
3761 editor.selections.display_ranges(cx),
3762 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3763 );
3764 });
3765}
3766
3767#[gpui::test]
3768fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3769 init_test(cx, |_| {});
3770
3771 cx.add_window(|window, cx| {
3772 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3773 let mut editor = build_editor(buffer.clone(), window, cx);
3774 let buffer = buffer.read(cx).as_singleton().unwrap();
3775
3776 assert_eq!(
3777 editor.selections.ranges::<Point>(cx),
3778 &[Point::new(0, 0)..Point::new(0, 0)]
3779 );
3780
3781 // When on single line, replace newline at end by space
3782 editor.join_lines(&JoinLines, window, cx);
3783 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3784 assert_eq!(
3785 editor.selections.ranges::<Point>(cx),
3786 &[Point::new(0, 3)..Point::new(0, 3)]
3787 );
3788
3789 // When multiple lines are selected, remove newlines that are spanned by the selection
3790 editor.change_selections(None, window, cx, |s| {
3791 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3792 });
3793 editor.join_lines(&JoinLines, window, cx);
3794 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3795 assert_eq!(
3796 editor.selections.ranges::<Point>(cx),
3797 &[Point::new(0, 11)..Point::new(0, 11)]
3798 );
3799
3800 // Undo should be transactional
3801 editor.undo(&Undo, window, cx);
3802 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3803 assert_eq!(
3804 editor.selections.ranges::<Point>(cx),
3805 &[Point::new(0, 5)..Point::new(2, 2)]
3806 );
3807
3808 // When joining an empty line don't insert a space
3809 editor.change_selections(None, window, cx, |s| {
3810 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3811 });
3812 editor.join_lines(&JoinLines, window, cx);
3813 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3814 assert_eq!(
3815 editor.selections.ranges::<Point>(cx),
3816 [Point::new(2, 3)..Point::new(2, 3)]
3817 );
3818
3819 // We can remove trailing newlines
3820 editor.join_lines(&JoinLines, window, cx);
3821 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3822 assert_eq!(
3823 editor.selections.ranges::<Point>(cx),
3824 [Point::new(2, 3)..Point::new(2, 3)]
3825 );
3826
3827 // We don't blow up on the last line
3828 editor.join_lines(&JoinLines, window, cx);
3829 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3830 assert_eq!(
3831 editor.selections.ranges::<Point>(cx),
3832 [Point::new(2, 3)..Point::new(2, 3)]
3833 );
3834
3835 // reset to test indentation
3836 editor.buffer.update(cx, |buffer, cx| {
3837 buffer.edit(
3838 [
3839 (Point::new(1, 0)..Point::new(1, 2), " "),
3840 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3841 ],
3842 None,
3843 cx,
3844 )
3845 });
3846
3847 // We remove any leading spaces
3848 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3849 editor.change_selections(None, window, cx, |s| {
3850 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3851 });
3852 editor.join_lines(&JoinLines, window, cx);
3853 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3854
3855 // We don't insert a space for a line containing only spaces
3856 editor.join_lines(&JoinLines, window, cx);
3857 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3858
3859 // We ignore any leading tabs
3860 editor.join_lines(&JoinLines, window, cx);
3861 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3862
3863 editor
3864 });
3865}
3866
3867#[gpui::test]
3868fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3869 init_test(cx, |_| {});
3870
3871 cx.add_window(|window, cx| {
3872 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3873 let mut editor = build_editor(buffer.clone(), window, cx);
3874 let buffer = buffer.read(cx).as_singleton().unwrap();
3875
3876 editor.change_selections(None, window, cx, |s| {
3877 s.select_ranges([
3878 Point::new(0, 2)..Point::new(1, 1),
3879 Point::new(1, 2)..Point::new(1, 2),
3880 Point::new(3, 1)..Point::new(3, 2),
3881 ])
3882 });
3883
3884 editor.join_lines(&JoinLines, window, cx);
3885 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3886
3887 assert_eq!(
3888 editor.selections.ranges::<Point>(cx),
3889 [
3890 Point::new(0, 7)..Point::new(0, 7),
3891 Point::new(1, 3)..Point::new(1, 3)
3892 ]
3893 );
3894 editor
3895 });
3896}
3897
3898#[gpui::test]
3899async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3900 init_test(cx, |_| {});
3901
3902 let mut cx = EditorTestContext::new(cx).await;
3903
3904 let diff_base = r#"
3905 Line 0
3906 Line 1
3907 Line 2
3908 Line 3
3909 "#
3910 .unindent();
3911
3912 cx.set_state(
3913 &r#"
3914 ˇLine 0
3915 Line 1
3916 Line 2
3917 Line 3
3918 "#
3919 .unindent(),
3920 );
3921
3922 cx.set_head_text(&diff_base);
3923 executor.run_until_parked();
3924
3925 // Join lines
3926 cx.update_editor(|editor, window, cx| {
3927 editor.join_lines(&JoinLines, window, cx);
3928 });
3929 executor.run_until_parked();
3930
3931 cx.assert_editor_state(
3932 &r#"
3933 Line 0ˇ Line 1
3934 Line 2
3935 Line 3
3936 "#
3937 .unindent(),
3938 );
3939 // Join again
3940 cx.update_editor(|editor, window, cx| {
3941 editor.join_lines(&JoinLines, window, cx);
3942 });
3943 executor.run_until_parked();
3944
3945 cx.assert_editor_state(
3946 &r#"
3947 Line 0 Line 1ˇ Line 2
3948 Line 3
3949 "#
3950 .unindent(),
3951 );
3952}
3953
3954#[gpui::test]
3955async fn test_custom_newlines_cause_no_false_positive_diffs(
3956 executor: BackgroundExecutor,
3957 cx: &mut TestAppContext,
3958) {
3959 init_test(cx, |_| {});
3960 let mut cx = EditorTestContext::new(cx).await;
3961 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3962 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3963 executor.run_until_parked();
3964
3965 cx.update_editor(|editor, window, cx| {
3966 let snapshot = editor.snapshot(window, cx);
3967 assert_eq!(
3968 snapshot
3969 .buffer_snapshot
3970 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3971 .collect::<Vec<_>>(),
3972 Vec::new(),
3973 "Should not have any diffs for files with custom newlines"
3974 );
3975 });
3976}
3977
3978#[gpui::test]
3979async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3980 init_test(cx, |_| {});
3981
3982 let mut cx = EditorTestContext::new(cx).await;
3983
3984 // Test sort_lines_case_insensitive()
3985 cx.set_state(indoc! {"
3986 «z
3987 y
3988 x
3989 Z
3990 Y
3991 Xˇ»
3992 "});
3993 cx.update_editor(|e, window, cx| {
3994 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3995 });
3996 cx.assert_editor_state(indoc! {"
3997 «x
3998 X
3999 y
4000 Y
4001 z
4002 Zˇ»
4003 "});
4004
4005 // Test reverse_lines()
4006 cx.set_state(indoc! {"
4007 «5
4008 4
4009 3
4010 2
4011 1ˇ»
4012 "});
4013 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
4014 cx.assert_editor_state(indoc! {"
4015 «1
4016 2
4017 3
4018 4
4019 5ˇ»
4020 "});
4021
4022 // Skip testing shuffle_line()
4023
4024 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
4025 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
4026
4027 // Don't manipulate when cursor is on single line, but expand the selection
4028 cx.set_state(indoc! {"
4029 ddˇdd
4030 ccc
4031 bb
4032 a
4033 "});
4034 cx.update_editor(|e, window, cx| {
4035 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4036 });
4037 cx.assert_editor_state(indoc! {"
4038 «ddddˇ»
4039 ccc
4040 bb
4041 a
4042 "});
4043
4044 // Basic manipulate case
4045 // Start selection moves to column 0
4046 // End of selection shrinks to fit shorter line
4047 cx.set_state(indoc! {"
4048 dd«d
4049 ccc
4050 bb
4051 aaaaaˇ»
4052 "});
4053 cx.update_editor(|e, window, cx| {
4054 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4055 });
4056 cx.assert_editor_state(indoc! {"
4057 «aaaaa
4058 bb
4059 ccc
4060 dddˇ»
4061 "});
4062
4063 // Manipulate case with newlines
4064 cx.set_state(indoc! {"
4065 dd«d
4066 ccc
4067
4068 bb
4069 aaaaa
4070
4071 ˇ»
4072 "});
4073 cx.update_editor(|e, window, cx| {
4074 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4075 });
4076 cx.assert_editor_state(indoc! {"
4077 «
4078
4079 aaaaa
4080 bb
4081 ccc
4082 dddˇ»
4083
4084 "});
4085
4086 // Adding new line
4087 cx.set_state(indoc! {"
4088 aa«a
4089 bbˇ»b
4090 "});
4091 cx.update_editor(|e, window, cx| {
4092 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
4093 });
4094 cx.assert_editor_state(indoc! {"
4095 «aaa
4096 bbb
4097 added_lineˇ»
4098 "});
4099
4100 // Removing line
4101 cx.set_state(indoc! {"
4102 aa«a
4103 bbbˇ»
4104 "});
4105 cx.update_editor(|e, window, cx| {
4106 e.manipulate_lines(window, cx, |lines| {
4107 lines.pop();
4108 })
4109 });
4110 cx.assert_editor_state(indoc! {"
4111 «aaaˇ»
4112 "});
4113
4114 // Removing all lines
4115 cx.set_state(indoc! {"
4116 aa«a
4117 bbbˇ»
4118 "});
4119 cx.update_editor(|e, window, cx| {
4120 e.manipulate_lines(window, cx, |lines| {
4121 lines.drain(..);
4122 })
4123 });
4124 cx.assert_editor_state(indoc! {"
4125 ˇ
4126 "});
4127}
4128
4129#[gpui::test]
4130async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
4131 init_test(cx, |_| {});
4132
4133 let mut cx = EditorTestContext::new(cx).await;
4134
4135 // Consider continuous selection as single selection
4136 cx.set_state(indoc! {"
4137 Aaa«aa
4138 cˇ»c«c
4139 bb
4140 aaaˇ»aa
4141 "});
4142 cx.update_editor(|e, window, cx| {
4143 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4144 });
4145 cx.assert_editor_state(indoc! {"
4146 «Aaaaa
4147 ccc
4148 bb
4149 aaaaaˇ»
4150 "});
4151
4152 cx.set_state(indoc! {"
4153 Aaa«aa
4154 cˇ»c«c
4155 bb
4156 aaaˇ»aa
4157 "});
4158 cx.update_editor(|e, window, cx| {
4159 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4160 });
4161 cx.assert_editor_state(indoc! {"
4162 «Aaaaa
4163 ccc
4164 bbˇ»
4165 "});
4166
4167 // Consider non continuous selection as distinct dedup operations
4168 cx.set_state(indoc! {"
4169 «aaaaa
4170 bb
4171 aaaaa
4172 aaaaaˇ»
4173
4174 aaa«aaˇ»
4175 "});
4176 cx.update_editor(|e, window, cx| {
4177 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4178 });
4179 cx.assert_editor_state(indoc! {"
4180 «aaaaa
4181 bbˇ»
4182
4183 «aaaaaˇ»
4184 "});
4185}
4186
4187#[gpui::test]
4188async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
4189 init_test(cx, |_| {});
4190
4191 let mut cx = EditorTestContext::new(cx).await;
4192
4193 cx.set_state(indoc! {"
4194 «Aaa
4195 aAa
4196 Aaaˇ»
4197 "});
4198 cx.update_editor(|e, window, cx| {
4199 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
4200 });
4201 cx.assert_editor_state(indoc! {"
4202 «Aaa
4203 aAaˇ»
4204 "});
4205
4206 cx.set_state(indoc! {"
4207 «Aaa
4208 aAa
4209 aaAˇ»
4210 "});
4211 cx.update_editor(|e, window, cx| {
4212 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
4213 });
4214 cx.assert_editor_state(indoc! {"
4215 «Aaaˇ»
4216 "});
4217}
4218
4219#[gpui::test]
4220async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
4221 init_test(cx, |_| {});
4222
4223 let mut cx = EditorTestContext::new(cx).await;
4224
4225 // Manipulate with multiple selections on a single line
4226 cx.set_state(indoc! {"
4227 dd«dd
4228 cˇ»c«c
4229 bb
4230 aaaˇ»aa
4231 "});
4232 cx.update_editor(|e, window, cx| {
4233 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4234 });
4235 cx.assert_editor_state(indoc! {"
4236 «aaaaa
4237 bb
4238 ccc
4239 ddddˇ»
4240 "});
4241
4242 // Manipulate with multiple disjoin selections
4243 cx.set_state(indoc! {"
4244 5«
4245 4
4246 3
4247 2
4248 1ˇ»
4249
4250 dd«dd
4251 ccc
4252 bb
4253 aaaˇ»aa
4254 "});
4255 cx.update_editor(|e, window, cx| {
4256 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
4257 });
4258 cx.assert_editor_state(indoc! {"
4259 «1
4260 2
4261 3
4262 4
4263 5ˇ»
4264
4265 «aaaaa
4266 bb
4267 ccc
4268 ddddˇ»
4269 "});
4270
4271 // Adding lines on each selection
4272 cx.set_state(indoc! {"
4273 2«
4274 1ˇ»
4275
4276 bb«bb
4277 aaaˇ»aa
4278 "});
4279 cx.update_editor(|e, window, cx| {
4280 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4281 });
4282 cx.assert_editor_state(indoc! {"
4283 «2
4284 1
4285 added lineˇ»
4286
4287 «bbbb
4288 aaaaa
4289 added lineˇ»
4290 "});
4291
4292 // Removing lines on each selection
4293 cx.set_state(indoc! {"
4294 2«
4295 1ˇ»
4296
4297 bb«bb
4298 aaaˇ»aa
4299 "});
4300 cx.update_editor(|e, window, cx| {
4301 e.manipulate_lines(window, cx, |lines| {
4302 lines.pop();
4303 })
4304 });
4305 cx.assert_editor_state(indoc! {"
4306 «2ˇ»
4307
4308 «bbbbˇ»
4309 "});
4310}
4311
4312#[gpui::test]
4313async fn test_toggle_case(cx: &mut TestAppContext) {
4314 init_test(cx, |_| {});
4315
4316 let mut cx = EditorTestContext::new(cx).await;
4317
4318 // If all lower case -> upper case
4319 cx.set_state(indoc! {"
4320 «hello worldˇ»
4321 "});
4322 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4323 cx.assert_editor_state(indoc! {"
4324 «HELLO WORLDˇ»
4325 "});
4326
4327 // If all upper case -> lower case
4328 cx.set_state(indoc! {"
4329 «HELLO WORLDˇ»
4330 "});
4331 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4332 cx.assert_editor_state(indoc! {"
4333 «hello worldˇ»
4334 "});
4335
4336 // If any upper case characters are identified -> lower case
4337 // This matches JetBrains IDEs
4338 cx.set_state(indoc! {"
4339 «hEllo worldˇ»
4340 "});
4341 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4342 cx.assert_editor_state(indoc! {"
4343 «hello worldˇ»
4344 "});
4345}
4346
4347#[gpui::test]
4348async fn test_manipulate_text(cx: &mut TestAppContext) {
4349 init_test(cx, |_| {});
4350
4351 let mut cx = EditorTestContext::new(cx).await;
4352
4353 // Test convert_to_upper_case()
4354 cx.set_state(indoc! {"
4355 «hello worldˇ»
4356 "});
4357 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4358 cx.assert_editor_state(indoc! {"
4359 «HELLO WORLDˇ»
4360 "});
4361
4362 // Test convert_to_lower_case()
4363 cx.set_state(indoc! {"
4364 «HELLO WORLDˇ»
4365 "});
4366 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4367 cx.assert_editor_state(indoc! {"
4368 «hello worldˇ»
4369 "});
4370
4371 // Test multiple line, single selection case
4372 cx.set_state(indoc! {"
4373 «The quick brown
4374 fox jumps over
4375 the lazy dogˇ»
4376 "});
4377 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4378 cx.assert_editor_state(indoc! {"
4379 «The Quick Brown
4380 Fox Jumps Over
4381 The Lazy Dogˇ»
4382 "});
4383
4384 // Test multiple line, single selection case
4385 cx.set_state(indoc! {"
4386 «The quick brown
4387 fox jumps over
4388 the lazy dogˇ»
4389 "});
4390 cx.update_editor(|e, window, cx| {
4391 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4392 });
4393 cx.assert_editor_state(indoc! {"
4394 «TheQuickBrown
4395 FoxJumpsOver
4396 TheLazyDogˇ»
4397 "});
4398
4399 // From here on out, test more complex cases of manipulate_text()
4400
4401 // Test no selection case - should affect words cursors are in
4402 // Cursor at beginning, middle, and end of word
4403 cx.set_state(indoc! {"
4404 ˇhello big beauˇtiful worldˇ
4405 "});
4406 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4407 cx.assert_editor_state(indoc! {"
4408 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4409 "});
4410
4411 // Test multiple selections on a single line and across multiple lines
4412 cx.set_state(indoc! {"
4413 «Theˇ» quick «brown
4414 foxˇ» jumps «overˇ»
4415 the «lazyˇ» dog
4416 "});
4417 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4418 cx.assert_editor_state(indoc! {"
4419 «THEˇ» quick «BROWN
4420 FOXˇ» jumps «OVERˇ»
4421 the «LAZYˇ» dog
4422 "});
4423
4424 // Test case where text length grows
4425 cx.set_state(indoc! {"
4426 «tschüߡ»
4427 "});
4428 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4429 cx.assert_editor_state(indoc! {"
4430 «TSCHÜSSˇ»
4431 "});
4432
4433 // Test to make sure we don't crash when text shrinks
4434 cx.set_state(indoc! {"
4435 aaa_bbbˇ
4436 "});
4437 cx.update_editor(|e, window, cx| {
4438 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4439 });
4440 cx.assert_editor_state(indoc! {"
4441 «aaaBbbˇ»
4442 "});
4443
4444 // Test to make sure we all aware of the fact that each word can grow and shrink
4445 // Final selections should be aware of this fact
4446 cx.set_state(indoc! {"
4447 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4448 "});
4449 cx.update_editor(|e, window, cx| {
4450 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4451 });
4452 cx.assert_editor_state(indoc! {"
4453 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4454 "});
4455
4456 cx.set_state(indoc! {"
4457 «hElLo, WoRld!ˇ»
4458 "});
4459 cx.update_editor(|e, window, cx| {
4460 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4461 });
4462 cx.assert_editor_state(indoc! {"
4463 «HeLlO, wOrLD!ˇ»
4464 "});
4465}
4466
4467#[gpui::test]
4468fn test_duplicate_line(cx: &mut TestAppContext) {
4469 init_test(cx, |_| {});
4470
4471 let editor = cx.add_window(|window, cx| {
4472 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4473 build_editor(buffer, window, cx)
4474 });
4475 _ = editor.update(cx, |editor, window, cx| {
4476 editor.change_selections(None, window, cx, |s| {
4477 s.select_display_ranges([
4478 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4479 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4480 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4481 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4482 ])
4483 });
4484 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4485 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4486 assert_eq!(
4487 editor.selections.display_ranges(cx),
4488 vec![
4489 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4490 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4491 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4492 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4493 ]
4494 );
4495 });
4496
4497 let editor = cx.add_window(|window, cx| {
4498 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4499 build_editor(buffer, window, cx)
4500 });
4501 _ = editor.update(cx, |editor, window, cx| {
4502 editor.change_selections(None, window, cx, |s| {
4503 s.select_display_ranges([
4504 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4505 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4506 ])
4507 });
4508 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4509 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4510 assert_eq!(
4511 editor.selections.display_ranges(cx),
4512 vec![
4513 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4514 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4515 ]
4516 );
4517 });
4518
4519 // With `move_upwards` the selections stay in place, except for
4520 // the lines inserted above them
4521 let editor = cx.add_window(|window, cx| {
4522 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4523 build_editor(buffer, window, cx)
4524 });
4525 _ = editor.update(cx, |editor, window, cx| {
4526 editor.change_selections(None, window, cx, |s| {
4527 s.select_display_ranges([
4528 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4529 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4530 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4531 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4532 ])
4533 });
4534 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4535 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4536 assert_eq!(
4537 editor.selections.display_ranges(cx),
4538 vec![
4539 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4540 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4541 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4542 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4543 ]
4544 );
4545 });
4546
4547 let editor = cx.add_window(|window, cx| {
4548 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4549 build_editor(buffer, window, cx)
4550 });
4551 _ = editor.update(cx, |editor, window, cx| {
4552 editor.change_selections(None, window, cx, |s| {
4553 s.select_display_ranges([
4554 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4555 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4556 ])
4557 });
4558 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4559 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4560 assert_eq!(
4561 editor.selections.display_ranges(cx),
4562 vec![
4563 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4564 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4565 ]
4566 );
4567 });
4568
4569 let editor = cx.add_window(|window, cx| {
4570 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4571 build_editor(buffer, window, cx)
4572 });
4573 _ = editor.update(cx, |editor, window, cx| {
4574 editor.change_selections(None, window, cx, |s| {
4575 s.select_display_ranges([
4576 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4577 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4578 ])
4579 });
4580 editor.duplicate_selection(&DuplicateSelection, window, cx);
4581 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4582 assert_eq!(
4583 editor.selections.display_ranges(cx),
4584 vec![
4585 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4586 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4587 ]
4588 );
4589 });
4590}
4591
4592#[gpui::test]
4593fn test_move_line_up_down(cx: &mut TestAppContext) {
4594 init_test(cx, |_| {});
4595
4596 let editor = cx.add_window(|window, cx| {
4597 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4598 build_editor(buffer, window, cx)
4599 });
4600 _ = editor.update(cx, |editor, window, cx| {
4601 editor.fold_creases(
4602 vec![
4603 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4604 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4605 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4606 ],
4607 true,
4608 window,
4609 cx,
4610 );
4611 editor.change_selections(None, window, cx, |s| {
4612 s.select_display_ranges([
4613 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4614 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4615 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4616 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4617 ])
4618 });
4619 assert_eq!(
4620 editor.display_text(cx),
4621 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4622 );
4623
4624 editor.move_line_up(&MoveLineUp, window, cx);
4625 assert_eq!(
4626 editor.display_text(cx),
4627 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4628 );
4629 assert_eq!(
4630 editor.selections.display_ranges(cx),
4631 vec![
4632 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4633 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4634 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4635 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4636 ]
4637 );
4638 });
4639
4640 _ = editor.update(cx, |editor, window, cx| {
4641 editor.move_line_down(&MoveLineDown, window, cx);
4642 assert_eq!(
4643 editor.display_text(cx),
4644 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4645 );
4646 assert_eq!(
4647 editor.selections.display_ranges(cx),
4648 vec![
4649 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4650 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4651 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4652 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4653 ]
4654 );
4655 });
4656
4657 _ = editor.update(cx, |editor, window, cx| {
4658 editor.move_line_down(&MoveLineDown, window, cx);
4659 assert_eq!(
4660 editor.display_text(cx),
4661 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4662 );
4663 assert_eq!(
4664 editor.selections.display_ranges(cx),
4665 vec![
4666 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4667 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4668 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4669 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4670 ]
4671 );
4672 });
4673
4674 _ = editor.update(cx, |editor, window, cx| {
4675 editor.move_line_up(&MoveLineUp, window, cx);
4676 assert_eq!(
4677 editor.display_text(cx),
4678 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4679 );
4680 assert_eq!(
4681 editor.selections.display_ranges(cx),
4682 vec![
4683 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4684 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4685 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4686 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4687 ]
4688 );
4689 });
4690}
4691
4692#[gpui::test]
4693fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4694 init_test(cx, |_| {});
4695
4696 let editor = cx.add_window(|window, cx| {
4697 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4698 build_editor(buffer, window, cx)
4699 });
4700 _ = editor.update(cx, |editor, window, cx| {
4701 let snapshot = editor.buffer.read(cx).snapshot(cx);
4702 editor.insert_blocks(
4703 [BlockProperties {
4704 style: BlockStyle::Fixed,
4705 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4706 height: Some(1),
4707 render: Arc::new(|_| div().into_any()),
4708 priority: 0,
4709 render_in_minimap: true,
4710 }],
4711 Some(Autoscroll::fit()),
4712 cx,
4713 );
4714 editor.change_selections(None, window, cx, |s| {
4715 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4716 });
4717 editor.move_line_down(&MoveLineDown, window, cx);
4718 });
4719}
4720
4721#[gpui::test]
4722async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4723 init_test(cx, |_| {});
4724
4725 let mut cx = EditorTestContext::new(cx).await;
4726 cx.set_state(
4727 &"
4728 ˇzero
4729 one
4730 two
4731 three
4732 four
4733 five
4734 "
4735 .unindent(),
4736 );
4737
4738 // Create a four-line block that replaces three lines of text.
4739 cx.update_editor(|editor, window, cx| {
4740 let snapshot = editor.snapshot(window, cx);
4741 let snapshot = &snapshot.buffer_snapshot;
4742 let placement = BlockPlacement::Replace(
4743 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4744 );
4745 editor.insert_blocks(
4746 [BlockProperties {
4747 placement,
4748 height: Some(4),
4749 style: BlockStyle::Sticky,
4750 render: Arc::new(|_| gpui::div().into_any_element()),
4751 priority: 0,
4752 render_in_minimap: true,
4753 }],
4754 None,
4755 cx,
4756 );
4757 });
4758
4759 // Move down so that the cursor touches the block.
4760 cx.update_editor(|editor, window, cx| {
4761 editor.move_down(&Default::default(), window, cx);
4762 });
4763 cx.assert_editor_state(
4764 &"
4765 zero
4766 «one
4767 two
4768 threeˇ»
4769 four
4770 five
4771 "
4772 .unindent(),
4773 );
4774
4775 // Move down past the block.
4776 cx.update_editor(|editor, window, cx| {
4777 editor.move_down(&Default::default(), window, cx);
4778 });
4779 cx.assert_editor_state(
4780 &"
4781 zero
4782 one
4783 two
4784 three
4785 ˇfour
4786 five
4787 "
4788 .unindent(),
4789 );
4790}
4791
4792#[gpui::test]
4793fn test_transpose(cx: &mut TestAppContext) {
4794 init_test(cx, |_| {});
4795
4796 _ = cx.add_window(|window, cx| {
4797 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4798 editor.set_style(EditorStyle::default(), window, cx);
4799 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4800 editor.transpose(&Default::default(), window, cx);
4801 assert_eq!(editor.text(cx), "bac");
4802 assert_eq!(editor.selections.ranges(cx), [2..2]);
4803
4804 editor.transpose(&Default::default(), window, cx);
4805 assert_eq!(editor.text(cx), "bca");
4806 assert_eq!(editor.selections.ranges(cx), [3..3]);
4807
4808 editor.transpose(&Default::default(), window, cx);
4809 assert_eq!(editor.text(cx), "bac");
4810 assert_eq!(editor.selections.ranges(cx), [3..3]);
4811
4812 editor
4813 });
4814
4815 _ = cx.add_window(|window, cx| {
4816 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4817 editor.set_style(EditorStyle::default(), window, cx);
4818 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4819 editor.transpose(&Default::default(), window, cx);
4820 assert_eq!(editor.text(cx), "acb\nde");
4821 assert_eq!(editor.selections.ranges(cx), [3..3]);
4822
4823 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4824 editor.transpose(&Default::default(), window, cx);
4825 assert_eq!(editor.text(cx), "acbd\ne");
4826 assert_eq!(editor.selections.ranges(cx), [5..5]);
4827
4828 editor.transpose(&Default::default(), window, cx);
4829 assert_eq!(editor.text(cx), "acbde\n");
4830 assert_eq!(editor.selections.ranges(cx), [6..6]);
4831
4832 editor.transpose(&Default::default(), window, cx);
4833 assert_eq!(editor.text(cx), "acbd\ne");
4834 assert_eq!(editor.selections.ranges(cx), [6..6]);
4835
4836 editor
4837 });
4838
4839 _ = cx.add_window(|window, cx| {
4840 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4841 editor.set_style(EditorStyle::default(), window, cx);
4842 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4843 editor.transpose(&Default::default(), window, cx);
4844 assert_eq!(editor.text(cx), "bacd\ne");
4845 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4846
4847 editor.transpose(&Default::default(), window, cx);
4848 assert_eq!(editor.text(cx), "bcade\n");
4849 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4850
4851 editor.transpose(&Default::default(), window, cx);
4852 assert_eq!(editor.text(cx), "bcda\ne");
4853 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4854
4855 editor.transpose(&Default::default(), window, cx);
4856 assert_eq!(editor.text(cx), "bcade\n");
4857 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4858
4859 editor.transpose(&Default::default(), window, cx);
4860 assert_eq!(editor.text(cx), "bcaed\n");
4861 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4862
4863 editor
4864 });
4865
4866 _ = cx.add_window(|window, cx| {
4867 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4868 editor.set_style(EditorStyle::default(), window, cx);
4869 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4870 editor.transpose(&Default::default(), window, cx);
4871 assert_eq!(editor.text(cx), "🏀🍐✋");
4872 assert_eq!(editor.selections.ranges(cx), [8..8]);
4873
4874 editor.transpose(&Default::default(), window, cx);
4875 assert_eq!(editor.text(cx), "🏀✋🍐");
4876 assert_eq!(editor.selections.ranges(cx), [11..11]);
4877
4878 editor.transpose(&Default::default(), window, cx);
4879 assert_eq!(editor.text(cx), "🏀🍐✋");
4880 assert_eq!(editor.selections.ranges(cx), [11..11]);
4881
4882 editor
4883 });
4884}
4885
4886#[gpui::test]
4887async fn test_rewrap(cx: &mut TestAppContext) {
4888 init_test(cx, |settings| {
4889 settings.languages.extend([
4890 (
4891 "Markdown".into(),
4892 LanguageSettingsContent {
4893 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4894 ..Default::default()
4895 },
4896 ),
4897 (
4898 "Plain Text".into(),
4899 LanguageSettingsContent {
4900 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4901 ..Default::default()
4902 },
4903 ),
4904 ])
4905 });
4906
4907 let mut cx = EditorTestContext::new(cx).await;
4908
4909 let language_with_c_comments = Arc::new(Language::new(
4910 LanguageConfig {
4911 line_comments: vec!["// ".into()],
4912 ..LanguageConfig::default()
4913 },
4914 None,
4915 ));
4916 let language_with_pound_comments = Arc::new(Language::new(
4917 LanguageConfig {
4918 line_comments: vec!["# ".into()],
4919 ..LanguageConfig::default()
4920 },
4921 None,
4922 ));
4923 let markdown_language = Arc::new(Language::new(
4924 LanguageConfig {
4925 name: "Markdown".into(),
4926 ..LanguageConfig::default()
4927 },
4928 None,
4929 ));
4930 let language_with_doc_comments = Arc::new(Language::new(
4931 LanguageConfig {
4932 line_comments: vec!["// ".into(), "/// ".into()],
4933 ..LanguageConfig::default()
4934 },
4935 Some(tree_sitter_rust::LANGUAGE.into()),
4936 ));
4937
4938 let plaintext_language = Arc::new(Language::new(
4939 LanguageConfig {
4940 name: "Plain Text".into(),
4941 ..LanguageConfig::default()
4942 },
4943 None,
4944 ));
4945
4946 assert_rewrap(
4947 indoc! {"
4948 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4949 "},
4950 indoc! {"
4951 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4952 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4953 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4954 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4955 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4956 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4957 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4958 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4959 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4960 // porttitor id. Aliquam id accumsan eros.
4961 "},
4962 language_with_c_comments.clone(),
4963 &mut cx,
4964 );
4965
4966 // Test that rewrapping works inside of a selection
4967 assert_rewrap(
4968 indoc! {"
4969 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.ˇ»
4970 "},
4971 indoc! {"
4972 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4973 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4974 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4975 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4976 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4977 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4978 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4979 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4980 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4981 // porttitor id. Aliquam id accumsan eros.ˇ»
4982 "},
4983 language_with_c_comments.clone(),
4984 &mut cx,
4985 );
4986
4987 // Test that cursors that expand to the same region are collapsed.
4988 assert_rewrap(
4989 indoc! {"
4990 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4991 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4992 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4993 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
4994 "},
4995 indoc! {"
4996 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4997 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4998 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4999 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
5000 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
5001 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
5002 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
5003 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
5004 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
5005 // porttitor id. Aliquam id accumsan eros.
5006 "},
5007 language_with_c_comments.clone(),
5008 &mut cx,
5009 );
5010
5011 // Test that non-contiguous selections are treated separately.
5012 assert_rewrap(
5013 indoc! {"
5014 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
5015 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
5016 //
5017 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5018 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
5019 "},
5020 indoc! {"
5021 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
5022 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
5023 // auctor, eu lacinia sapien scelerisque.
5024 //
5025 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
5026 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
5027 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
5028 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
5029 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
5030 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
5031 // vulputate turpis porttitor id. Aliquam id accumsan eros.
5032 "},
5033 language_with_c_comments.clone(),
5034 &mut cx,
5035 );
5036
5037 // Test that different comment prefixes are supported.
5038 assert_rewrap(
5039 indoc! {"
5040 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id accumsan eros.
5041 "},
5042 indoc! {"
5043 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
5044 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5045 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5046 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5047 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
5048 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
5049 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
5050 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
5051 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
5052 # accumsan eros.
5053 "},
5054 language_with_pound_comments.clone(),
5055 &mut cx,
5056 );
5057
5058 // Test that rewrapping is ignored outside of comments in most languages.
5059 assert_rewrap(
5060 indoc! {"
5061 /// Adds two numbers.
5062 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5063 fn add(a: u32, b: u32) -> u32 {
5064 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
5065 }
5066 "},
5067 indoc! {"
5068 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
5069 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
5070 fn add(a: u32, b: u32) -> u32 {
5071 a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + b + a + bˇ
5072 }
5073 "},
5074 language_with_doc_comments.clone(),
5075 &mut cx,
5076 );
5077
5078 // Test that rewrapping works in Markdown and Plain Text languages.
5079 assert_rewrap(
5080 indoc! {"
5081 # Hello
5082
5083 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5084 "},
5085 indoc! {"
5086 # Hello
5087
5088 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5089 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5090 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5091 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5092 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5093 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5094 Integer sit amet scelerisque nisi.
5095 "},
5096 markdown_language,
5097 &mut cx,
5098 );
5099
5100 assert_rewrap(
5101 indoc! {"
5102 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque nisi.
5103 "},
5104 indoc! {"
5105 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
5106 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
5107 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
5108 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
5109 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
5110 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
5111 Integer sit amet scelerisque nisi.
5112 "},
5113 plaintext_language,
5114 &mut cx,
5115 );
5116
5117 // Test rewrapping unaligned comments in a selection.
5118 assert_rewrap(
5119 indoc! {"
5120 fn foo() {
5121 if true {
5122 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5123 // Praesent semper egestas tellus id dignissim.ˇ»
5124 do_something();
5125 } else {
5126 //
5127 }
5128 }
5129 "},
5130 indoc! {"
5131 fn foo() {
5132 if true {
5133 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5134 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5135 // egestas tellus id dignissim.ˇ»
5136 do_something();
5137 } else {
5138 //
5139 }
5140 }
5141 "},
5142 language_with_doc_comments.clone(),
5143 &mut cx,
5144 );
5145
5146 assert_rewrap(
5147 indoc! {"
5148 fn foo() {
5149 if true {
5150 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
5151 // Praesent semper egestas tellus id dignissim.»
5152 do_something();
5153 } else {
5154 //
5155 }
5156
5157 }
5158 "},
5159 indoc! {"
5160 fn foo() {
5161 if true {
5162 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
5163 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
5164 // egestas tellus id dignissim.»
5165 do_something();
5166 } else {
5167 //
5168 }
5169
5170 }
5171 "},
5172 language_with_doc_comments.clone(),
5173 &mut cx,
5174 );
5175
5176 #[track_caller]
5177 fn assert_rewrap(
5178 unwrapped_text: &str,
5179 wrapped_text: &str,
5180 language: Arc<Language>,
5181 cx: &mut EditorTestContext,
5182 ) {
5183 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
5184 cx.set_state(unwrapped_text);
5185 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
5186 cx.assert_editor_state(wrapped_text);
5187 }
5188}
5189
5190#[gpui::test]
5191async fn test_hard_wrap(cx: &mut TestAppContext) {
5192 init_test(cx, |_| {});
5193 let mut cx = EditorTestContext::new(cx).await;
5194
5195 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
5196 cx.update_editor(|editor, _, cx| {
5197 editor.set_hard_wrap(Some(14), cx);
5198 });
5199
5200 cx.set_state(indoc!(
5201 "
5202 one two three ˇ
5203 "
5204 ));
5205 cx.simulate_input("four");
5206 cx.run_until_parked();
5207
5208 cx.assert_editor_state(indoc!(
5209 "
5210 one two three
5211 fourˇ
5212 "
5213 ));
5214
5215 cx.update_editor(|editor, window, cx| {
5216 editor.newline(&Default::default(), window, cx);
5217 });
5218 cx.run_until_parked();
5219 cx.assert_editor_state(indoc!(
5220 "
5221 one two three
5222 four
5223 ˇ
5224 "
5225 ));
5226
5227 cx.simulate_input("five");
5228 cx.run_until_parked();
5229 cx.assert_editor_state(indoc!(
5230 "
5231 one two three
5232 four
5233 fiveˇ
5234 "
5235 ));
5236
5237 cx.update_editor(|editor, window, cx| {
5238 editor.newline(&Default::default(), window, cx);
5239 });
5240 cx.run_until_parked();
5241 cx.simulate_input("# ");
5242 cx.run_until_parked();
5243 cx.assert_editor_state(indoc!(
5244 "
5245 one two three
5246 four
5247 five
5248 # ˇ
5249 "
5250 ));
5251
5252 cx.update_editor(|editor, window, cx| {
5253 editor.newline(&Default::default(), window, cx);
5254 });
5255 cx.run_until_parked();
5256 cx.assert_editor_state(indoc!(
5257 "
5258 one two three
5259 four
5260 five
5261 #\x20
5262 #ˇ
5263 "
5264 ));
5265
5266 cx.simulate_input(" 6");
5267 cx.run_until_parked();
5268 cx.assert_editor_state(indoc!(
5269 "
5270 one two three
5271 four
5272 five
5273 #
5274 # 6ˇ
5275 "
5276 ));
5277}
5278
5279#[gpui::test]
5280async fn test_clipboard(cx: &mut TestAppContext) {
5281 init_test(cx, |_| {});
5282
5283 let mut cx = EditorTestContext::new(cx).await;
5284
5285 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5286 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5287 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5288
5289 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5290 cx.set_state("two ˇfour ˇsix ˇ");
5291 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5292 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5293
5294 // Paste again but with only two cursors. Since the number of cursors doesn't
5295 // match the number of slices in the clipboard, the entire clipboard text
5296 // is pasted at each cursor.
5297 cx.set_state("ˇtwo one✅ four three six five ˇ");
5298 cx.update_editor(|e, window, cx| {
5299 e.handle_input("( ", window, cx);
5300 e.paste(&Paste, window, cx);
5301 e.handle_input(") ", window, cx);
5302 });
5303 cx.assert_editor_state(
5304 &([
5305 "( one✅ ",
5306 "three ",
5307 "five ) ˇtwo one✅ four three six five ( one✅ ",
5308 "three ",
5309 "five ) ˇ",
5310 ]
5311 .join("\n")),
5312 );
5313
5314 // Cut with three selections, one of which is full-line.
5315 cx.set_state(indoc! {"
5316 1«2ˇ»3
5317 4ˇ567
5318 «8ˇ»9"});
5319 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5320 cx.assert_editor_state(indoc! {"
5321 1ˇ3
5322 ˇ9"});
5323
5324 // Paste with three selections, noticing how the copied selection that was full-line
5325 // gets inserted before the second cursor.
5326 cx.set_state(indoc! {"
5327 1ˇ3
5328 9ˇ
5329 «oˇ»ne"});
5330 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5331 cx.assert_editor_state(indoc! {"
5332 12ˇ3
5333 4567
5334 9ˇ
5335 8ˇne"});
5336
5337 // Copy with a single cursor only, which writes the whole line into the clipboard.
5338 cx.set_state(indoc! {"
5339 The quick brown
5340 fox juˇmps over
5341 the lazy dog"});
5342 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5343 assert_eq!(
5344 cx.read_from_clipboard()
5345 .and_then(|item| item.text().as_deref().map(str::to_string)),
5346 Some("fox jumps over\n".to_string())
5347 );
5348
5349 // Paste with three selections, noticing how the copied full-line selection is inserted
5350 // before the empty selections but replaces the selection that is non-empty.
5351 cx.set_state(indoc! {"
5352 Tˇhe quick brown
5353 «foˇ»x jumps over
5354 tˇhe lazy dog"});
5355 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5356 cx.assert_editor_state(indoc! {"
5357 fox jumps over
5358 Tˇhe quick brown
5359 fox jumps over
5360 ˇx jumps over
5361 fox jumps over
5362 tˇhe lazy dog"});
5363}
5364
5365#[gpui::test]
5366async fn test_copy_trim(cx: &mut TestAppContext) {
5367 init_test(cx, |_| {});
5368
5369 let mut cx = EditorTestContext::new(cx).await;
5370 cx.set_state(
5371 r#" «for selection in selections.iter() {
5372 let mut start = selection.start;
5373 let mut end = selection.end;
5374 let is_entire_line = selection.is_empty();
5375 if is_entire_line {
5376 start = Point::new(start.row, 0);ˇ»
5377 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5378 }
5379 "#,
5380 );
5381 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5382 assert_eq!(
5383 cx.read_from_clipboard()
5384 .and_then(|item| item.text().as_deref().map(str::to_string)),
5385 Some(
5386 "for selection in selections.iter() {
5387 let mut start = selection.start;
5388 let mut end = selection.end;
5389 let is_entire_line = selection.is_empty();
5390 if is_entire_line {
5391 start = Point::new(start.row, 0);"
5392 .to_string()
5393 ),
5394 "Regular copying preserves all indentation selected",
5395 );
5396 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5397 assert_eq!(
5398 cx.read_from_clipboard()
5399 .and_then(|item| item.text().as_deref().map(str::to_string)),
5400 Some(
5401 "for selection in selections.iter() {
5402let mut start = selection.start;
5403let mut end = selection.end;
5404let is_entire_line = selection.is_empty();
5405if is_entire_line {
5406 start = Point::new(start.row, 0);"
5407 .to_string()
5408 ),
5409 "Copying with stripping should strip all leading whitespaces"
5410 );
5411
5412 cx.set_state(
5413 r#" « for selection in selections.iter() {
5414 let mut start = selection.start;
5415 let mut end = selection.end;
5416 let is_entire_line = selection.is_empty();
5417 if is_entire_line {
5418 start = Point::new(start.row, 0);ˇ»
5419 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5420 }
5421 "#,
5422 );
5423 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5424 assert_eq!(
5425 cx.read_from_clipboard()
5426 .and_then(|item| item.text().as_deref().map(str::to_string)),
5427 Some(
5428 " for selection in selections.iter() {
5429 let mut start = selection.start;
5430 let mut end = selection.end;
5431 let is_entire_line = selection.is_empty();
5432 if is_entire_line {
5433 start = Point::new(start.row, 0);"
5434 .to_string()
5435 ),
5436 "Regular copying preserves all indentation selected",
5437 );
5438 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5439 assert_eq!(
5440 cx.read_from_clipboard()
5441 .and_then(|item| item.text().as_deref().map(str::to_string)),
5442 Some(
5443 "for selection in selections.iter() {
5444let mut start = selection.start;
5445let mut end = selection.end;
5446let is_entire_line = selection.is_empty();
5447if is_entire_line {
5448 start = Point::new(start.row, 0);"
5449 .to_string()
5450 ),
5451 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5452 );
5453
5454 cx.set_state(
5455 r#" «ˇ for selection in selections.iter() {
5456 let mut start = selection.start;
5457 let mut end = selection.end;
5458 let is_entire_line = selection.is_empty();
5459 if is_entire_line {
5460 start = Point::new(start.row, 0);»
5461 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5462 }
5463 "#,
5464 );
5465 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5466 assert_eq!(
5467 cx.read_from_clipboard()
5468 .and_then(|item| item.text().as_deref().map(str::to_string)),
5469 Some(
5470 " for selection in selections.iter() {
5471 let mut start = selection.start;
5472 let mut end = selection.end;
5473 let is_entire_line = selection.is_empty();
5474 if is_entire_line {
5475 start = Point::new(start.row, 0);"
5476 .to_string()
5477 ),
5478 "Regular copying for reverse selection works the same",
5479 );
5480 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5481 assert_eq!(
5482 cx.read_from_clipboard()
5483 .and_then(|item| item.text().as_deref().map(str::to_string)),
5484 Some(
5485 "for selection in selections.iter() {
5486let mut start = selection.start;
5487let mut end = selection.end;
5488let is_entire_line = selection.is_empty();
5489if is_entire_line {
5490 start = Point::new(start.row, 0);"
5491 .to_string()
5492 ),
5493 "Copying with stripping for reverse selection works the same"
5494 );
5495
5496 cx.set_state(
5497 r#" for selection «in selections.iter() {
5498 let mut start = selection.start;
5499 let mut end = selection.end;
5500 let is_entire_line = selection.is_empty();
5501 if is_entire_line {
5502 start = Point::new(start.row, 0);ˇ»
5503 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5504 }
5505 "#,
5506 );
5507 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5508 assert_eq!(
5509 cx.read_from_clipboard()
5510 .and_then(|item| item.text().as_deref().map(str::to_string)),
5511 Some(
5512 "in selections.iter() {
5513 let mut start = selection.start;
5514 let mut end = selection.end;
5515 let is_entire_line = selection.is_empty();
5516 if is_entire_line {
5517 start = Point::new(start.row, 0);"
5518 .to_string()
5519 ),
5520 "When selecting past the indent, the copying works as usual",
5521 );
5522 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5523 assert_eq!(
5524 cx.read_from_clipboard()
5525 .and_then(|item| item.text().as_deref().map(str::to_string)),
5526 Some(
5527 "in selections.iter() {
5528 let mut start = selection.start;
5529 let mut end = selection.end;
5530 let is_entire_line = selection.is_empty();
5531 if is_entire_line {
5532 start = Point::new(start.row, 0);"
5533 .to_string()
5534 ),
5535 "When selecting past the indent, nothing is trimmed"
5536 );
5537
5538 cx.set_state(
5539 r#" «for selection in selections.iter() {
5540 let mut start = selection.start;
5541
5542 let mut end = selection.end;
5543 let is_entire_line = selection.is_empty();
5544 if is_entire_line {
5545 start = Point::new(start.row, 0);
5546ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5547 }
5548 "#,
5549 );
5550 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5551 assert_eq!(
5552 cx.read_from_clipboard()
5553 .and_then(|item| item.text().as_deref().map(str::to_string)),
5554 Some(
5555 "for selection in selections.iter() {
5556let mut start = selection.start;
5557
5558let mut end = selection.end;
5559let is_entire_line = selection.is_empty();
5560if is_entire_line {
5561 start = Point::new(start.row, 0);
5562"
5563 .to_string()
5564 ),
5565 "Copying with stripping should ignore empty lines"
5566 );
5567}
5568
5569#[gpui::test]
5570async fn test_paste_multiline(cx: &mut TestAppContext) {
5571 init_test(cx, |_| {});
5572
5573 let mut cx = EditorTestContext::new(cx).await;
5574 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5575
5576 // Cut an indented block, without the leading whitespace.
5577 cx.set_state(indoc! {"
5578 const a: B = (
5579 c(),
5580 «d(
5581 e,
5582 f
5583 )ˇ»
5584 );
5585 "});
5586 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5587 cx.assert_editor_state(indoc! {"
5588 const a: B = (
5589 c(),
5590 ˇ
5591 );
5592 "});
5593
5594 // Paste it at the same position.
5595 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5596 cx.assert_editor_state(indoc! {"
5597 const a: B = (
5598 c(),
5599 d(
5600 e,
5601 f
5602 )ˇ
5603 );
5604 "});
5605
5606 // Paste it at a line with a lower indent level.
5607 cx.set_state(indoc! {"
5608 ˇ
5609 const a: B = (
5610 c(),
5611 );
5612 "});
5613 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5614 cx.assert_editor_state(indoc! {"
5615 d(
5616 e,
5617 f
5618 )ˇ
5619 const a: B = (
5620 c(),
5621 );
5622 "});
5623
5624 // Cut an indented block, with the leading whitespace.
5625 cx.set_state(indoc! {"
5626 const a: B = (
5627 c(),
5628 « d(
5629 e,
5630 f
5631 )
5632 ˇ»);
5633 "});
5634 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5635 cx.assert_editor_state(indoc! {"
5636 const a: B = (
5637 c(),
5638 ˇ);
5639 "});
5640
5641 // Paste it at the same position.
5642 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5643 cx.assert_editor_state(indoc! {"
5644 const a: B = (
5645 c(),
5646 d(
5647 e,
5648 f
5649 )
5650 ˇ);
5651 "});
5652
5653 // Paste it at a line with a higher indent level.
5654 cx.set_state(indoc! {"
5655 const a: B = (
5656 c(),
5657 d(
5658 e,
5659 fˇ
5660 )
5661 );
5662 "});
5663 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5664 cx.assert_editor_state(indoc! {"
5665 const a: B = (
5666 c(),
5667 d(
5668 e,
5669 f d(
5670 e,
5671 f
5672 )
5673 ˇ
5674 )
5675 );
5676 "});
5677
5678 // Copy an indented block, starting mid-line
5679 cx.set_state(indoc! {"
5680 const a: B = (
5681 c(),
5682 somethin«g(
5683 e,
5684 f
5685 )ˇ»
5686 );
5687 "});
5688 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5689
5690 // Paste it on a line with a lower indent level
5691 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5692 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5693 cx.assert_editor_state(indoc! {"
5694 const a: B = (
5695 c(),
5696 something(
5697 e,
5698 f
5699 )
5700 );
5701 g(
5702 e,
5703 f
5704 )ˇ"});
5705}
5706
5707#[gpui::test]
5708async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5709 init_test(cx, |_| {});
5710
5711 cx.write_to_clipboard(ClipboardItem::new_string(
5712 " d(\n e\n );\n".into(),
5713 ));
5714
5715 let mut cx = EditorTestContext::new(cx).await;
5716 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5717
5718 cx.set_state(indoc! {"
5719 fn a() {
5720 b();
5721 if c() {
5722 ˇ
5723 }
5724 }
5725 "});
5726
5727 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5728 cx.assert_editor_state(indoc! {"
5729 fn a() {
5730 b();
5731 if c() {
5732 d(
5733 e
5734 );
5735 ˇ
5736 }
5737 }
5738 "});
5739
5740 cx.set_state(indoc! {"
5741 fn a() {
5742 b();
5743 ˇ
5744 }
5745 "});
5746
5747 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5748 cx.assert_editor_state(indoc! {"
5749 fn a() {
5750 b();
5751 d(
5752 e
5753 );
5754 ˇ
5755 }
5756 "});
5757}
5758
5759#[gpui::test]
5760fn test_select_all(cx: &mut TestAppContext) {
5761 init_test(cx, |_| {});
5762
5763 let editor = cx.add_window(|window, cx| {
5764 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5765 build_editor(buffer, window, cx)
5766 });
5767 _ = editor.update(cx, |editor, window, cx| {
5768 editor.select_all(&SelectAll, window, cx);
5769 assert_eq!(
5770 editor.selections.display_ranges(cx),
5771 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5772 );
5773 });
5774}
5775
5776#[gpui::test]
5777fn test_select_line(cx: &mut TestAppContext) {
5778 init_test(cx, |_| {});
5779
5780 let editor = cx.add_window(|window, cx| {
5781 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5782 build_editor(buffer, window, cx)
5783 });
5784 _ = editor.update(cx, |editor, window, cx| {
5785 editor.change_selections(None, window, cx, |s| {
5786 s.select_display_ranges([
5787 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5788 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5789 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5790 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5791 ])
5792 });
5793 editor.select_line(&SelectLine, window, cx);
5794 assert_eq!(
5795 editor.selections.display_ranges(cx),
5796 vec![
5797 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5798 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5799 ]
5800 );
5801 });
5802
5803 _ = editor.update(cx, |editor, window, cx| {
5804 editor.select_line(&SelectLine, window, cx);
5805 assert_eq!(
5806 editor.selections.display_ranges(cx),
5807 vec![
5808 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5809 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5810 ]
5811 );
5812 });
5813
5814 _ = editor.update(cx, |editor, window, cx| {
5815 editor.select_line(&SelectLine, window, cx);
5816 assert_eq!(
5817 editor.selections.display_ranges(cx),
5818 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5819 );
5820 });
5821}
5822
5823#[gpui::test]
5824async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5825 init_test(cx, |_| {});
5826 let mut cx = EditorTestContext::new(cx).await;
5827
5828 #[track_caller]
5829 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5830 cx.set_state(initial_state);
5831 cx.update_editor(|e, window, cx| {
5832 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5833 });
5834 cx.assert_editor_state(expected_state);
5835 }
5836
5837 // Selection starts and ends at the middle of lines, left-to-right
5838 test(
5839 &mut cx,
5840 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5841 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5842 );
5843 // Same thing, right-to-left
5844 test(
5845 &mut cx,
5846 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5847 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5848 );
5849
5850 // Whole buffer, left-to-right, last line *doesn't* end with newline
5851 test(
5852 &mut cx,
5853 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5854 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5855 );
5856 // Same thing, right-to-left
5857 test(
5858 &mut cx,
5859 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5860 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5861 );
5862
5863 // Whole buffer, left-to-right, last line ends with newline
5864 test(
5865 &mut cx,
5866 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5867 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5868 );
5869 // Same thing, right-to-left
5870 test(
5871 &mut cx,
5872 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5873 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5874 );
5875
5876 // Starts at the end of a line, ends at the start of another
5877 test(
5878 &mut cx,
5879 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5880 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5881 );
5882}
5883
5884#[gpui::test]
5885async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5886 init_test(cx, |_| {});
5887
5888 let editor = cx.add_window(|window, cx| {
5889 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5890 build_editor(buffer, window, cx)
5891 });
5892
5893 // setup
5894 _ = editor.update(cx, |editor, window, cx| {
5895 editor.fold_creases(
5896 vec![
5897 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5898 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5899 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5900 ],
5901 true,
5902 window,
5903 cx,
5904 );
5905 assert_eq!(
5906 editor.display_text(cx),
5907 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5908 );
5909 });
5910
5911 _ = editor.update(cx, |editor, window, cx| {
5912 editor.change_selections(None, window, cx, |s| {
5913 s.select_display_ranges([
5914 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5915 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5916 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5917 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5918 ])
5919 });
5920 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5921 assert_eq!(
5922 editor.display_text(cx),
5923 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5924 );
5925 });
5926 EditorTestContext::for_editor(editor, cx)
5927 .await
5928 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5929
5930 _ = editor.update(cx, |editor, window, cx| {
5931 editor.change_selections(None, window, cx, |s| {
5932 s.select_display_ranges([
5933 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5934 ])
5935 });
5936 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5937 assert_eq!(
5938 editor.display_text(cx),
5939 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5940 );
5941 assert_eq!(
5942 editor.selections.display_ranges(cx),
5943 [
5944 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5945 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5946 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5947 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5948 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5949 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5950 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5951 ]
5952 );
5953 });
5954 EditorTestContext::for_editor(editor, cx)
5955 .await
5956 .assert_editor_state(
5957 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5958 );
5959}
5960
5961#[gpui::test]
5962async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5963 init_test(cx, |_| {});
5964
5965 let mut cx = EditorTestContext::new(cx).await;
5966
5967 cx.set_state(indoc!(
5968 r#"abc
5969 defˇghi
5970
5971 jk
5972 nlmo
5973 "#
5974 ));
5975
5976 cx.update_editor(|editor, window, cx| {
5977 editor.add_selection_above(&Default::default(), window, cx);
5978 });
5979
5980 cx.assert_editor_state(indoc!(
5981 r#"abcˇ
5982 defˇghi
5983
5984 jk
5985 nlmo
5986 "#
5987 ));
5988
5989 cx.update_editor(|editor, window, cx| {
5990 editor.add_selection_above(&Default::default(), window, cx);
5991 });
5992
5993 cx.assert_editor_state(indoc!(
5994 r#"abcˇ
5995 defˇghi
5996
5997 jk
5998 nlmo
5999 "#
6000 ));
6001
6002 cx.update_editor(|editor, window, cx| {
6003 editor.add_selection_below(&Default::default(), window, cx);
6004 });
6005
6006 cx.assert_editor_state(indoc!(
6007 r#"abc
6008 defˇghi
6009
6010 jk
6011 nlmo
6012 "#
6013 ));
6014
6015 cx.update_editor(|editor, window, cx| {
6016 editor.undo_selection(&Default::default(), window, cx);
6017 });
6018
6019 cx.assert_editor_state(indoc!(
6020 r#"abcˇ
6021 defˇghi
6022
6023 jk
6024 nlmo
6025 "#
6026 ));
6027
6028 cx.update_editor(|editor, window, cx| {
6029 editor.redo_selection(&Default::default(), window, cx);
6030 });
6031
6032 cx.assert_editor_state(indoc!(
6033 r#"abc
6034 defˇghi
6035
6036 jk
6037 nlmo
6038 "#
6039 ));
6040
6041 cx.update_editor(|editor, window, cx| {
6042 editor.add_selection_below(&Default::default(), window, cx);
6043 });
6044
6045 cx.assert_editor_state(indoc!(
6046 r#"abc
6047 defˇghi
6048 ˇ
6049 jk
6050 nlmo
6051 "#
6052 ));
6053
6054 cx.update_editor(|editor, window, cx| {
6055 editor.add_selection_below(&Default::default(), window, cx);
6056 });
6057
6058 cx.assert_editor_state(indoc!(
6059 r#"abc
6060 defˇghi
6061 ˇ
6062 jkˇ
6063 nlmo
6064 "#
6065 ));
6066
6067 cx.update_editor(|editor, window, cx| {
6068 editor.add_selection_below(&Default::default(), window, cx);
6069 });
6070
6071 cx.assert_editor_state(indoc!(
6072 r#"abc
6073 defˇghi
6074 ˇ
6075 jkˇ
6076 nlmˇo
6077 "#
6078 ));
6079
6080 cx.update_editor(|editor, window, cx| {
6081 editor.add_selection_below(&Default::default(), window, cx);
6082 });
6083
6084 cx.assert_editor_state(indoc!(
6085 r#"abc
6086 defˇghi
6087 ˇ
6088 jkˇ
6089 nlmˇo
6090 ˇ"#
6091 ));
6092
6093 // change selections
6094 cx.set_state(indoc!(
6095 r#"abc
6096 def«ˇg»hi
6097
6098 jk
6099 nlmo
6100 "#
6101 ));
6102
6103 cx.update_editor(|editor, window, cx| {
6104 editor.add_selection_below(&Default::default(), window, cx);
6105 });
6106
6107 cx.assert_editor_state(indoc!(
6108 r#"abc
6109 def«ˇg»hi
6110
6111 jk
6112 nlm«ˇo»
6113 "#
6114 ));
6115
6116 cx.update_editor(|editor, window, cx| {
6117 editor.add_selection_below(&Default::default(), window, cx);
6118 });
6119
6120 cx.assert_editor_state(indoc!(
6121 r#"abc
6122 def«ˇg»hi
6123
6124 jk
6125 nlm«ˇo»
6126 "#
6127 ));
6128
6129 cx.update_editor(|editor, window, cx| {
6130 editor.add_selection_above(&Default::default(), window, cx);
6131 });
6132
6133 cx.assert_editor_state(indoc!(
6134 r#"abc
6135 def«ˇg»hi
6136
6137 jk
6138 nlmo
6139 "#
6140 ));
6141
6142 cx.update_editor(|editor, window, cx| {
6143 editor.add_selection_above(&Default::default(), window, cx);
6144 });
6145
6146 cx.assert_editor_state(indoc!(
6147 r#"abc
6148 def«ˇg»hi
6149
6150 jk
6151 nlmo
6152 "#
6153 ));
6154
6155 // Change selections again
6156 cx.set_state(indoc!(
6157 r#"a«bc
6158 defgˇ»hi
6159
6160 jk
6161 nlmo
6162 "#
6163 ));
6164
6165 cx.update_editor(|editor, window, cx| {
6166 editor.add_selection_below(&Default::default(), window, cx);
6167 });
6168
6169 cx.assert_editor_state(indoc!(
6170 r#"a«bcˇ»
6171 d«efgˇ»hi
6172
6173 j«kˇ»
6174 nlmo
6175 "#
6176 ));
6177
6178 cx.update_editor(|editor, window, cx| {
6179 editor.add_selection_below(&Default::default(), window, cx);
6180 });
6181 cx.assert_editor_state(indoc!(
6182 r#"a«bcˇ»
6183 d«efgˇ»hi
6184
6185 j«kˇ»
6186 n«lmoˇ»
6187 "#
6188 ));
6189 cx.update_editor(|editor, window, cx| {
6190 editor.add_selection_above(&Default::default(), window, cx);
6191 });
6192
6193 cx.assert_editor_state(indoc!(
6194 r#"a«bcˇ»
6195 d«efgˇ»hi
6196
6197 j«kˇ»
6198 nlmo
6199 "#
6200 ));
6201
6202 // Change selections again
6203 cx.set_state(indoc!(
6204 r#"abc
6205 d«ˇefghi
6206
6207 jk
6208 nlm»o
6209 "#
6210 ));
6211
6212 cx.update_editor(|editor, window, cx| {
6213 editor.add_selection_above(&Default::default(), window, cx);
6214 });
6215
6216 cx.assert_editor_state(indoc!(
6217 r#"a«ˇbc»
6218 d«ˇef»ghi
6219
6220 j«ˇk»
6221 n«ˇlm»o
6222 "#
6223 ));
6224
6225 cx.update_editor(|editor, window, cx| {
6226 editor.add_selection_below(&Default::default(), window, cx);
6227 });
6228
6229 cx.assert_editor_state(indoc!(
6230 r#"abc
6231 d«ˇef»ghi
6232
6233 j«ˇk»
6234 n«ˇlm»o
6235 "#
6236 ));
6237}
6238
6239#[gpui::test]
6240async fn test_select_next(cx: &mut TestAppContext) {
6241 init_test(cx, |_| {});
6242
6243 let mut cx = EditorTestContext::new(cx).await;
6244 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6245
6246 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6247 .unwrap();
6248 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6249
6250 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6251 .unwrap();
6252 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6253
6254 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6255 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6256
6257 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6258 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
6259
6260 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6261 .unwrap();
6262 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6263
6264 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6265 .unwrap();
6266 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6267
6268 // Test selection direction should be preserved
6269 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6270
6271 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6272 .unwrap();
6273 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
6274}
6275
6276#[gpui::test]
6277async fn test_select_all_matches(cx: &mut TestAppContext) {
6278 init_test(cx, |_| {});
6279
6280 let mut cx = EditorTestContext::new(cx).await;
6281
6282 // Test caret-only selections
6283 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6284 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6285 .unwrap();
6286 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6287
6288 // Test left-to-right selections
6289 cx.set_state("abc\n«abcˇ»\nabc");
6290 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6291 .unwrap();
6292 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
6293
6294 // Test right-to-left selections
6295 cx.set_state("abc\n«ˇabc»\nabc");
6296 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6297 .unwrap();
6298 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
6299
6300 // Test selecting whitespace with caret selection
6301 cx.set_state("abc\nˇ abc\nabc");
6302 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6303 .unwrap();
6304 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6305
6306 // Test selecting whitespace with left-to-right selection
6307 cx.set_state("abc\n«ˇ »abc\nabc");
6308 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6309 .unwrap();
6310 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6311
6312 // Test no matches with right-to-left selection
6313 cx.set_state("abc\n« ˇ»abc\nabc");
6314 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6315 .unwrap();
6316 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6317}
6318
6319#[gpui::test]
6320async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6321 init_test(cx, |_| {});
6322
6323 let mut cx = EditorTestContext::new(cx).await;
6324
6325 let large_body_1 = "\nd".repeat(200);
6326 let large_body_2 = "\ne".repeat(200);
6327
6328 cx.set_state(&format!(
6329 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6330 ));
6331 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6332 let scroll_position = editor.scroll_position(cx);
6333 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6334 scroll_position
6335 });
6336
6337 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6338 .unwrap();
6339 cx.assert_editor_state(&format!(
6340 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6341 ));
6342 let scroll_position_after_selection =
6343 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6344 assert_eq!(
6345 initial_scroll_position, scroll_position_after_selection,
6346 "Scroll position should not change after selecting all matches"
6347 );
6348}
6349
6350#[gpui::test]
6351async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6352 init_test(cx, |_| {});
6353
6354 let mut cx = EditorLspTestContext::new_rust(
6355 lsp::ServerCapabilities {
6356 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6357 ..Default::default()
6358 },
6359 cx,
6360 )
6361 .await;
6362
6363 cx.set_state(indoc! {"
6364 line 1
6365 line 2
6366 linˇe 3
6367 line 4
6368 line 5
6369 "});
6370
6371 // Make an edit
6372 cx.update_editor(|editor, window, cx| {
6373 editor.handle_input("X", window, cx);
6374 });
6375
6376 // Move cursor to a different position
6377 cx.update_editor(|editor, window, cx| {
6378 editor.change_selections(None, window, cx, |s| {
6379 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6380 });
6381 });
6382
6383 cx.assert_editor_state(indoc! {"
6384 line 1
6385 line 2
6386 linXe 3
6387 line 4
6388 liˇne 5
6389 "});
6390
6391 cx.lsp
6392 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6393 Ok(Some(vec![lsp::TextEdit::new(
6394 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6395 "PREFIX ".to_string(),
6396 )]))
6397 });
6398
6399 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6400 .unwrap()
6401 .await
6402 .unwrap();
6403
6404 cx.assert_editor_state(indoc! {"
6405 PREFIX line 1
6406 line 2
6407 linXe 3
6408 line 4
6409 liˇne 5
6410 "});
6411
6412 // Undo formatting
6413 cx.update_editor(|editor, window, cx| {
6414 editor.undo(&Default::default(), window, cx);
6415 });
6416
6417 // Verify cursor moved back to position after edit
6418 cx.assert_editor_state(indoc! {"
6419 line 1
6420 line 2
6421 linXˇe 3
6422 line 4
6423 line 5
6424 "});
6425}
6426
6427#[gpui::test]
6428async fn test_undo_inline_completion_scrolls_to_edit_pos(cx: &mut TestAppContext) {
6429 init_test(cx, |_| {});
6430
6431 let mut cx = EditorTestContext::new(cx).await;
6432
6433 let provider = cx.new(|_| FakeInlineCompletionProvider::default());
6434 cx.update_editor(|editor, window, cx| {
6435 editor.set_edit_prediction_provider(Some(provider.clone()), window, cx);
6436 });
6437
6438 cx.set_state(indoc! {"
6439 line 1
6440 line 2
6441 linˇe 3
6442 line 4
6443 line 5
6444 line 6
6445 line 7
6446 line 8
6447 line 9
6448 line 10
6449 "});
6450
6451 let snapshot = cx.buffer_snapshot();
6452 let edit_position = snapshot.anchor_after(Point::new(2, 4));
6453
6454 cx.update(|_, cx| {
6455 provider.update(cx, |provider, _| {
6456 provider.set_inline_completion(Some(inline_completion::InlineCompletion {
6457 id: None,
6458 edits: vec![(edit_position..edit_position, "X".into())],
6459 edit_preview: None,
6460 }))
6461 })
6462 });
6463
6464 cx.update_editor(|editor, window, cx| editor.update_visible_inline_completion(window, cx));
6465 cx.update_editor(|editor, window, cx| {
6466 editor.accept_edit_prediction(&crate::AcceptEditPrediction, window, cx)
6467 });
6468
6469 cx.assert_editor_state(indoc! {"
6470 line 1
6471 line 2
6472 lineXˇ 3
6473 line 4
6474 line 5
6475 line 6
6476 line 7
6477 line 8
6478 line 9
6479 line 10
6480 "});
6481
6482 cx.update_editor(|editor, window, cx| {
6483 editor.change_selections(None, window, cx, |s| {
6484 s.select_ranges([Point::new(9, 2)..Point::new(9, 2)]);
6485 });
6486 });
6487
6488 cx.assert_editor_state(indoc! {"
6489 line 1
6490 line 2
6491 lineX 3
6492 line 4
6493 line 5
6494 line 6
6495 line 7
6496 line 8
6497 line 9
6498 liˇne 10
6499 "});
6500
6501 cx.update_editor(|editor, window, cx| {
6502 editor.undo(&Default::default(), window, cx);
6503 });
6504
6505 cx.assert_editor_state(indoc! {"
6506 line 1
6507 line 2
6508 lineˇ 3
6509 line 4
6510 line 5
6511 line 6
6512 line 7
6513 line 8
6514 line 9
6515 line 10
6516 "});
6517}
6518
6519#[gpui::test]
6520async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6521 init_test(cx, |_| {});
6522
6523 let mut cx = EditorTestContext::new(cx).await;
6524 cx.set_state(
6525 r#"let foo = 2;
6526lˇet foo = 2;
6527let fooˇ = 2;
6528let foo = 2;
6529let foo = ˇ2;"#,
6530 );
6531
6532 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6533 .unwrap();
6534 cx.assert_editor_state(
6535 r#"let foo = 2;
6536«letˇ» foo = 2;
6537let «fooˇ» = 2;
6538let foo = 2;
6539let foo = «2ˇ»;"#,
6540 );
6541
6542 // noop for multiple selections with different contents
6543 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6544 .unwrap();
6545 cx.assert_editor_state(
6546 r#"let foo = 2;
6547«letˇ» foo = 2;
6548let «fooˇ» = 2;
6549let foo = 2;
6550let foo = «2ˇ»;"#,
6551 );
6552
6553 // Test last selection direction should be preserved
6554 cx.set_state(
6555 r#"let foo = 2;
6556let foo = 2;
6557let «fooˇ» = 2;
6558let «ˇfoo» = 2;
6559let foo = 2;"#,
6560 );
6561
6562 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6563 .unwrap();
6564 cx.assert_editor_state(
6565 r#"let foo = 2;
6566let foo = 2;
6567let «fooˇ» = 2;
6568let «ˇfoo» = 2;
6569let «ˇfoo» = 2;"#,
6570 );
6571}
6572
6573#[gpui::test]
6574async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6575 init_test(cx, |_| {});
6576
6577 let mut cx =
6578 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6579
6580 cx.assert_editor_state(indoc! {"
6581 ˇbbb
6582 ccc
6583
6584 bbb
6585 ccc
6586 "});
6587 cx.dispatch_action(SelectPrevious::default());
6588 cx.assert_editor_state(indoc! {"
6589 «bbbˇ»
6590 ccc
6591
6592 bbb
6593 ccc
6594 "});
6595 cx.dispatch_action(SelectPrevious::default());
6596 cx.assert_editor_state(indoc! {"
6597 «bbbˇ»
6598 ccc
6599
6600 «bbbˇ»
6601 ccc
6602 "});
6603}
6604
6605#[gpui::test]
6606async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6607 init_test(cx, |_| {});
6608
6609 let mut cx = EditorTestContext::new(cx).await;
6610 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6611
6612 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6613 .unwrap();
6614 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6615
6616 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6617 .unwrap();
6618 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6619
6620 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6621 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6622
6623 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6624 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6625
6626 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6627 .unwrap();
6628 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6629
6630 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6631 .unwrap();
6632 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6633}
6634
6635#[gpui::test]
6636async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6637 init_test(cx, |_| {});
6638
6639 let mut cx = EditorTestContext::new(cx).await;
6640 cx.set_state("aˇ");
6641
6642 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6643 .unwrap();
6644 cx.assert_editor_state("«aˇ»");
6645 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6646 .unwrap();
6647 cx.assert_editor_state("«aˇ»");
6648}
6649
6650#[gpui::test]
6651async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6652 init_test(cx, |_| {});
6653
6654 let mut cx = EditorTestContext::new(cx).await;
6655 cx.set_state(
6656 r#"let foo = 2;
6657lˇet foo = 2;
6658let fooˇ = 2;
6659let foo = 2;
6660let foo = ˇ2;"#,
6661 );
6662
6663 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6664 .unwrap();
6665 cx.assert_editor_state(
6666 r#"let foo = 2;
6667«letˇ» foo = 2;
6668let «fooˇ» = 2;
6669let foo = 2;
6670let foo = «2ˇ»;"#,
6671 );
6672
6673 // noop for multiple selections with different contents
6674 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6675 .unwrap();
6676 cx.assert_editor_state(
6677 r#"let foo = 2;
6678«letˇ» foo = 2;
6679let «fooˇ» = 2;
6680let foo = 2;
6681let foo = «2ˇ»;"#,
6682 );
6683}
6684
6685#[gpui::test]
6686async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6687 init_test(cx, |_| {});
6688
6689 let mut cx = EditorTestContext::new(cx).await;
6690 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6691
6692 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6693 .unwrap();
6694 // selection direction is preserved
6695 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6696
6697 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6698 .unwrap();
6699 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6700
6701 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6702 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6703
6704 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6705 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6706
6707 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6708 .unwrap();
6709 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6710
6711 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6712 .unwrap();
6713 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6714}
6715
6716#[gpui::test]
6717async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6718 init_test(cx, |_| {});
6719
6720 let language = Arc::new(Language::new(
6721 LanguageConfig::default(),
6722 Some(tree_sitter_rust::LANGUAGE.into()),
6723 ));
6724
6725 let text = r#"
6726 use mod1::mod2::{mod3, mod4};
6727
6728 fn fn_1(param1: bool, param2: &str) {
6729 let var1 = "text";
6730 }
6731 "#
6732 .unindent();
6733
6734 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6735 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6736 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6737
6738 editor
6739 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6740 .await;
6741
6742 editor.update_in(cx, |editor, window, cx| {
6743 editor.change_selections(None, window, cx, |s| {
6744 s.select_display_ranges([
6745 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6746 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6747 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6748 ]);
6749 });
6750 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6751 });
6752 editor.update(cx, |editor, cx| {
6753 assert_text_with_selections(
6754 editor,
6755 indoc! {r#"
6756 use mod1::mod2::{mod3, «mod4ˇ»};
6757
6758 fn fn_1«ˇ(param1: bool, param2: &str)» {
6759 let var1 = "«ˇtext»";
6760 }
6761 "#},
6762 cx,
6763 );
6764 });
6765
6766 editor.update_in(cx, |editor, window, cx| {
6767 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6768 });
6769 editor.update(cx, |editor, cx| {
6770 assert_text_with_selections(
6771 editor,
6772 indoc! {r#"
6773 use mod1::mod2::«{mod3, mod4}ˇ»;
6774
6775 «ˇfn fn_1(param1: bool, param2: &str) {
6776 let var1 = "text";
6777 }»
6778 "#},
6779 cx,
6780 );
6781 });
6782
6783 editor.update_in(cx, |editor, window, cx| {
6784 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6785 });
6786 assert_eq!(
6787 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6788 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6789 );
6790
6791 // Trying to expand the selected syntax node one more time has no effect.
6792 editor.update_in(cx, |editor, window, cx| {
6793 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6794 });
6795 assert_eq!(
6796 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6797 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6798 );
6799
6800 editor.update_in(cx, |editor, window, cx| {
6801 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6802 });
6803 editor.update(cx, |editor, cx| {
6804 assert_text_with_selections(
6805 editor,
6806 indoc! {r#"
6807 use mod1::mod2::«{mod3, mod4}ˇ»;
6808
6809 «ˇfn fn_1(param1: bool, param2: &str) {
6810 let var1 = "text";
6811 }»
6812 "#},
6813 cx,
6814 );
6815 });
6816
6817 editor.update_in(cx, |editor, window, cx| {
6818 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6819 });
6820 editor.update(cx, |editor, cx| {
6821 assert_text_with_selections(
6822 editor,
6823 indoc! {r#"
6824 use mod1::mod2::{mod3, «mod4ˇ»};
6825
6826 fn fn_1«ˇ(param1: bool, param2: &str)» {
6827 let var1 = "«ˇtext»";
6828 }
6829 "#},
6830 cx,
6831 );
6832 });
6833
6834 editor.update_in(cx, |editor, window, cx| {
6835 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6836 });
6837 editor.update(cx, |editor, cx| {
6838 assert_text_with_selections(
6839 editor,
6840 indoc! {r#"
6841 use mod1::mod2::{mod3, mo«ˇ»d4};
6842
6843 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6844 let var1 = "te«ˇ»xt";
6845 }
6846 "#},
6847 cx,
6848 );
6849 });
6850
6851 // Trying to shrink the selected syntax node one more time has no effect.
6852 editor.update_in(cx, |editor, window, cx| {
6853 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6854 });
6855 editor.update_in(cx, |editor, _, cx| {
6856 assert_text_with_selections(
6857 editor,
6858 indoc! {r#"
6859 use mod1::mod2::{mod3, mo«ˇ»d4};
6860
6861 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6862 let var1 = "te«ˇ»xt";
6863 }
6864 "#},
6865 cx,
6866 );
6867 });
6868
6869 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6870 // a fold.
6871 editor.update_in(cx, |editor, window, cx| {
6872 editor.fold_creases(
6873 vec![
6874 Crease::simple(
6875 Point::new(0, 21)..Point::new(0, 24),
6876 FoldPlaceholder::test(),
6877 ),
6878 Crease::simple(
6879 Point::new(3, 20)..Point::new(3, 22),
6880 FoldPlaceholder::test(),
6881 ),
6882 ],
6883 true,
6884 window,
6885 cx,
6886 );
6887 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6888 });
6889 editor.update(cx, |editor, cx| {
6890 assert_text_with_selections(
6891 editor,
6892 indoc! {r#"
6893 use mod1::mod2::«{mod3, mod4}ˇ»;
6894
6895 fn fn_1«ˇ(param1: bool, param2: &str)» {
6896 let var1 = "«ˇtext»";
6897 }
6898 "#},
6899 cx,
6900 );
6901 });
6902}
6903
6904#[gpui::test]
6905async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6906 init_test(cx, |_| {});
6907
6908 let language = Arc::new(Language::new(
6909 LanguageConfig::default(),
6910 Some(tree_sitter_rust::LANGUAGE.into()),
6911 ));
6912
6913 let text = "let a = 2;";
6914
6915 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6916 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6917 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6918
6919 editor
6920 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6921 .await;
6922
6923 // Test case 1: Cursor at end of word
6924 editor.update_in(cx, |editor, window, cx| {
6925 editor.change_selections(None, window, cx, |s| {
6926 s.select_display_ranges([
6927 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6928 ]);
6929 });
6930 });
6931 editor.update(cx, |editor, cx| {
6932 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6933 });
6934 editor.update_in(cx, |editor, window, cx| {
6935 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6936 });
6937 editor.update(cx, |editor, cx| {
6938 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6939 });
6940 editor.update_in(cx, |editor, window, cx| {
6941 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6942 });
6943 editor.update(cx, |editor, cx| {
6944 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6945 });
6946
6947 // Test case 2: Cursor at end of statement
6948 editor.update_in(cx, |editor, window, cx| {
6949 editor.change_selections(None, window, cx, |s| {
6950 s.select_display_ranges([
6951 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6952 ]);
6953 });
6954 });
6955 editor.update(cx, |editor, cx| {
6956 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6957 });
6958 editor.update_in(cx, |editor, window, cx| {
6959 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6960 });
6961 editor.update(cx, |editor, cx| {
6962 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6963 });
6964}
6965
6966#[gpui::test]
6967async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6968 init_test(cx, |_| {});
6969
6970 let language = Arc::new(Language::new(
6971 LanguageConfig::default(),
6972 Some(tree_sitter_rust::LANGUAGE.into()),
6973 ));
6974
6975 let text = r#"
6976 use mod1::mod2::{mod3, mod4};
6977
6978 fn fn_1(param1: bool, param2: &str) {
6979 let var1 = "hello world";
6980 }
6981 "#
6982 .unindent();
6983
6984 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6985 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6986 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6987
6988 editor
6989 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6990 .await;
6991
6992 // Test 1: Cursor on a letter of a string word
6993 editor.update_in(cx, |editor, window, cx| {
6994 editor.change_selections(None, window, cx, |s| {
6995 s.select_display_ranges([
6996 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6997 ]);
6998 });
6999 });
7000 editor.update_in(cx, |editor, window, cx| {
7001 assert_text_with_selections(
7002 editor,
7003 indoc! {r#"
7004 use mod1::mod2::{mod3, mod4};
7005
7006 fn fn_1(param1: bool, param2: &str) {
7007 let var1 = "hˇello world";
7008 }
7009 "#},
7010 cx,
7011 );
7012 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7013 assert_text_with_selections(
7014 editor,
7015 indoc! {r#"
7016 use mod1::mod2::{mod3, mod4};
7017
7018 fn fn_1(param1: bool, param2: &str) {
7019 let var1 = "«ˇhello» world";
7020 }
7021 "#},
7022 cx,
7023 );
7024 });
7025
7026 // Test 2: Partial selection within a word
7027 editor.update_in(cx, |editor, window, cx| {
7028 editor.change_selections(None, window, cx, |s| {
7029 s.select_display_ranges([
7030 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
7031 ]);
7032 });
7033 });
7034 editor.update_in(cx, |editor, window, cx| {
7035 assert_text_with_selections(
7036 editor,
7037 indoc! {r#"
7038 use mod1::mod2::{mod3, mod4};
7039
7040 fn fn_1(param1: bool, param2: &str) {
7041 let var1 = "h«elˇ»lo world";
7042 }
7043 "#},
7044 cx,
7045 );
7046 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7047 assert_text_with_selections(
7048 editor,
7049 indoc! {r#"
7050 use mod1::mod2::{mod3, mod4};
7051
7052 fn fn_1(param1: bool, param2: &str) {
7053 let var1 = "«ˇhello» world";
7054 }
7055 "#},
7056 cx,
7057 );
7058 });
7059
7060 // Test 3: Complete word already selected
7061 editor.update_in(cx, |editor, window, cx| {
7062 editor.change_selections(None, window, cx, |s| {
7063 s.select_display_ranges([
7064 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
7065 ]);
7066 });
7067 });
7068 editor.update_in(cx, |editor, window, cx| {
7069 assert_text_with_selections(
7070 editor,
7071 indoc! {r#"
7072 use mod1::mod2::{mod3, mod4};
7073
7074 fn fn_1(param1: bool, param2: &str) {
7075 let var1 = "«helloˇ» world";
7076 }
7077 "#},
7078 cx,
7079 );
7080 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7081 assert_text_with_selections(
7082 editor,
7083 indoc! {r#"
7084 use mod1::mod2::{mod3, mod4};
7085
7086 fn fn_1(param1: bool, param2: &str) {
7087 let var1 = "«hello worldˇ»";
7088 }
7089 "#},
7090 cx,
7091 );
7092 });
7093
7094 // Test 4: Selection spanning across words
7095 editor.update_in(cx, |editor, window, cx| {
7096 editor.change_selections(None, window, cx, |s| {
7097 s.select_display_ranges([
7098 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
7099 ]);
7100 });
7101 });
7102 editor.update_in(cx, |editor, window, cx| {
7103 assert_text_with_selections(
7104 editor,
7105 indoc! {r#"
7106 use mod1::mod2::{mod3, mod4};
7107
7108 fn fn_1(param1: bool, param2: &str) {
7109 let var1 = "hel«lo woˇ»rld";
7110 }
7111 "#},
7112 cx,
7113 );
7114 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7115 assert_text_with_selections(
7116 editor,
7117 indoc! {r#"
7118 use mod1::mod2::{mod3, mod4};
7119
7120 fn fn_1(param1: bool, param2: &str) {
7121 let var1 = "«ˇhello world»";
7122 }
7123 "#},
7124 cx,
7125 );
7126 });
7127
7128 // Test 5: Expansion beyond string
7129 editor.update_in(cx, |editor, window, cx| {
7130 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7131 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
7132 assert_text_with_selections(
7133 editor,
7134 indoc! {r#"
7135 use mod1::mod2::{mod3, mod4};
7136
7137 fn fn_1(param1: bool, param2: &str) {
7138 «ˇlet var1 = "hello world";»
7139 }
7140 "#},
7141 cx,
7142 );
7143 });
7144}
7145
7146#[gpui::test]
7147async fn test_fold_function_bodies(cx: &mut TestAppContext) {
7148 init_test(cx, |_| {});
7149
7150 let base_text = r#"
7151 impl A {
7152 // this is an uncommitted comment
7153
7154 fn b() {
7155 c();
7156 }
7157
7158 // this is another uncommitted comment
7159
7160 fn d() {
7161 // e
7162 // f
7163 }
7164 }
7165
7166 fn g() {
7167 // h
7168 }
7169 "#
7170 .unindent();
7171
7172 let text = r#"
7173 ˇimpl A {
7174
7175 fn b() {
7176 c();
7177 }
7178
7179 fn d() {
7180 // e
7181 // f
7182 }
7183 }
7184
7185 fn g() {
7186 // h
7187 }
7188 "#
7189 .unindent();
7190
7191 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7192 cx.set_state(&text);
7193 cx.set_head_text(&base_text);
7194 cx.update_editor(|editor, window, cx| {
7195 editor.expand_all_diff_hunks(&Default::default(), window, cx);
7196 });
7197
7198 cx.assert_state_with_diff(
7199 "
7200 ˇimpl A {
7201 - // this is an uncommitted comment
7202
7203 fn b() {
7204 c();
7205 }
7206
7207 - // this is another uncommitted comment
7208 -
7209 fn d() {
7210 // e
7211 // f
7212 }
7213 }
7214
7215 fn g() {
7216 // h
7217 }
7218 "
7219 .unindent(),
7220 );
7221
7222 let expected_display_text = "
7223 impl A {
7224 // this is an uncommitted comment
7225
7226 fn b() {
7227 ⋯
7228 }
7229
7230 // this is another uncommitted comment
7231
7232 fn d() {
7233 ⋯
7234 }
7235 }
7236
7237 fn g() {
7238 ⋯
7239 }
7240 "
7241 .unindent();
7242
7243 cx.update_editor(|editor, window, cx| {
7244 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
7245 assert_eq!(editor.display_text(cx), expected_display_text);
7246 });
7247}
7248
7249#[gpui::test]
7250async fn test_autoindent(cx: &mut TestAppContext) {
7251 init_test(cx, |_| {});
7252
7253 let language = Arc::new(
7254 Language::new(
7255 LanguageConfig {
7256 brackets: BracketPairConfig {
7257 pairs: vec![
7258 BracketPair {
7259 start: "{".to_string(),
7260 end: "}".to_string(),
7261 close: false,
7262 surround: false,
7263 newline: true,
7264 },
7265 BracketPair {
7266 start: "(".to_string(),
7267 end: ")".to_string(),
7268 close: false,
7269 surround: false,
7270 newline: true,
7271 },
7272 ],
7273 ..Default::default()
7274 },
7275 ..Default::default()
7276 },
7277 Some(tree_sitter_rust::LANGUAGE.into()),
7278 )
7279 .with_indents_query(
7280 r#"
7281 (_ "(" ")" @end) @indent
7282 (_ "{" "}" @end) @indent
7283 "#,
7284 )
7285 .unwrap(),
7286 );
7287
7288 let text = "fn a() {}";
7289
7290 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7291 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7292 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7293 editor
7294 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7295 .await;
7296
7297 editor.update_in(cx, |editor, window, cx| {
7298 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
7299 editor.newline(&Newline, window, cx);
7300 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
7301 assert_eq!(
7302 editor.selections.ranges(cx),
7303 &[
7304 Point::new(1, 4)..Point::new(1, 4),
7305 Point::new(3, 4)..Point::new(3, 4),
7306 Point::new(5, 0)..Point::new(5, 0)
7307 ]
7308 );
7309 });
7310}
7311
7312#[gpui::test]
7313async fn test_autoindent_selections(cx: &mut TestAppContext) {
7314 init_test(cx, |_| {});
7315
7316 {
7317 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
7318 cx.set_state(indoc! {"
7319 impl A {
7320
7321 fn b() {}
7322
7323 «fn c() {
7324
7325 }ˇ»
7326 }
7327 "});
7328
7329 cx.update_editor(|editor, window, cx| {
7330 editor.autoindent(&Default::default(), window, cx);
7331 });
7332
7333 cx.assert_editor_state(indoc! {"
7334 impl A {
7335
7336 fn b() {}
7337
7338 «fn c() {
7339
7340 }ˇ»
7341 }
7342 "});
7343 }
7344
7345 {
7346 let mut cx = EditorTestContext::new_multibuffer(
7347 cx,
7348 [indoc! { "
7349 impl A {
7350 «
7351 // a
7352 fn b(){}
7353 »
7354 «
7355 }
7356 fn c(){}
7357 »
7358 "}],
7359 );
7360
7361 let buffer = cx.update_editor(|editor, _, cx| {
7362 let buffer = editor.buffer().update(cx, |buffer, _| {
7363 buffer.all_buffers().iter().next().unwrap().clone()
7364 });
7365 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
7366 buffer
7367 });
7368
7369 cx.run_until_parked();
7370 cx.update_editor(|editor, window, cx| {
7371 editor.select_all(&Default::default(), window, cx);
7372 editor.autoindent(&Default::default(), window, cx)
7373 });
7374 cx.run_until_parked();
7375
7376 cx.update(|_, cx| {
7377 assert_eq!(
7378 buffer.read(cx).text(),
7379 indoc! { "
7380 impl A {
7381
7382 // a
7383 fn b(){}
7384
7385
7386 }
7387 fn c(){}
7388
7389 " }
7390 )
7391 });
7392 }
7393}
7394
7395#[gpui::test]
7396async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7397 init_test(cx, |_| {});
7398
7399 let mut cx = EditorTestContext::new(cx).await;
7400
7401 let language = Arc::new(Language::new(
7402 LanguageConfig {
7403 brackets: BracketPairConfig {
7404 pairs: vec![
7405 BracketPair {
7406 start: "{".to_string(),
7407 end: "}".to_string(),
7408 close: true,
7409 surround: true,
7410 newline: true,
7411 },
7412 BracketPair {
7413 start: "(".to_string(),
7414 end: ")".to_string(),
7415 close: true,
7416 surround: true,
7417 newline: true,
7418 },
7419 BracketPair {
7420 start: "/*".to_string(),
7421 end: " */".to_string(),
7422 close: true,
7423 surround: true,
7424 newline: true,
7425 },
7426 BracketPair {
7427 start: "[".to_string(),
7428 end: "]".to_string(),
7429 close: false,
7430 surround: false,
7431 newline: true,
7432 },
7433 BracketPair {
7434 start: "\"".to_string(),
7435 end: "\"".to_string(),
7436 close: true,
7437 surround: true,
7438 newline: false,
7439 },
7440 BracketPair {
7441 start: "<".to_string(),
7442 end: ">".to_string(),
7443 close: false,
7444 surround: true,
7445 newline: true,
7446 },
7447 ],
7448 ..Default::default()
7449 },
7450 autoclose_before: "})]".to_string(),
7451 ..Default::default()
7452 },
7453 Some(tree_sitter_rust::LANGUAGE.into()),
7454 ));
7455
7456 cx.language_registry().add(language.clone());
7457 cx.update_buffer(|buffer, cx| {
7458 buffer.set_language(Some(language), cx);
7459 });
7460
7461 cx.set_state(
7462 &r#"
7463 🏀ˇ
7464 εˇ
7465 ❤️ˇ
7466 "#
7467 .unindent(),
7468 );
7469
7470 // autoclose multiple nested brackets at multiple cursors
7471 cx.update_editor(|editor, window, cx| {
7472 editor.handle_input("{", window, cx);
7473 editor.handle_input("{", window, cx);
7474 editor.handle_input("{", window, cx);
7475 });
7476 cx.assert_editor_state(
7477 &"
7478 🏀{{{ˇ}}}
7479 ε{{{ˇ}}}
7480 ❤️{{{ˇ}}}
7481 "
7482 .unindent(),
7483 );
7484
7485 // insert a different closing bracket
7486 cx.update_editor(|editor, window, cx| {
7487 editor.handle_input(")", window, cx);
7488 });
7489 cx.assert_editor_state(
7490 &"
7491 🏀{{{)ˇ}}}
7492 ε{{{)ˇ}}}
7493 ❤️{{{)ˇ}}}
7494 "
7495 .unindent(),
7496 );
7497
7498 // skip over the auto-closed brackets when typing a closing bracket
7499 cx.update_editor(|editor, window, cx| {
7500 editor.move_right(&MoveRight, window, cx);
7501 editor.handle_input("}", window, cx);
7502 editor.handle_input("}", window, cx);
7503 editor.handle_input("}", window, cx);
7504 });
7505 cx.assert_editor_state(
7506 &"
7507 🏀{{{)}}}}ˇ
7508 ε{{{)}}}}ˇ
7509 ❤️{{{)}}}}ˇ
7510 "
7511 .unindent(),
7512 );
7513
7514 // autoclose multi-character pairs
7515 cx.set_state(
7516 &"
7517 ˇ
7518 ˇ
7519 "
7520 .unindent(),
7521 );
7522 cx.update_editor(|editor, window, cx| {
7523 editor.handle_input("/", window, cx);
7524 editor.handle_input("*", window, cx);
7525 });
7526 cx.assert_editor_state(
7527 &"
7528 /*ˇ */
7529 /*ˇ */
7530 "
7531 .unindent(),
7532 );
7533
7534 // one cursor autocloses a multi-character pair, one cursor
7535 // does not autoclose.
7536 cx.set_state(
7537 &"
7538 /ˇ
7539 ˇ
7540 "
7541 .unindent(),
7542 );
7543 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7544 cx.assert_editor_state(
7545 &"
7546 /*ˇ */
7547 *ˇ
7548 "
7549 .unindent(),
7550 );
7551
7552 // Don't autoclose if the next character isn't whitespace and isn't
7553 // listed in the language's "autoclose_before" section.
7554 cx.set_state("ˇa b");
7555 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7556 cx.assert_editor_state("{ˇa b");
7557
7558 // Don't autoclose if `close` is false for the bracket pair
7559 cx.set_state("ˇ");
7560 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7561 cx.assert_editor_state("[ˇ");
7562
7563 // Surround with brackets if text is selected
7564 cx.set_state("«aˇ» b");
7565 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7566 cx.assert_editor_state("{«aˇ»} b");
7567
7568 // Autoclose when not immediately after a word character
7569 cx.set_state("a ˇ");
7570 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7571 cx.assert_editor_state("a \"ˇ\"");
7572
7573 // Autoclose pair where the start and end characters are the same
7574 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7575 cx.assert_editor_state("a \"\"ˇ");
7576
7577 // Don't autoclose when immediately after a word character
7578 cx.set_state("aˇ");
7579 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7580 cx.assert_editor_state("a\"ˇ");
7581
7582 // Do autoclose when after a non-word character
7583 cx.set_state("{ˇ");
7584 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7585 cx.assert_editor_state("{\"ˇ\"");
7586
7587 // Non identical pairs autoclose regardless of preceding character
7588 cx.set_state("aˇ");
7589 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7590 cx.assert_editor_state("a{ˇ}");
7591
7592 // Don't autoclose pair if autoclose is disabled
7593 cx.set_state("ˇ");
7594 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7595 cx.assert_editor_state("<ˇ");
7596
7597 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7598 cx.set_state("«aˇ» b");
7599 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7600 cx.assert_editor_state("<«aˇ»> b");
7601}
7602
7603#[gpui::test]
7604async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7605 init_test(cx, |settings| {
7606 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7607 });
7608
7609 let mut cx = EditorTestContext::new(cx).await;
7610
7611 let language = Arc::new(Language::new(
7612 LanguageConfig {
7613 brackets: BracketPairConfig {
7614 pairs: vec![
7615 BracketPair {
7616 start: "{".to_string(),
7617 end: "}".to_string(),
7618 close: true,
7619 surround: true,
7620 newline: true,
7621 },
7622 BracketPair {
7623 start: "(".to_string(),
7624 end: ")".to_string(),
7625 close: true,
7626 surround: true,
7627 newline: true,
7628 },
7629 BracketPair {
7630 start: "[".to_string(),
7631 end: "]".to_string(),
7632 close: false,
7633 surround: false,
7634 newline: true,
7635 },
7636 ],
7637 ..Default::default()
7638 },
7639 autoclose_before: "})]".to_string(),
7640 ..Default::default()
7641 },
7642 Some(tree_sitter_rust::LANGUAGE.into()),
7643 ));
7644
7645 cx.language_registry().add(language.clone());
7646 cx.update_buffer(|buffer, cx| {
7647 buffer.set_language(Some(language), cx);
7648 });
7649
7650 cx.set_state(
7651 &"
7652 ˇ
7653 ˇ
7654 ˇ
7655 "
7656 .unindent(),
7657 );
7658
7659 // ensure only matching closing brackets are skipped over
7660 cx.update_editor(|editor, window, cx| {
7661 editor.handle_input("}", window, cx);
7662 editor.move_left(&MoveLeft, window, cx);
7663 editor.handle_input(")", window, cx);
7664 editor.move_left(&MoveLeft, window, cx);
7665 });
7666 cx.assert_editor_state(
7667 &"
7668 ˇ)}
7669 ˇ)}
7670 ˇ)}
7671 "
7672 .unindent(),
7673 );
7674
7675 // skip-over closing brackets at multiple cursors
7676 cx.update_editor(|editor, window, cx| {
7677 editor.handle_input(")", window, cx);
7678 editor.handle_input("}", window, cx);
7679 });
7680 cx.assert_editor_state(
7681 &"
7682 )}ˇ
7683 )}ˇ
7684 )}ˇ
7685 "
7686 .unindent(),
7687 );
7688
7689 // ignore non-close brackets
7690 cx.update_editor(|editor, window, cx| {
7691 editor.handle_input("]", window, cx);
7692 editor.move_left(&MoveLeft, window, cx);
7693 editor.handle_input("]", window, cx);
7694 });
7695 cx.assert_editor_state(
7696 &"
7697 )}]ˇ]
7698 )}]ˇ]
7699 )}]ˇ]
7700 "
7701 .unindent(),
7702 );
7703}
7704
7705#[gpui::test]
7706async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7707 init_test(cx, |_| {});
7708
7709 let mut cx = EditorTestContext::new(cx).await;
7710
7711 let html_language = Arc::new(
7712 Language::new(
7713 LanguageConfig {
7714 name: "HTML".into(),
7715 brackets: BracketPairConfig {
7716 pairs: vec![
7717 BracketPair {
7718 start: "<".into(),
7719 end: ">".into(),
7720 close: true,
7721 ..Default::default()
7722 },
7723 BracketPair {
7724 start: "{".into(),
7725 end: "}".into(),
7726 close: true,
7727 ..Default::default()
7728 },
7729 BracketPair {
7730 start: "(".into(),
7731 end: ")".into(),
7732 close: true,
7733 ..Default::default()
7734 },
7735 ],
7736 ..Default::default()
7737 },
7738 autoclose_before: "})]>".into(),
7739 ..Default::default()
7740 },
7741 Some(tree_sitter_html::LANGUAGE.into()),
7742 )
7743 .with_injection_query(
7744 r#"
7745 (script_element
7746 (raw_text) @injection.content
7747 (#set! injection.language "javascript"))
7748 "#,
7749 )
7750 .unwrap(),
7751 );
7752
7753 let javascript_language = Arc::new(Language::new(
7754 LanguageConfig {
7755 name: "JavaScript".into(),
7756 brackets: BracketPairConfig {
7757 pairs: vec![
7758 BracketPair {
7759 start: "/*".into(),
7760 end: " */".into(),
7761 close: true,
7762 ..Default::default()
7763 },
7764 BracketPair {
7765 start: "{".into(),
7766 end: "}".into(),
7767 close: true,
7768 ..Default::default()
7769 },
7770 BracketPair {
7771 start: "(".into(),
7772 end: ")".into(),
7773 close: true,
7774 ..Default::default()
7775 },
7776 ],
7777 ..Default::default()
7778 },
7779 autoclose_before: "})]>".into(),
7780 ..Default::default()
7781 },
7782 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7783 ));
7784
7785 cx.language_registry().add(html_language.clone());
7786 cx.language_registry().add(javascript_language.clone());
7787
7788 cx.update_buffer(|buffer, cx| {
7789 buffer.set_language(Some(html_language), cx);
7790 });
7791
7792 cx.set_state(
7793 &r#"
7794 <body>ˇ
7795 <script>
7796 var x = 1;ˇ
7797 </script>
7798 </body>ˇ
7799 "#
7800 .unindent(),
7801 );
7802
7803 // Precondition: different languages are active at different locations.
7804 cx.update_editor(|editor, window, cx| {
7805 let snapshot = editor.snapshot(window, cx);
7806 let cursors = editor.selections.ranges::<usize>(cx);
7807 let languages = cursors
7808 .iter()
7809 .map(|c| snapshot.language_at(c.start).unwrap().name())
7810 .collect::<Vec<_>>();
7811 assert_eq!(
7812 languages,
7813 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7814 );
7815 });
7816
7817 // Angle brackets autoclose in HTML, but not JavaScript.
7818 cx.update_editor(|editor, window, cx| {
7819 editor.handle_input("<", window, cx);
7820 editor.handle_input("a", window, cx);
7821 });
7822 cx.assert_editor_state(
7823 &r#"
7824 <body><aˇ>
7825 <script>
7826 var x = 1;<aˇ
7827 </script>
7828 </body><aˇ>
7829 "#
7830 .unindent(),
7831 );
7832
7833 // Curly braces and parens autoclose in both HTML and JavaScript.
7834 cx.update_editor(|editor, window, cx| {
7835 editor.handle_input(" b=", window, cx);
7836 editor.handle_input("{", window, cx);
7837 editor.handle_input("c", window, cx);
7838 editor.handle_input("(", window, cx);
7839 });
7840 cx.assert_editor_state(
7841 &r#"
7842 <body><a b={c(ˇ)}>
7843 <script>
7844 var x = 1;<a b={c(ˇ)}
7845 </script>
7846 </body><a b={c(ˇ)}>
7847 "#
7848 .unindent(),
7849 );
7850
7851 // Brackets that were already autoclosed are skipped.
7852 cx.update_editor(|editor, window, cx| {
7853 editor.handle_input(")", window, cx);
7854 editor.handle_input("d", window, cx);
7855 editor.handle_input("}", window, cx);
7856 });
7857 cx.assert_editor_state(
7858 &r#"
7859 <body><a b={c()d}ˇ>
7860 <script>
7861 var x = 1;<a b={c()d}ˇ
7862 </script>
7863 </body><a b={c()d}ˇ>
7864 "#
7865 .unindent(),
7866 );
7867 cx.update_editor(|editor, window, cx| {
7868 editor.handle_input(">", window, cx);
7869 });
7870 cx.assert_editor_state(
7871 &r#"
7872 <body><a b={c()d}>ˇ
7873 <script>
7874 var x = 1;<a b={c()d}>ˇ
7875 </script>
7876 </body><a b={c()d}>ˇ
7877 "#
7878 .unindent(),
7879 );
7880
7881 // Reset
7882 cx.set_state(
7883 &r#"
7884 <body>ˇ
7885 <script>
7886 var x = 1;ˇ
7887 </script>
7888 </body>ˇ
7889 "#
7890 .unindent(),
7891 );
7892
7893 cx.update_editor(|editor, window, cx| {
7894 editor.handle_input("<", window, cx);
7895 });
7896 cx.assert_editor_state(
7897 &r#"
7898 <body><ˇ>
7899 <script>
7900 var x = 1;<ˇ
7901 </script>
7902 </body><ˇ>
7903 "#
7904 .unindent(),
7905 );
7906
7907 // When backspacing, the closing angle brackets are removed.
7908 cx.update_editor(|editor, window, cx| {
7909 editor.backspace(&Backspace, window, cx);
7910 });
7911 cx.assert_editor_state(
7912 &r#"
7913 <body>ˇ
7914 <script>
7915 var x = 1;ˇ
7916 </script>
7917 </body>ˇ
7918 "#
7919 .unindent(),
7920 );
7921
7922 // Block comments autoclose in JavaScript, but not HTML.
7923 cx.update_editor(|editor, window, cx| {
7924 editor.handle_input("/", window, cx);
7925 editor.handle_input("*", window, cx);
7926 });
7927 cx.assert_editor_state(
7928 &r#"
7929 <body>/*ˇ
7930 <script>
7931 var x = 1;/*ˇ */
7932 </script>
7933 </body>/*ˇ
7934 "#
7935 .unindent(),
7936 );
7937}
7938
7939#[gpui::test]
7940async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7941 init_test(cx, |_| {});
7942
7943 let mut cx = EditorTestContext::new(cx).await;
7944
7945 let rust_language = Arc::new(
7946 Language::new(
7947 LanguageConfig {
7948 name: "Rust".into(),
7949 brackets: serde_json::from_value(json!([
7950 { "start": "{", "end": "}", "close": true, "newline": true },
7951 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7952 ]))
7953 .unwrap(),
7954 autoclose_before: "})]>".into(),
7955 ..Default::default()
7956 },
7957 Some(tree_sitter_rust::LANGUAGE.into()),
7958 )
7959 .with_override_query("(string_literal) @string")
7960 .unwrap(),
7961 );
7962
7963 cx.language_registry().add(rust_language.clone());
7964 cx.update_buffer(|buffer, cx| {
7965 buffer.set_language(Some(rust_language), cx);
7966 });
7967
7968 cx.set_state(
7969 &r#"
7970 let x = ˇ
7971 "#
7972 .unindent(),
7973 );
7974
7975 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7976 cx.update_editor(|editor, window, cx| {
7977 editor.handle_input("\"", window, cx);
7978 });
7979 cx.assert_editor_state(
7980 &r#"
7981 let x = "ˇ"
7982 "#
7983 .unindent(),
7984 );
7985
7986 // Inserting another quotation mark. The cursor moves across the existing
7987 // automatically-inserted quotation mark.
7988 cx.update_editor(|editor, window, cx| {
7989 editor.handle_input("\"", window, cx);
7990 });
7991 cx.assert_editor_state(
7992 &r#"
7993 let x = ""ˇ
7994 "#
7995 .unindent(),
7996 );
7997
7998 // Reset
7999 cx.set_state(
8000 &r#"
8001 let x = ˇ
8002 "#
8003 .unindent(),
8004 );
8005
8006 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
8007 cx.update_editor(|editor, window, cx| {
8008 editor.handle_input("\"", window, cx);
8009 editor.handle_input(" ", window, cx);
8010 editor.move_left(&Default::default(), window, cx);
8011 editor.handle_input("\\", window, cx);
8012 editor.handle_input("\"", window, cx);
8013 });
8014 cx.assert_editor_state(
8015 &r#"
8016 let x = "\"ˇ "
8017 "#
8018 .unindent(),
8019 );
8020
8021 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
8022 // mark. Nothing is inserted.
8023 cx.update_editor(|editor, window, cx| {
8024 editor.move_right(&Default::default(), window, cx);
8025 editor.handle_input("\"", window, cx);
8026 });
8027 cx.assert_editor_state(
8028 &r#"
8029 let x = "\" "ˇ
8030 "#
8031 .unindent(),
8032 );
8033}
8034
8035#[gpui::test]
8036async fn test_surround_with_pair(cx: &mut TestAppContext) {
8037 init_test(cx, |_| {});
8038
8039 let language = Arc::new(Language::new(
8040 LanguageConfig {
8041 brackets: BracketPairConfig {
8042 pairs: vec![
8043 BracketPair {
8044 start: "{".to_string(),
8045 end: "}".to_string(),
8046 close: true,
8047 surround: true,
8048 newline: true,
8049 },
8050 BracketPair {
8051 start: "/* ".to_string(),
8052 end: "*/".to_string(),
8053 close: true,
8054 surround: true,
8055 ..Default::default()
8056 },
8057 ],
8058 ..Default::default()
8059 },
8060 ..Default::default()
8061 },
8062 Some(tree_sitter_rust::LANGUAGE.into()),
8063 ));
8064
8065 let text = r#"
8066 a
8067 b
8068 c
8069 "#
8070 .unindent();
8071
8072 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8073 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8074 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8075 editor
8076 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8077 .await;
8078
8079 editor.update_in(cx, |editor, window, cx| {
8080 editor.change_selections(None, window, cx, |s| {
8081 s.select_display_ranges([
8082 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8083 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8084 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
8085 ])
8086 });
8087
8088 editor.handle_input("{", window, cx);
8089 editor.handle_input("{", window, cx);
8090 editor.handle_input("{", window, cx);
8091 assert_eq!(
8092 editor.text(cx),
8093 "
8094 {{{a}}}
8095 {{{b}}}
8096 {{{c}}}
8097 "
8098 .unindent()
8099 );
8100 assert_eq!(
8101 editor.selections.display_ranges(cx),
8102 [
8103 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
8104 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
8105 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
8106 ]
8107 );
8108
8109 editor.undo(&Undo, window, cx);
8110 editor.undo(&Undo, window, cx);
8111 editor.undo(&Undo, window, cx);
8112 assert_eq!(
8113 editor.text(cx),
8114 "
8115 a
8116 b
8117 c
8118 "
8119 .unindent()
8120 );
8121 assert_eq!(
8122 editor.selections.display_ranges(cx),
8123 [
8124 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8125 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8126 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8127 ]
8128 );
8129
8130 // Ensure inserting the first character of a multi-byte bracket pair
8131 // doesn't surround the selections with the bracket.
8132 editor.handle_input("/", window, cx);
8133 assert_eq!(
8134 editor.text(cx),
8135 "
8136 /
8137 /
8138 /
8139 "
8140 .unindent()
8141 );
8142 assert_eq!(
8143 editor.selections.display_ranges(cx),
8144 [
8145 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8146 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8147 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8148 ]
8149 );
8150
8151 editor.undo(&Undo, window, cx);
8152 assert_eq!(
8153 editor.text(cx),
8154 "
8155 a
8156 b
8157 c
8158 "
8159 .unindent()
8160 );
8161 assert_eq!(
8162 editor.selections.display_ranges(cx),
8163 [
8164 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
8165 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
8166 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
8167 ]
8168 );
8169
8170 // Ensure inserting the last character of a multi-byte bracket pair
8171 // doesn't surround the selections with the bracket.
8172 editor.handle_input("*", window, cx);
8173 assert_eq!(
8174 editor.text(cx),
8175 "
8176 *
8177 *
8178 *
8179 "
8180 .unindent()
8181 );
8182 assert_eq!(
8183 editor.selections.display_ranges(cx),
8184 [
8185 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
8186 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
8187 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
8188 ]
8189 );
8190 });
8191}
8192
8193#[gpui::test]
8194async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
8195 init_test(cx, |_| {});
8196
8197 let language = Arc::new(Language::new(
8198 LanguageConfig {
8199 brackets: BracketPairConfig {
8200 pairs: vec![BracketPair {
8201 start: "{".to_string(),
8202 end: "}".to_string(),
8203 close: true,
8204 surround: true,
8205 newline: true,
8206 }],
8207 ..Default::default()
8208 },
8209 autoclose_before: "}".to_string(),
8210 ..Default::default()
8211 },
8212 Some(tree_sitter_rust::LANGUAGE.into()),
8213 ));
8214
8215 let text = r#"
8216 a
8217 b
8218 c
8219 "#
8220 .unindent();
8221
8222 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
8223 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8224 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8225 editor
8226 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8227 .await;
8228
8229 editor.update_in(cx, |editor, window, cx| {
8230 editor.change_selections(None, window, cx, |s| {
8231 s.select_ranges([
8232 Point::new(0, 1)..Point::new(0, 1),
8233 Point::new(1, 1)..Point::new(1, 1),
8234 Point::new(2, 1)..Point::new(2, 1),
8235 ])
8236 });
8237
8238 editor.handle_input("{", window, cx);
8239 editor.handle_input("{", window, cx);
8240 editor.handle_input("_", window, cx);
8241 assert_eq!(
8242 editor.text(cx),
8243 "
8244 a{{_}}
8245 b{{_}}
8246 c{{_}}
8247 "
8248 .unindent()
8249 );
8250 assert_eq!(
8251 editor.selections.ranges::<Point>(cx),
8252 [
8253 Point::new(0, 4)..Point::new(0, 4),
8254 Point::new(1, 4)..Point::new(1, 4),
8255 Point::new(2, 4)..Point::new(2, 4)
8256 ]
8257 );
8258
8259 editor.backspace(&Default::default(), window, cx);
8260 editor.backspace(&Default::default(), window, cx);
8261 assert_eq!(
8262 editor.text(cx),
8263 "
8264 a{}
8265 b{}
8266 c{}
8267 "
8268 .unindent()
8269 );
8270 assert_eq!(
8271 editor.selections.ranges::<Point>(cx),
8272 [
8273 Point::new(0, 2)..Point::new(0, 2),
8274 Point::new(1, 2)..Point::new(1, 2),
8275 Point::new(2, 2)..Point::new(2, 2)
8276 ]
8277 );
8278
8279 editor.delete_to_previous_word_start(&Default::default(), window, cx);
8280 assert_eq!(
8281 editor.text(cx),
8282 "
8283 a
8284 b
8285 c
8286 "
8287 .unindent()
8288 );
8289 assert_eq!(
8290 editor.selections.ranges::<Point>(cx),
8291 [
8292 Point::new(0, 1)..Point::new(0, 1),
8293 Point::new(1, 1)..Point::new(1, 1),
8294 Point::new(2, 1)..Point::new(2, 1)
8295 ]
8296 );
8297 });
8298}
8299
8300#[gpui::test]
8301async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
8302 init_test(cx, |settings| {
8303 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
8304 });
8305
8306 let mut cx = EditorTestContext::new(cx).await;
8307
8308 let language = Arc::new(Language::new(
8309 LanguageConfig {
8310 brackets: BracketPairConfig {
8311 pairs: vec![
8312 BracketPair {
8313 start: "{".to_string(),
8314 end: "}".to_string(),
8315 close: true,
8316 surround: true,
8317 newline: true,
8318 },
8319 BracketPair {
8320 start: "(".to_string(),
8321 end: ")".to_string(),
8322 close: true,
8323 surround: true,
8324 newline: true,
8325 },
8326 BracketPair {
8327 start: "[".to_string(),
8328 end: "]".to_string(),
8329 close: false,
8330 surround: true,
8331 newline: true,
8332 },
8333 ],
8334 ..Default::default()
8335 },
8336 autoclose_before: "})]".to_string(),
8337 ..Default::default()
8338 },
8339 Some(tree_sitter_rust::LANGUAGE.into()),
8340 ));
8341
8342 cx.language_registry().add(language.clone());
8343 cx.update_buffer(|buffer, cx| {
8344 buffer.set_language(Some(language), cx);
8345 });
8346
8347 cx.set_state(
8348 &"
8349 {(ˇ)}
8350 [[ˇ]]
8351 {(ˇ)}
8352 "
8353 .unindent(),
8354 );
8355
8356 cx.update_editor(|editor, window, cx| {
8357 editor.backspace(&Default::default(), window, cx);
8358 editor.backspace(&Default::default(), window, cx);
8359 });
8360
8361 cx.assert_editor_state(
8362 &"
8363 ˇ
8364 ˇ]]
8365 ˇ
8366 "
8367 .unindent(),
8368 );
8369
8370 cx.update_editor(|editor, window, cx| {
8371 editor.handle_input("{", window, cx);
8372 editor.handle_input("{", window, cx);
8373 editor.move_right(&MoveRight, window, cx);
8374 editor.move_right(&MoveRight, window, cx);
8375 editor.move_left(&MoveLeft, window, cx);
8376 editor.move_left(&MoveLeft, window, cx);
8377 editor.backspace(&Default::default(), window, cx);
8378 });
8379
8380 cx.assert_editor_state(
8381 &"
8382 {ˇ}
8383 {ˇ}]]
8384 {ˇ}
8385 "
8386 .unindent(),
8387 );
8388
8389 cx.update_editor(|editor, window, cx| {
8390 editor.backspace(&Default::default(), window, cx);
8391 });
8392
8393 cx.assert_editor_state(
8394 &"
8395 ˇ
8396 ˇ]]
8397 ˇ
8398 "
8399 .unindent(),
8400 );
8401}
8402
8403#[gpui::test]
8404async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8405 init_test(cx, |_| {});
8406
8407 let language = Arc::new(Language::new(
8408 LanguageConfig::default(),
8409 Some(tree_sitter_rust::LANGUAGE.into()),
8410 ));
8411
8412 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8413 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8414 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8415 editor
8416 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8417 .await;
8418
8419 editor.update_in(cx, |editor, window, cx| {
8420 editor.set_auto_replace_emoji_shortcode(true);
8421
8422 editor.handle_input("Hello ", window, cx);
8423 editor.handle_input(":wave", window, cx);
8424 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8425
8426 editor.handle_input(":", window, cx);
8427 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8428
8429 editor.handle_input(" :smile", window, cx);
8430 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8431
8432 editor.handle_input(":", window, cx);
8433 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8434
8435 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8436 editor.handle_input(":wave", window, cx);
8437 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8438
8439 editor.handle_input(":", window, cx);
8440 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8441
8442 editor.handle_input(":1", window, cx);
8443 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8444
8445 editor.handle_input(":", window, cx);
8446 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8447
8448 // Ensure shortcode does not get replaced when it is part of a word
8449 editor.handle_input(" Test:wave", window, cx);
8450 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8451
8452 editor.handle_input(":", window, cx);
8453 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8454
8455 editor.set_auto_replace_emoji_shortcode(false);
8456
8457 // Ensure shortcode does not get replaced when auto replace is off
8458 editor.handle_input(" :wave", window, cx);
8459 assert_eq!(
8460 editor.text(cx),
8461 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8462 );
8463
8464 editor.handle_input(":", window, cx);
8465 assert_eq!(
8466 editor.text(cx),
8467 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8468 );
8469 });
8470}
8471
8472#[gpui::test]
8473async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8474 init_test(cx, |_| {});
8475
8476 let (text, insertion_ranges) = marked_text_ranges(
8477 indoc! {"
8478 ˇ
8479 "},
8480 false,
8481 );
8482
8483 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8484 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8485
8486 _ = editor.update_in(cx, |editor, window, cx| {
8487 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8488
8489 editor
8490 .insert_snippet(&insertion_ranges, snippet, window, cx)
8491 .unwrap();
8492
8493 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8494 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8495 assert_eq!(editor.text(cx), expected_text);
8496 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8497 }
8498
8499 assert(
8500 editor,
8501 cx,
8502 indoc! {"
8503 type «» =•
8504 "},
8505 );
8506
8507 assert!(editor.context_menu_visible(), "There should be a matches");
8508 });
8509}
8510
8511#[gpui::test]
8512async fn test_snippets(cx: &mut TestAppContext) {
8513 init_test(cx, |_| {});
8514
8515 let (text, insertion_ranges) = marked_text_ranges(
8516 indoc! {"
8517 a.ˇ b
8518 a.ˇ b
8519 a.ˇ b
8520 "},
8521 false,
8522 );
8523
8524 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8525 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8526
8527 editor.update_in(cx, |editor, window, cx| {
8528 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8529
8530 editor
8531 .insert_snippet(&insertion_ranges, snippet, window, cx)
8532 .unwrap();
8533
8534 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8535 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8536 assert_eq!(editor.text(cx), expected_text);
8537 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8538 }
8539
8540 assert(
8541 editor,
8542 cx,
8543 indoc! {"
8544 a.f(«one», two, «three») b
8545 a.f(«one», two, «three») b
8546 a.f(«one», two, «three») b
8547 "},
8548 );
8549
8550 // Can't move earlier than the first tab stop
8551 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8552 assert(
8553 editor,
8554 cx,
8555 indoc! {"
8556 a.f(«one», two, «three») b
8557 a.f(«one», two, «three») b
8558 a.f(«one», two, «three») b
8559 "},
8560 );
8561
8562 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8563 assert(
8564 editor,
8565 cx,
8566 indoc! {"
8567 a.f(one, «two», three) b
8568 a.f(one, «two», three) b
8569 a.f(one, «two», three) b
8570 "},
8571 );
8572
8573 editor.move_to_prev_snippet_tabstop(window, cx);
8574 assert(
8575 editor,
8576 cx,
8577 indoc! {"
8578 a.f(«one», two, «three») b
8579 a.f(«one», two, «three») b
8580 a.f(«one», two, «three») b
8581 "},
8582 );
8583
8584 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8585 assert(
8586 editor,
8587 cx,
8588 indoc! {"
8589 a.f(one, «two», three) b
8590 a.f(one, «two», three) b
8591 a.f(one, «two», three) b
8592 "},
8593 );
8594 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8595 assert(
8596 editor,
8597 cx,
8598 indoc! {"
8599 a.f(one, two, three)ˇ b
8600 a.f(one, two, three)ˇ b
8601 a.f(one, two, three)ˇ b
8602 "},
8603 );
8604
8605 // As soon as the last tab stop is reached, snippet state is gone
8606 editor.move_to_prev_snippet_tabstop(window, cx);
8607 assert(
8608 editor,
8609 cx,
8610 indoc! {"
8611 a.f(one, two, three)ˇ b
8612 a.f(one, two, three)ˇ b
8613 a.f(one, two, three)ˇ b
8614 "},
8615 );
8616 });
8617}
8618
8619#[gpui::test]
8620async fn test_document_format_during_save(cx: &mut TestAppContext) {
8621 init_test(cx, |_| {});
8622
8623 let fs = FakeFs::new(cx.executor());
8624 fs.insert_file(path!("/file.rs"), Default::default()).await;
8625
8626 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8627
8628 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8629 language_registry.add(rust_lang());
8630 let mut fake_servers = language_registry.register_fake_lsp(
8631 "Rust",
8632 FakeLspAdapter {
8633 capabilities: lsp::ServerCapabilities {
8634 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8635 ..Default::default()
8636 },
8637 ..Default::default()
8638 },
8639 );
8640
8641 let buffer = project
8642 .update(cx, |project, cx| {
8643 project.open_local_buffer(path!("/file.rs"), cx)
8644 })
8645 .await
8646 .unwrap();
8647
8648 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8649 let (editor, cx) = cx.add_window_view(|window, cx| {
8650 build_editor_with_project(project.clone(), buffer, window, cx)
8651 });
8652 editor.update_in(cx, |editor, window, cx| {
8653 editor.set_text("one\ntwo\nthree\n", window, cx)
8654 });
8655 assert!(cx.read(|cx| editor.is_dirty(cx)));
8656
8657 cx.executor().start_waiting();
8658 let fake_server = fake_servers.next().await.unwrap();
8659
8660 {
8661 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8662 move |params, _| async move {
8663 assert_eq!(
8664 params.text_document.uri,
8665 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8666 );
8667 assert_eq!(params.options.tab_size, 4);
8668 Ok(Some(vec![lsp::TextEdit::new(
8669 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8670 ", ".to_string(),
8671 )]))
8672 },
8673 );
8674 let save = editor
8675 .update_in(cx, |editor, window, cx| {
8676 editor.save(true, project.clone(), window, cx)
8677 })
8678 .unwrap();
8679 cx.executor().start_waiting();
8680 save.await;
8681
8682 assert_eq!(
8683 editor.update(cx, |editor, cx| editor.text(cx)),
8684 "one, two\nthree\n"
8685 );
8686 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8687 }
8688
8689 {
8690 editor.update_in(cx, |editor, window, cx| {
8691 editor.set_text("one\ntwo\nthree\n", window, cx)
8692 });
8693 assert!(cx.read(|cx| editor.is_dirty(cx)));
8694
8695 // Ensure we can still save even if formatting hangs.
8696 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8697 move |params, _| async move {
8698 assert_eq!(
8699 params.text_document.uri,
8700 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8701 );
8702 futures::future::pending::<()>().await;
8703 unreachable!()
8704 },
8705 );
8706 let save = editor
8707 .update_in(cx, |editor, window, cx| {
8708 editor.save(true, project.clone(), window, cx)
8709 })
8710 .unwrap();
8711 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8712 cx.executor().start_waiting();
8713 save.await;
8714 assert_eq!(
8715 editor.update(cx, |editor, cx| editor.text(cx)),
8716 "one\ntwo\nthree\n"
8717 );
8718 }
8719
8720 // For non-dirty buffer, no formatting request should be sent
8721 {
8722 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8723
8724 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8725 panic!("Should not be invoked on non-dirty buffer");
8726 });
8727 let save = editor
8728 .update_in(cx, |editor, window, cx| {
8729 editor.save(true, project.clone(), window, cx)
8730 })
8731 .unwrap();
8732 cx.executor().start_waiting();
8733 save.await;
8734 }
8735
8736 // Set rust language override and assert overridden tabsize is sent to language server
8737 update_test_language_settings(cx, |settings| {
8738 settings.languages.insert(
8739 "Rust".into(),
8740 LanguageSettingsContent {
8741 tab_size: NonZeroU32::new(8),
8742 ..Default::default()
8743 },
8744 );
8745 });
8746
8747 {
8748 editor.update_in(cx, |editor, window, cx| {
8749 editor.set_text("somehting_new\n", window, cx)
8750 });
8751 assert!(cx.read(|cx| editor.is_dirty(cx)));
8752 let _formatting_request_signal = fake_server
8753 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8754 assert_eq!(
8755 params.text_document.uri,
8756 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8757 );
8758 assert_eq!(params.options.tab_size, 8);
8759 Ok(Some(vec![]))
8760 });
8761 let save = editor
8762 .update_in(cx, |editor, window, cx| {
8763 editor.save(true, project.clone(), window, cx)
8764 })
8765 .unwrap();
8766 cx.executor().start_waiting();
8767 save.await;
8768 }
8769}
8770
8771#[gpui::test]
8772async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8773 init_test(cx, |_| {});
8774
8775 let cols = 4;
8776 let rows = 10;
8777 let sample_text_1 = sample_text(rows, cols, 'a');
8778 assert_eq!(
8779 sample_text_1,
8780 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8781 );
8782 let sample_text_2 = sample_text(rows, cols, 'l');
8783 assert_eq!(
8784 sample_text_2,
8785 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8786 );
8787 let sample_text_3 = sample_text(rows, cols, 'v');
8788 assert_eq!(
8789 sample_text_3,
8790 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8791 );
8792
8793 let fs = FakeFs::new(cx.executor());
8794 fs.insert_tree(
8795 path!("/a"),
8796 json!({
8797 "main.rs": sample_text_1,
8798 "other.rs": sample_text_2,
8799 "lib.rs": sample_text_3,
8800 }),
8801 )
8802 .await;
8803
8804 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8805 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8806 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8807
8808 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8809 language_registry.add(rust_lang());
8810 let mut fake_servers = language_registry.register_fake_lsp(
8811 "Rust",
8812 FakeLspAdapter {
8813 capabilities: lsp::ServerCapabilities {
8814 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8815 ..Default::default()
8816 },
8817 ..Default::default()
8818 },
8819 );
8820
8821 let worktree = project.update(cx, |project, cx| {
8822 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8823 assert_eq!(worktrees.len(), 1);
8824 worktrees.pop().unwrap()
8825 });
8826 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8827
8828 let buffer_1 = project
8829 .update(cx, |project, cx| {
8830 project.open_buffer((worktree_id, "main.rs"), cx)
8831 })
8832 .await
8833 .unwrap();
8834 let buffer_2 = project
8835 .update(cx, |project, cx| {
8836 project.open_buffer((worktree_id, "other.rs"), cx)
8837 })
8838 .await
8839 .unwrap();
8840 let buffer_3 = project
8841 .update(cx, |project, cx| {
8842 project.open_buffer((worktree_id, "lib.rs"), cx)
8843 })
8844 .await
8845 .unwrap();
8846
8847 let multi_buffer = cx.new(|cx| {
8848 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8849 multi_buffer.push_excerpts(
8850 buffer_1.clone(),
8851 [
8852 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8853 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8854 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8855 ],
8856 cx,
8857 );
8858 multi_buffer.push_excerpts(
8859 buffer_2.clone(),
8860 [
8861 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8862 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8863 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8864 ],
8865 cx,
8866 );
8867 multi_buffer.push_excerpts(
8868 buffer_3.clone(),
8869 [
8870 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8871 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8872 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8873 ],
8874 cx,
8875 );
8876 multi_buffer
8877 });
8878 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8879 Editor::new(
8880 EditorMode::full(),
8881 multi_buffer,
8882 Some(project.clone()),
8883 window,
8884 cx,
8885 )
8886 });
8887
8888 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8889 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8890 s.select_ranges(Some(1..2))
8891 });
8892 editor.insert("|one|two|three|", window, cx);
8893 });
8894 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8895 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8896 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8897 s.select_ranges(Some(60..70))
8898 });
8899 editor.insert("|four|five|six|", window, cx);
8900 });
8901 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8902
8903 // First two buffers should be edited, but not the third one.
8904 assert_eq!(
8905 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8906 "a|one|two|three|aa\nbbbb\ncccc\n\nffff\ngggg\n\njjjj\nllll\nmmmm\nnnnn|four|five|six|\nr\n\nuuuu\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}",
8907 );
8908 buffer_1.update(cx, |buffer, _| {
8909 assert!(buffer.is_dirty());
8910 assert_eq!(
8911 buffer.text(),
8912 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8913 )
8914 });
8915 buffer_2.update(cx, |buffer, _| {
8916 assert!(buffer.is_dirty());
8917 assert_eq!(
8918 buffer.text(),
8919 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8920 )
8921 });
8922 buffer_3.update(cx, |buffer, _| {
8923 assert!(!buffer.is_dirty());
8924 assert_eq!(buffer.text(), sample_text_3,)
8925 });
8926 cx.executor().run_until_parked();
8927
8928 cx.executor().start_waiting();
8929 let save = multi_buffer_editor
8930 .update_in(cx, |editor, window, cx| {
8931 editor.save(true, project.clone(), window, cx)
8932 })
8933 .unwrap();
8934
8935 let fake_server = fake_servers.next().await.unwrap();
8936 fake_server
8937 .server
8938 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8939 Ok(Some(vec![lsp::TextEdit::new(
8940 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8941 format!("[{} formatted]", params.text_document.uri),
8942 )]))
8943 })
8944 .detach();
8945 save.await;
8946
8947 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8948 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8949 assert_eq!(
8950 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8951 uri!(
8952 "a|o[file:///a/main.rs formatted]bbbb\ncccc\n\nffff\ngggg\n\njjjj\n\nlll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|\nr\n\nuuuu\n\nvvvv\nwwww\nxxxx\n\n{{{{\n||||\n\n\u{7f}\u{7f}\u{7f}\u{7f}"
8953 ),
8954 );
8955 buffer_1.update(cx, |buffer, _| {
8956 assert!(!buffer.is_dirty());
8957 assert_eq!(
8958 buffer.text(),
8959 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8960 )
8961 });
8962 buffer_2.update(cx, |buffer, _| {
8963 assert!(!buffer.is_dirty());
8964 assert_eq!(
8965 buffer.text(),
8966 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8967 )
8968 });
8969 buffer_3.update(cx, |buffer, _| {
8970 assert!(!buffer.is_dirty());
8971 assert_eq!(buffer.text(), sample_text_3,)
8972 });
8973}
8974
8975#[gpui::test]
8976async fn test_range_format_during_save(cx: &mut TestAppContext) {
8977 init_test(cx, |_| {});
8978
8979 let fs = FakeFs::new(cx.executor());
8980 fs.insert_file(path!("/file.rs"), Default::default()).await;
8981
8982 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8983
8984 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8985 language_registry.add(rust_lang());
8986 let mut fake_servers = language_registry.register_fake_lsp(
8987 "Rust",
8988 FakeLspAdapter {
8989 capabilities: lsp::ServerCapabilities {
8990 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8991 ..Default::default()
8992 },
8993 ..Default::default()
8994 },
8995 );
8996
8997 let buffer = project
8998 .update(cx, |project, cx| {
8999 project.open_local_buffer(path!("/file.rs"), cx)
9000 })
9001 .await
9002 .unwrap();
9003
9004 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9005 let (editor, cx) = cx.add_window_view(|window, cx| {
9006 build_editor_with_project(project.clone(), buffer, window, cx)
9007 });
9008 editor.update_in(cx, |editor, window, cx| {
9009 editor.set_text("one\ntwo\nthree\n", window, cx)
9010 });
9011 assert!(cx.read(|cx| editor.is_dirty(cx)));
9012
9013 cx.executor().start_waiting();
9014 let fake_server = fake_servers.next().await.unwrap();
9015
9016 let save = editor
9017 .update_in(cx, |editor, window, cx| {
9018 editor.save(true, project.clone(), window, cx)
9019 })
9020 .unwrap();
9021 fake_server
9022 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9023 assert_eq!(
9024 params.text_document.uri,
9025 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9026 );
9027 assert_eq!(params.options.tab_size, 4);
9028 Ok(Some(vec![lsp::TextEdit::new(
9029 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9030 ", ".to_string(),
9031 )]))
9032 })
9033 .next()
9034 .await;
9035 cx.executor().start_waiting();
9036 save.await;
9037 assert_eq!(
9038 editor.update(cx, |editor, cx| editor.text(cx)),
9039 "one, two\nthree\n"
9040 );
9041 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9042
9043 editor.update_in(cx, |editor, window, cx| {
9044 editor.set_text("one\ntwo\nthree\n", window, cx)
9045 });
9046 assert!(cx.read(|cx| editor.is_dirty(cx)));
9047
9048 // Ensure we can still save even if formatting hangs.
9049 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
9050 move |params, _| async move {
9051 assert_eq!(
9052 params.text_document.uri,
9053 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9054 );
9055 futures::future::pending::<()>().await;
9056 unreachable!()
9057 },
9058 );
9059 let save = editor
9060 .update_in(cx, |editor, window, cx| {
9061 editor.save(true, project.clone(), window, cx)
9062 })
9063 .unwrap();
9064 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9065 cx.executor().start_waiting();
9066 save.await;
9067 assert_eq!(
9068 editor.update(cx, |editor, cx| editor.text(cx)),
9069 "one\ntwo\nthree\n"
9070 );
9071 assert!(!cx.read(|cx| editor.is_dirty(cx)));
9072
9073 // For non-dirty buffer, no formatting request should be sent
9074 let save = editor
9075 .update_in(cx, |editor, window, cx| {
9076 editor.save(true, project.clone(), window, cx)
9077 })
9078 .unwrap();
9079 let _pending_format_request = fake_server
9080 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
9081 panic!("Should not be invoked on non-dirty buffer");
9082 })
9083 .next();
9084 cx.executor().start_waiting();
9085 save.await;
9086
9087 // Set Rust language override and assert overridden tabsize is sent to language server
9088 update_test_language_settings(cx, |settings| {
9089 settings.languages.insert(
9090 "Rust".into(),
9091 LanguageSettingsContent {
9092 tab_size: NonZeroU32::new(8),
9093 ..Default::default()
9094 },
9095 );
9096 });
9097
9098 editor.update_in(cx, |editor, window, cx| {
9099 editor.set_text("somehting_new\n", window, cx)
9100 });
9101 assert!(cx.read(|cx| editor.is_dirty(cx)));
9102 let save = editor
9103 .update_in(cx, |editor, window, cx| {
9104 editor.save(true, project.clone(), window, cx)
9105 })
9106 .unwrap();
9107 fake_server
9108 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
9109 assert_eq!(
9110 params.text_document.uri,
9111 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9112 );
9113 assert_eq!(params.options.tab_size, 8);
9114 Ok(Some(Vec::new()))
9115 })
9116 .next()
9117 .await;
9118 save.await;
9119}
9120
9121#[gpui::test]
9122async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
9123 init_test(cx, |settings| {
9124 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9125 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9126 ))
9127 });
9128
9129 let fs = FakeFs::new(cx.executor());
9130 fs.insert_file(path!("/file.rs"), Default::default()).await;
9131
9132 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9133
9134 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9135 language_registry.add(Arc::new(Language::new(
9136 LanguageConfig {
9137 name: "Rust".into(),
9138 matcher: LanguageMatcher {
9139 path_suffixes: vec!["rs".to_string()],
9140 ..Default::default()
9141 },
9142 ..LanguageConfig::default()
9143 },
9144 Some(tree_sitter_rust::LANGUAGE.into()),
9145 )));
9146 update_test_language_settings(cx, |settings| {
9147 // Enable Prettier formatting for the same buffer, and ensure
9148 // LSP is called instead of Prettier.
9149 settings.defaults.prettier = Some(PrettierSettings {
9150 allowed: true,
9151 ..PrettierSettings::default()
9152 });
9153 });
9154 let mut fake_servers = language_registry.register_fake_lsp(
9155 "Rust",
9156 FakeLspAdapter {
9157 capabilities: lsp::ServerCapabilities {
9158 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9159 ..Default::default()
9160 },
9161 ..Default::default()
9162 },
9163 );
9164
9165 let buffer = project
9166 .update(cx, |project, cx| {
9167 project.open_local_buffer(path!("/file.rs"), cx)
9168 })
9169 .await
9170 .unwrap();
9171
9172 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9173 let (editor, cx) = cx.add_window_view(|window, cx| {
9174 build_editor_with_project(project.clone(), buffer, window, cx)
9175 });
9176 editor.update_in(cx, |editor, window, cx| {
9177 editor.set_text("one\ntwo\nthree\n", window, cx)
9178 });
9179
9180 cx.executor().start_waiting();
9181 let fake_server = fake_servers.next().await.unwrap();
9182
9183 let format = editor
9184 .update_in(cx, |editor, window, cx| {
9185 editor.perform_format(
9186 project.clone(),
9187 FormatTrigger::Manual,
9188 FormatTarget::Buffers,
9189 window,
9190 cx,
9191 )
9192 })
9193 .unwrap();
9194 fake_server
9195 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
9196 assert_eq!(
9197 params.text_document.uri,
9198 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9199 );
9200 assert_eq!(params.options.tab_size, 4);
9201 Ok(Some(vec![lsp::TextEdit::new(
9202 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
9203 ", ".to_string(),
9204 )]))
9205 })
9206 .next()
9207 .await;
9208 cx.executor().start_waiting();
9209 format.await;
9210 assert_eq!(
9211 editor.update(cx, |editor, cx| editor.text(cx)),
9212 "one, two\nthree\n"
9213 );
9214
9215 editor.update_in(cx, |editor, window, cx| {
9216 editor.set_text("one\ntwo\nthree\n", window, cx)
9217 });
9218 // Ensure we don't lock if formatting hangs.
9219 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9220 move |params, _| async move {
9221 assert_eq!(
9222 params.text_document.uri,
9223 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
9224 );
9225 futures::future::pending::<()>().await;
9226 unreachable!()
9227 },
9228 );
9229 let format = editor
9230 .update_in(cx, |editor, window, cx| {
9231 editor.perform_format(
9232 project,
9233 FormatTrigger::Manual,
9234 FormatTarget::Buffers,
9235 window,
9236 cx,
9237 )
9238 })
9239 .unwrap();
9240 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
9241 cx.executor().start_waiting();
9242 format.await;
9243 assert_eq!(
9244 editor.update(cx, |editor, cx| editor.text(cx)),
9245 "one\ntwo\nthree\n"
9246 );
9247}
9248
9249#[gpui::test]
9250async fn test_multiple_formatters(cx: &mut TestAppContext) {
9251 init_test(cx, |settings| {
9252 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
9253 settings.defaults.formatter =
9254 Some(language_settings::SelectedFormatter::List(FormatterList(
9255 vec![
9256 Formatter::LanguageServer { name: None },
9257 Formatter::CodeActions(
9258 [
9259 ("code-action-1".into(), true),
9260 ("code-action-2".into(), true),
9261 ]
9262 .into_iter()
9263 .collect(),
9264 ),
9265 ]
9266 .into(),
9267 )))
9268 });
9269
9270 let fs = FakeFs::new(cx.executor());
9271 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
9272 .await;
9273
9274 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9275 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9276 language_registry.add(rust_lang());
9277
9278 let mut fake_servers = language_registry.register_fake_lsp(
9279 "Rust",
9280 FakeLspAdapter {
9281 capabilities: lsp::ServerCapabilities {
9282 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9283 execute_command_provider: Some(lsp::ExecuteCommandOptions {
9284 commands: vec!["the-command-for-code-action-1".into()],
9285 ..Default::default()
9286 }),
9287 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9288 ..Default::default()
9289 },
9290 ..Default::default()
9291 },
9292 );
9293
9294 let buffer = project
9295 .update(cx, |project, cx| {
9296 project.open_local_buffer(path!("/file.rs"), cx)
9297 })
9298 .await
9299 .unwrap();
9300
9301 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9302 let (editor, cx) = cx.add_window_view(|window, cx| {
9303 build_editor_with_project(project.clone(), buffer, window, cx)
9304 });
9305
9306 cx.executor().start_waiting();
9307
9308 let fake_server = fake_servers.next().await.unwrap();
9309 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
9310 move |_params, _| async move {
9311 Ok(Some(vec![lsp::TextEdit::new(
9312 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9313 "applied-formatting\n".to_string(),
9314 )]))
9315 },
9316 );
9317 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9318 move |params, _| async move {
9319 assert_eq!(
9320 params.context.only,
9321 Some(vec!["code-action-1".into(), "code-action-2".into()])
9322 );
9323 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
9324 Ok(Some(vec![
9325 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9326 kind: Some("code-action-1".into()),
9327 edit: Some(lsp::WorkspaceEdit::new(
9328 [(
9329 uri.clone(),
9330 vec![lsp::TextEdit::new(
9331 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9332 "applied-code-action-1-edit\n".to_string(),
9333 )],
9334 )]
9335 .into_iter()
9336 .collect(),
9337 )),
9338 command: Some(lsp::Command {
9339 command: "the-command-for-code-action-1".into(),
9340 ..Default::default()
9341 }),
9342 ..Default::default()
9343 }),
9344 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
9345 kind: Some("code-action-2".into()),
9346 edit: Some(lsp::WorkspaceEdit::new(
9347 [(
9348 uri.clone(),
9349 vec![lsp::TextEdit::new(
9350 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
9351 "applied-code-action-2-edit\n".to_string(),
9352 )],
9353 )]
9354 .into_iter()
9355 .collect(),
9356 )),
9357 ..Default::default()
9358 }),
9359 ]))
9360 },
9361 );
9362
9363 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
9364 move |params, _| async move { Ok(params) }
9365 });
9366
9367 let command_lock = Arc::new(futures::lock::Mutex::new(()));
9368 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
9369 let fake = fake_server.clone();
9370 let lock = command_lock.clone();
9371 move |params, _| {
9372 assert_eq!(params.command, "the-command-for-code-action-1");
9373 let fake = fake.clone();
9374 let lock = lock.clone();
9375 async move {
9376 lock.lock().await;
9377 fake.server
9378 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
9379 label: None,
9380 edit: lsp::WorkspaceEdit {
9381 changes: Some(
9382 [(
9383 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
9384 vec![lsp::TextEdit {
9385 range: lsp::Range::new(
9386 lsp::Position::new(0, 0),
9387 lsp::Position::new(0, 0),
9388 ),
9389 new_text: "applied-code-action-1-command\n".into(),
9390 }],
9391 )]
9392 .into_iter()
9393 .collect(),
9394 ),
9395 ..Default::default()
9396 },
9397 })
9398 .await
9399 .into_response()
9400 .unwrap();
9401 Ok(Some(json!(null)))
9402 }
9403 }
9404 });
9405
9406 cx.executor().start_waiting();
9407 editor
9408 .update_in(cx, |editor, window, cx| {
9409 editor.perform_format(
9410 project.clone(),
9411 FormatTrigger::Manual,
9412 FormatTarget::Buffers,
9413 window,
9414 cx,
9415 )
9416 })
9417 .unwrap()
9418 .await;
9419 editor.update(cx, |editor, cx| {
9420 assert_eq!(
9421 editor.text(cx),
9422 r#"
9423 applied-code-action-2-edit
9424 applied-code-action-1-command
9425 applied-code-action-1-edit
9426 applied-formatting
9427 one
9428 two
9429 three
9430 "#
9431 .unindent()
9432 );
9433 });
9434
9435 editor.update_in(cx, |editor, window, cx| {
9436 editor.undo(&Default::default(), window, cx);
9437 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9438 });
9439
9440 // Perform a manual edit while waiting for an LSP command
9441 // that's being run as part of a formatting code action.
9442 let lock_guard = command_lock.lock().await;
9443 let format = editor
9444 .update_in(cx, |editor, window, cx| {
9445 editor.perform_format(
9446 project.clone(),
9447 FormatTrigger::Manual,
9448 FormatTarget::Buffers,
9449 window,
9450 cx,
9451 )
9452 })
9453 .unwrap();
9454 cx.run_until_parked();
9455 editor.update(cx, |editor, cx| {
9456 assert_eq!(
9457 editor.text(cx),
9458 r#"
9459 applied-code-action-1-edit
9460 applied-formatting
9461 one
9462 two
9463 three
9464 "#
9465 .unindent()
9466 );
9467
9468 editor.buffer.update(cx, |buffer, cx| {
9469 let ix = buffer.len(cx);
9470 buffer.edit([(ix..ix, "edited\n")], None, cx);
9471 });
9472 });
9473
9474 // Allow the LSP command to proceed. Because the buffer was edited,
9475 // the second code action will not be run.
9476 drop(lock_guard);
9477 format.await;
9478 editor.update_in(cx, |editor, window, cx| {
9479 assert_eq!(
9480 editor.text(cx),
9481 r#"
9482 applied-code-action-1-command
9483 applied-code-action-1-edit
9484 applied-formatting
9485 one
9486 two
9487 three
9488 edited
9489 "#
9490 .unindent()
9491 );
9492
9493 // The manual edit is undone first, because it is the last thing the user did
9494 // (even though the command completed afterwards).
9495 editor.undo(&Default::default(), window, cx);
9496 assert_eq!(
9497 editor.text(cx),
9498 r#"
9499 applied-code-action-1-command
9500 applied-code-action-1-edit
9501 applied-formatting
9502 one
9503 two
9504 three
9505 "#
9506 .unindent()
9507 );
9508
9509 // All the formatting (including the command, which completed after the manual edit)
9510 // is undone together.
9511 editor.undo(&Default::default(), window, cx);
9512 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9513 });
9514}
9515
9516#[gpui::test]
9517async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9518 init_test(cx, |settings| {
9519 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9520 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9521 ))
9522 });
9523
9524 let fs = FakeFs::new(cx.executor());
9525 fs.insert_file(path!("/file.ts"), Default::default()).await;
9526
9527 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9528
9529 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9530 language_registry.add(Arc::new(Language::new(
9531 LanguageConfig {
9532 name: "TypeScript".into(),
9533 matcher: LanguageMatcher {
9534 path_suffixes: vec!["ts".to_string()],
9535 ..Default::default()
9536 },
9537 ..LanguageConfig::default()
9538 },
9539 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9540 )));
9541 update_test_language_settings(cx, |settings| {
9542 settings.defaults.prettier = Some(PrettierSettings {
9543 allowed: true,
9544 ..PrettierSettings::default()
9545 });
9546 });
9547 let mut fake_servers = language_registry.register_fake_lsp(
9548 "TypeScript",
9549 FakeLspAdapter {
9550 capabilities: lsp::ServerCapabilities {
9551 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9552 ..Default::default()
9553 },
9554 ..Default::default()
9555 },
9556 );
9557
9558 let buffer = project
9559 .update(cx, |project, cx| {
9560 project.open_local_buffer(path!("/file.ts"), cx)
9561 })
9562 .await
9563 .unwrap();
9564
9565 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9566 let (editor, cx) = cx.add_window_view(|window, cx| {
9567 build_editor_with_project(project.clone(), buffer, window, cx)
9568 });
9569 editor.update_in(cx, |editor, window, cx| {
9570 editor.set_text(
9571 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9572 window,
9573 cx,
9574 )
9575 });
9576
9577 cx.executor().start_waiting();
9578 let fake_server = fake_servers.next().await.unwrap();
9579
9580 let format = editor
9581 .update_in(cx, |editor, window, cx| {
9582 editor.perform_code_action_kind(
9583 project.clone(),
9584 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9585 window,
9586 cx,
9587 )
9588 })
9589 .unwrap();
9590 fake_server
9591 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9592 assert_eq!(
9593 params.text_document.uri,
9594 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9595 );
9596 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9597 lsp::CodeAction {
9598 title: "Organize Imports".to_string(),
9599 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9600 edit: Some(lsp::WorkspaceEdit {
9601 changes: Some(
9602 [(
9603 params.text_document.uri.clone(),
9604 vec![lsp::TextEdit::new(
9605 lsp::Range::new(
9606 lsp::Position::new(1, 0),
9607 lsp::Position::new(2, 0),
9608 ),
9609 "".to_string(),
9610 )],
9611 )]
9612 .into_iter()
9613 .collect(),
9614 ),
9615 ..Default::default()
9616 }),
9617 ..Default::default()
9618 },
9619 )]))
9620 })
9621 .next()
9622 .await;
9623 cx.executor().start_waiting();
9624 format.await;
9625 assert_eq!(
9626 editor.update(cx, |editor, cx| editor.text(cx)),
9627 "import { a } from 'module';\n\nconst x = a;\n"
9628 );
9629
9630 editor.update_in(cx, |editor, window, cx| {
9631 editor.set_text(
9632 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9633 window,
9634 cx,
9635 )
9636 });
9637 // Ensure we don't lock if code action hangs.
9638 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9639 move |params, _| async move {
9640 assert_eq!(
9641 params.text_document.uri,
9642 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9643 );
9644 futures::future::pending::<()>().await;
9645 unreachable!()
9646 },
9647 );
9648 let format = editor
9649 .update_in(cx, |editor, window, cx| {
9650 editor.perform_code_action_kind(
9651 project,
9652 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9653 window,
9654 cx,
9655 )
9656 })
9657 .unwrap();
9658 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9659 cx.executor().start_waiting();
9660 format.await;
9661 assert_eq!(
9662 editor.update(cx, |editor, cx| editor.text(cx)),
9663 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9664 );
9665}
9666
9667#[gpui::test]
9668async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9669 init_test(cx, |_| {});
9670
9671 let mut cx = EditorLspTestContext::new_rust(
9672 lsp::ServerCapabilities {
9673 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9674 ..Default::default()
9675 },
9676 cx,
9677 )
9678 .await;
9679
9680 cx.set_state(indoc! {"
9681 one.twoˇ
9682 "});
9683
9684 // The format request takes a long time. When it completes, it inserts
9685 // a newline and an indent before the `.`
9686 cx.lsp
9687 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9688 let executor = cx.background_executor().clone();
9689 async move {
9690 executor.timer(Duration::from_millis(100)).await;
9691 Ok(Some(vec![lsp::TextEdit {
9692 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9693 new_text: "\n ".into(),
9694 }]))
9695 }
9696 });
9697
9698 // Submit a format request.
9699 let format_1 = cx
9700 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9701 .unwrap();
9702 cx.executor().run_until_parked();
9703
9704 // Submit a second format request.
9705 let format_2 = cx
9706 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9707 .unwrap();
9708 cx.executor().run_until_parked();
9709
9710 // Wait for both format requests to complete
9711 cx.executor().advance_clock(Duration::from_millis(200));
9712 cx.executor().start_waiting();
9713 format_1.await.unwrap();
9714 cx.executor().start_waiting();
9715 format_2.await.unwrap();
9716
9717 // The formatting edits only happens once.
9718 cx.assert_editor_state(indoc! {"
9719 one
9720 .twoˇ
9721 "});
9722}
9723
9724#[gpui::test]
9725async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9726 init_test(cx, |settings| {
9727 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9728 });
9729
9730 let mut cx = EditorLspTestContext::new_rust(
9731 lsp::ServerCapabilities {
9732 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9733 ..Default::default()
9734 },
9735 cx,
9736 )
9737 .await;
9738
9739 // Set up a buffer white some trailing whitespace and no trailing newline.
9740 cx.set_state(
9741 &[
9742 "one ", //
9743 "twoˇ", //
9744 "three ", //
9745 "four", //
9746 ]
9747 .join("\n"),
9748 );
9749
9750 // Submit a format request.
9751 let format = cx
9752 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9753 .unwrap();
9754
9755 // Record which buffer changes have been sent to the language server
9756 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9757 cx.lsp
9758 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9759 let buffer_changes = buffer_changes.clone();
9760 move |params, _| {
9761 buffer_changes.lock().extend(
9762 params
9763 .content_changes
9764 .into_iter()
9765 .map(|e| (e.range.unwrap(), e.text)),
9766 );
9767 }
9768 });
9769
9770 // Handle formatting requests to the language server.
9771 cx.lsp
9772 .set_request_handler::<lsp::request::Formatting, _, _>({
9773 let buffer_changes = buffer_changes.clone();
9774 move |_, _| {
9775 // When formatting is requested, trailing whitespace has already been stripped,
9776 // and the trailing newline has already been added.
9777 assert_eq!(
9778 &buffer_changes.lock()[1..],
9779 &[
9780 (
9781 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9782 "".into()
9783 ),
9784 (
9785 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9786 "".into()
9787 ),
9788 (
9789 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9790 "\n".into()
9791 ),
9792 ]
9793 );
9794
9795 // Insert blank lines between each line of the buffer.
9796 async move {
9797 Ok(Some(vec![
9798 lsp::TextEdit {
9799 range: lsp::Range::new(
9800 lsp::Position::new(1, 0),
9801 lsp::Position::new(1, 0),
9802 ),
9803 new_text: "\n".into(),
9804 },
9805 lsp::TextEdit {
9806 range: lsp::Range::new(
9807 lsp::Position::new(2, 0),
9808 lsp::Position::new(2, 0),
9809 ),
9810 new_text: "\n".into(),
9811 },
9812 ]))
9813 }
9814 }
9815 });
9816
9817 // After formatting the buffer, the trailing whitespace is stripped,
9818 // a newline is appended, and the edits provided by the language server
9819 // have been applied.
9820 format.await.unwrap();
9821 cx.assert_editor_state(
9822 &[
9823 "one", //
9824 "", //
9825 "twoˇ", //
9826 "", //
9827 "three", //
9828 "four", //
9829 "", //
9830 ]
9831 .join("\n"),
9832 );
9833
9834 // Undoing the formatting undoes the trailing whitespace removal, the
9835 // trailing newline, and the LSP edits.
9836 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9837 cx.assert_editor_state(
9838 &[
9839 "one ", //
9840 "twoˇ", //
9841 "three ", //
9842 "four", //
9843 ]
9844 .join("\n"),
9845 );
9846}
9847
9848#[gpui::test]
9849async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9850 cx: &mut TestAppContext,
9851) {
9852 init_test(cx, |_| {});
9853
9854 cx.update(|cx| {
9855 cx.update_global::<SettingsStore, _>(|settings, cx| {
9856 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9857 settings.auto_signature_help = Some(true);
9858 });
9859 });
9860 });
9861
9862 let mut cx = EditorLspTestContext::new_rust(
9863 lsp::ServerCapabilities {
9864 signature_help_provider: Some(lsp::SignatureHelpOptions {
9865 ..Default::default()
9866 }),
9867 ..Default::default()
9868 },
9869 cx,
9870 )
9871 .await;
9872
9873 let language = Language::new(
9874 LanguageConfig {
9875 name: "Rust".into(),
9876 brackets: BracketPairConfig {
9877 pairs: vec![
9878 BracketPair {
9879 start: "{".to_string(),
9880 end: "}".to_string(),
9881 close: true,
9882 surround: true,
9883 newline: true,
9884 },
9885 BracketPair {
9886 start: "(".to_string(),
9887 end: ")".to_string(),
9888 close: true,
9889 surround: true,
9890 newline: true,
9891 },
9892 BracketPair {
9893 start: "/*".to_string(),
9894 end: " */".to_string(),
9895 close: true,
9896 surround: true,
9897 newline: true,
9898 },
9899 BracketPair {
9900 start: "[".to_string(),
9901 end: "]".to_string(),
9902 close: false,
9903 surround: false,
9904 newline: true,
9905 },
9906 BracketPair {
9907 start: "\"".to_string(),
9908 end: "\"".to_string(),
9909 close: true,
9910 surround: true,
9911 newline: false,
9912 },
9913 BracketPair {
9914 start: "<".to_string(),
9915 end: ">".to_string(),
9916 close: false,
9917 surround: true,
9918 newline: true,
9919 },
9920 ],
9921 ..Default::default()
9922 },
9923 autoclose_before: "})]".to_string(),
9924 ..Default::default()
9925 },
9926 Some(tree_sitter_rust::LANGUAGE.into()),
9927 );
9928 let language = Arc::new(language);
9929
9930 cx.language_registry().add(language.clone());
9931 cx.update_buffer(|buffer, cx| {
9932 buffer.set_language(Some(language), cx);
9933 });
9934
9935 cx.set_state(
9936 &r#"
9937 fn main() {
9938 sampleˇ
9939 }
9940 "#
9941 .unindent(),
9942 );
9943
9944 cx.update_editor(|editor, window, cx| {
9945 editor.handle_input("(", window, cx);
9946 });
9947 cx.assert_editor_state(
9948 &"
9949 fn main() {
9950 sample(ˇ)
9951 }
9952 "
9953 .unindent(),
9954 );
9955
9956 let mocked_response = lsp::SignatureHelp {
9957 signatures: vec![lsp::SignatureInformation {
9958 label: "fn sample(param1: u8, param2: u8)".to_string(),
9959 documentation: None,
9960 parameters: Some(vec![
9961 lsp::ParameterInformation {
9962 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9963 documentation: None,
9964 },
9965 lsp::ParameterInformation {
9966 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9967 documentation: None,
9968 },
9969 ]),
9970 active_parameter: None,
9971 }],
9972 active_signature: Some(0),
9973 active_parameter: Some(0),
9974 };
9975 handle_signature_help_request(&mut cx, mocked_response).await;
9976
9977 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9978 .await;
9979
9980 cx.editor(|editor, _, _| {
9981 let signature_help_state = editor.signature_help_state.popover().cloned();
9982 assert_eq!(
9983 signature_help_state.unwrap().label,
9984 "param1: u8, param2: u8"
9985 );
9986 });
9987}
9988
9989#[gpui::test]
9990async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9991 init_test(cx, |_| {});
9992
9993 cx.update(|cx| {
9994 cx.update_global::<SettingsStore, _>(|settings, cx| {
9995 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9996 settings.auto_signature_help = Some(false);
9997 settings.show_signature_help_after_edits = Some(false);
9998 });
9999 });
10000 });
10001
10002 let mut cx = EditorLspTestContext::new_rust(
10003 lsp::ServerCapabilities {
10004 signature_help_provider: Some(lsp::SignatureHelpOptions {
10005 ..Default::default()
10006 }),
10007 ..Default::default()
10008 },
10009 cx,
10010 )
10011 .await;
10012
10013 let language = Language::new(
10014 LanguageConfig {
10015 name: "Rust".into(),
10016 brackets: BracketPairConfig {
10017 pairs: vec![
10018 BracketPair {
10019 start: "{".to_string(),
10020 end: "}".to_string(),
10021 close: true,
10022 surround: true,
10023 newline: true,
10024 },
10025 BracketPair {
10026 start: "(".to_string(),
10027 end: ")".to_string(),
10028 close: true,
10029 surround: true,
10030 newline: true,
10031 },
10032 BracketPair {
10033 start: "/*".to_string(),
10034 end: " */".to_string(),
10035 close: true,
10036 surround: true,
10037 newline: true,
10038 },
10039 BracketPair {
10040 start: "[".to_string(),
10041 end: "]".to_string(),
10042 close: false,
10043 surround: false,
10044 newline: true,
10045 },
10046 BracketPair {
10047 start: "\"".to_string(),
10048 end: "\"".to_string(),
10049 close: true,
10050 surround: true,
10051 newline: false,
10052 },
10053 BracketPair {
10054 start: "<".to_string(),
10055 end: ">".to_string(),
10056 close: false,
10057 surround: true,
10058 newline: true,
10059 },
10060 ],
10061 ..Default::default()
10062 },
10063 autoclose_before: "})]".to_string(),
10064 ..Default::default()
10065 },
10066 Some(tree_sitter_rust::LANGUAGE.into()),
10067 );
10068 let language = Arc::new(language);
10069
10070 cx.language_registry().add(language.clone());
10071 cx.update_buffer(|buffer, cx| {
10072 buffer.set_language(Some(language), cx);
10073 });
10074
10075 // Ensure that signature_help is not called when no signature help is enabled.
10076 cx.set_state(
10077 &r#"
10078 fn main() {
10079 sampleˇ
10080 }
10081 "#
10082 .unindent(),
10083 );
10084 cx.update_editor(|editor, window, cx| {
10085 editor.handle_input("(", window, cx);
10086 });
10087 cx.assert_editor_state(
10088 &"
10089 fn main() {
10090 sample(ˇ)
10091 }
10092 "
10093 .unindent(),
10094 );
10095 cx.editor(|editor, _, _| {
10096 assert!(editor.signature_help_state.task().is_none());
10097 });
10098
10099 let mocked_response = lsp::SignatureHelp {
10100 signatures: vec![lsp::SignatureInformation {
10101 label: "fn sample(param1: u8, param2: u8)".to_string(),
10102 documentation: None,
10103 parameters: Some(vec![
10104 lsp::ParameterInformation {
10105 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10106 documentation: None,
10107 },
10108 lsp::ParameterInformation {
10109 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10110 documentation: None,
10111 },
10112 ]),
10113 active_parameter: None,
10114 }],
10115 active_signature: Some(0),
10116 active_parameter: Some(0),
10117 };
10118
10119 // Ensure that signature_help is called when enabled afte edits
10120 cx.update(|_, cx| {
10121 cx.update_global::<SettingsStore, _>(|settings, cx| {
10122 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10123 settings.auto_signature_help = Some(false);
10124 settings.show_signature_help_after_edits = Some(true);
10125 });
10126 });
10127 });
10128 cx.set_state(
10129 &r#"
10130 fn main() {
10131 sampleˇ
10132 }
10133 "#
10134 .unindent(),
10135 );
10136 cx.update_editor(|editor, window, cx| {
10137 editor.handle_input("(", window, cx);
10138 });
10139 cx.assert_editor_state(
10140 &"
10141 fn main() {
10142 sample(ˇ)
10143 }
10144 "
10145 .unindent(),
10146 );
10147 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10148 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10149 .await;
10150 cx.update_editor(|editor, _, _| {
10151 let signature_help_state = editor.signature_help_state.popover().cloned();
10152 assert!(signature_help_state.is_some());
10153 assert_eq!(
10154 signature_help_state.unwrap().label,
10155 "param1: u8, param2: u8"
10156 );
10157 editor.signature_help_state = SignatureHelpState::default();
10158 });
10159
10160 // Ensure that signature_help is called when auto signature help override is enabled
10161 cx.update(|_, cx| {
10162 cx.update_global::<SettingsStore, _>(|settings, cx| {
10163 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10164 settings.auto_signature_help = Some(true);
10165 settings.show_signature_help_after_edits = Some(false);
10166 });
10167 });
10168 });
10169 cx.set_state(
10170 &r#"
10171 fn main() {
10172 sampleˇ
10173 }
10174 "#
10175 .unindent(),
10176 );
10177 cx.update_editor(|editor, window, cx| {
10178 editor.handle_input("(", window, cx);
10179 });
10180 cx.assert_editor_state(
10181 &"
10182 fn main() {
10183 sample(ˇ)
10184 }
10185 "
10186 .unindent(),
10187 );
10188 handle_signature_help_request(&mut cx, mocked_response).await;
10189 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10190 .await;
10191 cx.editor(|editor, _, _| {
10192 let signature_help_state = editor.signature_help_state.popover().cloned();
10193 assert!(signature_help_state.is_some());
10194 assert_eq!(
10195 signature_help_state.unwrap().label,
10196 "param1: u8, param2: u8"
10197 );
10198 });
10199}
10200
10201#[gpui::test]
10202async fn test_signature_help(cx: &mut TestAppContext) {
10203 init_test(cx, |_| {});
10204 cx.update(|cx| {
10205 cx.update_global::<SettingsStore, _>(|settings, cx| {
10206 settings.update_user_settings::<EditorSettings>(cx, |settings| {
10207 settings.auto_signature_help = Some(true);
10208 });
10209 });
10210 });
10211
10212 let mut cx = EditorLspTestContext::new_rust(
10213 lsp::ServerCapabilities {
10214 signature_help_provider: Some(lsp::SignatureHelpOptions {
10215 ..Default::default()
10216 }),
10217 ..Default::default()
10218 },
10219 cx,
10220 )
10221 .await;
10222
10223 // A test that directly calls `show_signature_help`
10224 cx.update_editor(|editor, window, cx| {
10225 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10226 });
10227
10228 let mocked_response = lsp::SignatureHelp {
10229 signatures: vec![lsp::SignatureInformation {
10230 label: "fn sample(param1: u8, param2: u8)".to_string(),
10231 documentation: None,
10232 parameters: Some(vec![
10233 lsp::ParameterInformation {
10234 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10235 documentation: None,
10236 },
10237 lsp::ParameterInformation {
10238 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10239 documentation: None,
10240 },
10241 ]),
10242 active_parameter: None,
10243 }],
10244 active_signature: Some(0),
10245 active_parameter: Some(0),
10246 };
10247 handle_signature_help_request(&mut cx, mocked_response).await;
10248
10249 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10250 .await;
10251
10252 cx.editor(|editor, _, _| {
10253 let signature_help_state = editor.signature_help_state.popover().cloned();
10254 assert!(signature_help_state.is_some());
10255 assert_eq!(
10256 signature_help_state.unwrap().label,
10257 "param1: u8, param2: u8"
10258 );
10259 });
10260
10261 // When exiting outside from inside the brackets, `signature_help` is closed.
10262 cx.set_state(indoc! {"
10263 fn main() {
10264 sample(ˇ);
10265 }
10266
10267 fn sample(param1: u8, param2: u8) {}
10268 "});
10269
10270 cx.update_editor(|editor, window, cx| {
10271 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
10272 });
10273
10274 let mocked_response = lsp::SignatureHelp {
10275 signatures: Vec::new(),
10276 active_signature: None,
10277 active_parameter: None,
10278 };
10279 handle_signature_help_request(&mut cx, mocked_response).await;
10280
10281 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10282 .await;
10283
10284 cx.editor(|editor, _, _| {
10285 assert!(!editor.signature_help_state.is_shown());
10286 });
10287
10288 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
10289 cx.set_state(indoc! {"
10290 fn main() {
10291 sample(ˇ);
10292 }
10293
10294 fn sample(param1: u8, param2: u8) {}
10295 "});
10296
10297 let mocked_response = lsp::SignatureHelp {
10298 signatures: vec![lsp::SignatureInformation {
10299 label: "fn sample(param1: u8, param2: u8)".to_string(),
10300 documentation: None,
10301 parameters: Some(vec![
10302 lsp::ParameterInformation {
10303 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10304 documentation: None,
10305 },
10306 lsp::ParameterInformation {
10307 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10308 documentation: None,
10309 },
10310 ]),
10311 active_parameter: None,
10312 }],
10313 active_signature: Some(0),
10314 active_parameter: Some(0),
10315 };
10316 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10317 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10318 .await;
10319 cx.editor(|editor, _, _| {
10320 assert!(editor.signature_help_state.is_shown());
10321 });
10322
10323 // Restore the popover with more parameter input
10324 cx.set_state(indoc! {"
10325 fn main() {
10326 sample(param1, param2ˇ);
10327 }
10328
10329 fn sample(param1: u8, param2: u8) {}
10330 "});
10331
10332 let mocked_response = lsp::SignatureHelp {
10333 signatures: vec![lsp::SignatureInformation {
10334 label: "fn sample(param1: u8, param2: u8)".to_string(),
10335 documentation: None,
10336 parameters: Some(vec![
10337 lsp::ParameterInformation {
10338 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10339 documentation: None,
10340 },
10341 lsp::ParameterInformation {
10342 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10343 documentation: None,
10344 },
10345 ]),
10346 active_parameter: None,
10347 }],
10348 active_signature: Some(0),
10349 active_parameter: Some(1),
10350 };
10351 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10352 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10353 .await;
10354
10355 // When selecting a range, the popover is gone.
10356 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
10357 cx.update_editor(|editor, window, cx| {
10358 editor.change_selections(None, window, cx, |s| {
10359 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10360 })
10361 });
10362 cx.assert_editor_state(indoc! {"
10363 fn main() {
10364 sample(param1, «ˇparam2»);
10365 }
10366
10367 fn sample(param1: u8, param2: u8) {}
10368 "});
10369 cx.editor(|editor, _, _| {
10370 assert!(!editor.signature_help_state.is_shown());
10371 });
10372
10373 // When unselecting again, the popover is back if within the brackets.
10374 cx.update_editor(|editor, window, cx| {
10375 editor.change_selections(None, window, cx, |s| {
10376 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10377 })
10378 });
10379 cx.assert_editor_state(indoc! {"
10380 fn main() {
10381 sample(param1, ˇparam2);
10382 }
10383
10384 fn sample(param1: u8, param2: u8) {}
10385 "});
10386 handle_signature_help_request(&mut cx, mocked_response).await;
10387 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10388 .await;
10389 cx.editor(|editor, _, _| {
10390 assert!(editor.signature_help_state.is_shown());
10391 });
10392
10393 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10394 cx.update_editor(|editor, window, cx| {
10395 editor.change_selections(None, window, cx, |s| {
10396 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10397 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10398 })
10399 });
10400 cx.assert_editor_state(indoc! {"
10401 fn main() {
10402 sample(param1, ˇparam2);
10403 }
10404
10405 fn sample(param1: u8, param2: u8) {}
10406 "});
10407
10408 let mocked_response = lsp::SignatureHelp {
10409 signatures: vec![lsp::SignatureInformation {
10410 label: "fn sample(param1: u8, param2: u8)".to_string(),
10411 documentation: None,
10412 parameters: Some(vec![
10413 lsp::ParameterInformation {
10414 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10415 documentation: None,
10416 },
10417 lsp::ParameterInformation {
10418 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10419 documentation: None,
10420 },
10421 ]),
10422 active_parameter: None,
10423 }],
10424 active_signature: Some(0),
10425 active_parameter: Some(1),
10426 };
10427 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10428 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10429 .await;
10430 cx.update_editor(|editor, _, cx| {
10431 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10432 });
10433 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10434 .await;
10435 cx.update_editor(|editor, window, cx| {
10436 editor.change_selections(None, window, cx, |s| {
10437 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10438 })
10439 });
10440 cx.assert_editor_state(indoc! {"
10441 fn main() {
10442 sample(param1, «ˇparam2»);
10443 }
10444
10445 fn sample(param1: u8, param2: u8) {}
10446 "});
10447 cx.update_editor(|editor, window, cx| {
10448 editor.change_selections(None, window, cx, |s| {
10449 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10450 })
10451 });
10452 cx.assert_editor_state(indoc! {"
10453 fn main() {
10454 sample(param1, ˇparam2);
10455 }
10456
10457 fn sample(param1: u8, param2: u8) {}
10458 "});
10459 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10460 .await;
10461}
10462
10463#[gpui::test]
10464async fn test_completion_mode(cx: &mut TestAppContext) {
10465 init_test(cx, |_| {});
10466 let mut cx = EditorLspTestContext::new_rust(
10467 lsp::ServerCapabilities {
10468 completion_provider: Some(lsp::CompletionOptions {
10469 resolve_provider: Some(true),
10470 ..Default::default()
10471 }),
10472 ..Default::default()
10473 },
10474 cx,
10475 )
10476 .await;
10477
10478 struct Run {
10479 run_description: &'static str,
10480 initial_state: String,
10481 buffer_marked_text: String,
10482 completion_label: &'static str,
10483 completion_text: &'static str,
10484 expected_with_insert_mode: String,
10485 expected_with_replace_mode: String,
10486 expected_with_replace_subsequence_mode: String,
10487 expected_with_replace_suffix_mode: String,
10488 }
10489
10490 let runs = [
10491 Run {
10492 run_description: "Start of word matches completion text",
10493 initial_state: "before ediˇ after".into(),
10494 buffer_marked_text: "before <edi|> after".into(),
10495 completion_label: "editor",
10496 completion_text: "editor",
10497 expected_with_insert_mode: "before editorˇ after".into(),
10498 expected_with_replace_mode: "before editorˇ after".into(),
10499 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10500 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10501 },
10502 Run {
10503 run_description: "Accept same text at the middle of the word",
10504 initial_state: "before ediˇtor after".into(),
10505 buffer_marked_text: "before <edi|tor> after".into(),
10506 completion_label: "editor",
10507 completion_text: "editor",
10508 expected_with_insert_mode: "before editorˇtor after".into(),
10509 expected_with_replace_mode: "before editorˇ after".into(),
10510 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10511 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10512 },
10513 Run {
10514 run_description: "End of word matches completion text -- cursor at end",
10515 initial_state: "before torˇ after".into(),
10516 buffer_marked_text: "before <tor|> after".into(),
10517 completion_label: "editor",
10518 completion_text: "editor",
10519 expected_with_insert_mode: "before editorˇ after".into(),
10520 expected_with_replace_mode: "before editorˇ after".into(),
10521 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10522 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10523 },
10524 Run {
10525 run_description: "End of word matches completion text -- cursor at start",
10526 initial_state: "before ˇtor after".into(),
10527 buffer_marked_text: "before <|tor> after".into(),
10528 completion_label: "editor",
10529 completion_text: "editor",
10530 expected_with_insert_mode: "before editorˇtor after".into(),
10531 expected_with_replace_mode: "before editorˇ after".into(),
10532 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10533 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10534 },
10535 Run {
10536 run_description: "Prepend text containing whitespace",
10537 initial_state: "pˇfield: bool".into(),
10538 buffer_marked_text: "<p|field>: bool".into(),
10539 completion_label: "pub ",
10540 completion_text: "pub ",
10541 expected_with_insert_mode: "pub ˇfield: bool".into(),
10542 expected_with_replace_mode: "pub ˇ: bool".into(),
10543 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10544 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10545 },
10546 Run {
10547 run_description: "Add element to start of list",
10548 initial_state: "[element_ˇelement_2]".into(),
10549 buffer_marked_text: "[<element_|element_2>]".into(),
10550 completion_label: "element_1",
10551 completion_text: "element_1",
10552 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10553 expected_with_replace_mode: "[element_1ˇ]".into(),
10554 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10555 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10556 },
10557 Run {
10558 run_description: "Add element to start of list -- first and second elements are equal",
10559 initial_state: "[elˇelement]".into(),
10560 buffer_marked_text: "[<el|element>]".into(),
10561 completion_label: "element",
10562 completion_text: "element",
10563 expected_with_insert_mode: "[elementˇelement]".into(),
10564 expected_with_replace_mode: "[elementˇ]".into(),
10565 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10566 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10567 },
10568 Run {
10569 run_description: "Ends with matching suffix",
10570 initial_state: "SubˇError".into(),
10571 buffer_marked_text: "<Sub|Error>".into(),
10572 completion_label: "SubscriptionError",
10573 completion_text: "SubscriptionError",
10574 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10575 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10576 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10577 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10578 },
10579 Run {
10580 run_description: "Suffix is a subsequence -- contiguous",
10581 initial_state: "SubˇErr".into(),
10582 buffer_marked_text: "<Sub|Err>".into(),
10583 completion_label: "SubscriptionError",
10584 completion_text: "SubscriptionError",
10585 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10586 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10587 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10588 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10589 },
10590 Run {
10591 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10592 initial_state: "Suˇscrirr".into(),
10593 buffer_marked_text: "<Su|scrirr>".into(),
10594 completion_label: "SubscriptionError",
10595 completion_text: "SubscriptionError",
10596 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10597 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10598 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10599 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10600 },
10601 Run {
10602 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10603 initial_state: "foo(indˇix)".into(),
10604 buffer_marked_text: "foo(<ind|ix>)".into(),
10605 completion_label: "node_index",
10606 completion_text: "node_index",
10607 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10608 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10609 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10610 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10611 },
10612 Run {
10613 run_description: "Replace range ends before cursor - should extend to cursor",
10614 initial_state: "before editˇo after".into(),
10615 buffer_marked_text: "before <{ed}>it|o after".into(),
10616 completion_label: "editor",
10617 completion_text: "editor",
10618 expected_with_insert_mode: "before editorˇo after".into(),
10619 expected_with_replace_mode: "before editorˇo after".into(),
10620 expected_with_replace_subsequence_mode: "before editorˇo after".into(),
10621 expected_with_replace_suffix_mode: "before editorˇo after".into(),
10622 },
10623 Run {
10624 run_description: "Uses label for suffix matching",
10625 initial_state: "before ediˇtor after".into(),
10626 buffer_marked_text: "before <edi|tor> after".into(),
10627 completion_label: "editor",
10628 completion_text: "editor()",
10629 expected_with_insert_mode: "before editor()ˇtor after".into(),
10630 expected_with_replace_mode: "before editor()ˇ after".into(),
10631 expected_with_replace_subsequence_mode: "before editor()ˇ after".into(),
10632 expected_with_replace_suffix_mode: "before editor()ˇ after".into(),
10633 },
10634 Run {
10635 run_description: "Case insensitive subsequence and suffix matching",
10636 initial_state: "before EDiˇtoR after".into(),
10637 buffer_marked_text: "before <EDi|toR> after".into(),
10638 completion_label: "editor",
10639 completion_text: "editor",
10640 expected_with_insert_mode: "before editorˇtoR after".into(),
10641 expected_with_replace_mode: "before editorˇ after".into(),
10642 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10643 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10644 },
10645 ];
10646
10647 for run in runs {
10648 let run_variations = [
10649 (LspInsertMode::Insert, run.expected_with_insert_mode),
10650 (LspInsertMode::Replace, run.expected_with_replace_mode),
10651 (
10652 LspInsertMode::ReplaceSubsequence,
10653 run.expected_with_replace_subsequence_mode,
10654 ),
10655 (
10656 LspInsertMode::ReplaceSuffix,
10657 run.expected_with_replace_suffix_mode,
10658 ),
10659 ];
10660
10661 for (lsp_insert_mode, expected_text) in run_variations {
10662 eprintln!(
10663 "run = {:?}, mode = {lsp_insert_mode:.?}",
10664 run.run_description,
10665 );
10666
10667 update_test_language_settings(&mut cx, |settings| {
10668 settings.defaults.completions = Some(CompletionSettings {
10669 lsp_insert_mode,
10670 words: WordsCompletionMode::Disabled,
10671 lsp: true,
10672 lsp_fetch_timeout_ms: 0,
10673 });
10674 });
10675
10676 cx.set_state(&run.initial_state);
10677 cx.update_editor(|editor, window, cx| {
10678 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10679 });
10680
10681 let counter = Arc::new(AtomicUsize::new(0));
10682 handle_completion_request_with_insert_and_replace(
10683 &mut cx,
10684 &run.buffer_marked_text,
10685 vec![(run.completion_label, run.completion_text)],
10686 counter.clone(),
10687 )
10688 .await;
10689 cx.condition(|editor, _| editor.context_menu_visible())
10690 .await;
10691 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10692
10693 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10694 editor
10695 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10696 .unwrap()
10697 });
10698 cx.assert_editor_state(&expected_text);
10699 handle_resolve_completion_request(&mut cx, None).await;
10700 apply_additional_edits.await.unwrap();
10701 }
10702 }
10703}
10704
10705#[gpui::test]
10706async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10707 init_test(cx, |_| {});
10708 let mut cx = EditorLspTestContext::new_rust(
10709 lsp::ServerCapabilities {
10710 completion_provider: Some(lsp::CompletionOptions {
10711 resolve_provider: Some(true),
10712 ..Default::default()
10713 }),
10714 ..Default::default()
10715 },
10716 cx,
10717 )
10718 .await;
10719
10720 let initial_state = "SubˇError";
10721 let buffer_marked_text = "<Sub|Error>";
10722 let completion_text = "SubscriptionError";
10723 let expected_with_insert_mode = "SubscriptionErrorˇError";
10724 let expected_with_replace_mode = "SubscriptionErrorˇ";
10725
10726 update_test_language_settings(&mut cx, |settings| {
10727 settings.defaults.completions = Some(CompletionSettings {
10728 words: WordsCompletionMode::Disabled,
10729 // set the opposite here to ensure that the action is overriding the default behavior
10730 lsp_insert_mode: LspInsertMode::Insert,
10731 lsp: true,
10732 lsp_fetch_timeout_ms: 0,
10733 });
10734 });
10735
10736 cx.set_state(initial_state);
10737 cx.update_editor(|editor, window, cx| {
10738 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10739 });
10740
10741 let counter = Arc::new(AtomicUsize::new(0));
10742 handle_completion_request_with_insert_and_replace(
10743 &mut cx,
10744 &buffer_marked_text,
10745 vec![(completion_text, completion_text)],
10746 counter.clone(),
10747 )
10748 .await;
10749 cx.condition(|editor, _| editor.context_menu_visible())
10750 .await;
10751 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10752
10753 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10754 editor
10755 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10756 .unwrap()
10757 });
10758 cx.assert_editor_state(&expected_with_replace_mode);
10759 handle_resolve_completion_request(&mut cx, None).await;
10760 apply_additional_edits.await.unwrap();
10761
10762 update_test_language_settings(&mut cx, |settings| {
10763 settings.defaults.completions = Some(CompletionSettings {
10764 words: WordsCompletionMode::Disabled,
10765 // set the opposite here to ensure that the action is overriding the default behavior
10766 lsp_insert_mode: LspInsertMode::Replace,
10767 lsp: true,
10768 lsp_fetch_timeout_ms: 0,
10769 });
10770 });
10771
10772 cx.set_state(initial_state);
10773 cx.update_editor(|editor, window, cx| {
10774 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10775 });
10776 handle_completion_request_with_insert_and_replace(
10777 &mut cx,
10778 &buffer_marked_text,
10779 vec![(completion_text, completion_text)],
10780 counter.clone(),
10781 )
10782 .await;
10783 cx.condition(|editor, _| editor.context_menu_visible())
10784 .await;
10785 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10786
10787 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10788 editor
10789 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10790 .unwrap()
10791 });
10792 cx.assert_editor_state(&expected_with_insert_mode);
10793 handle_resolve_completion_request(&mut cx, None).await;
10794 apply_additional_edits.await.unwrap();
10795}
10796
10797#[gpui::test]
10798async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10799 init_test(cx, |_| {});
10800 let mut cx = EditorLspTestContext::new_rust(
10801 lsp::ServerCapabilities {
10802 completion_provider: Some(lsp::CompletionOptions {
10803 resolve_provider: Some(true),
10804 ..Default::default()
10805 }),
10806 ..Default::default()
10807 },
10808 cx,
10809 )
10810 .await;
10811
10812 // scenario: surrounding text matches completion text
10813 let completion_text = "to_offset";
10814 let initial_state = indoc! {"
10815 1. buf.to_offˇsuffix
10816 2. buf.to_offˇsuf
10817 3. buf.to_offˇfix
10818 4. buf.to_offˇ
10819 5. into_offˇensive
10820 6. ˇsuffix
10821 7. let ˇ //
10822 8. aaˇzz
10823 9. buf.to_off«zzzzzˇ»suffix
10824 10. buf.«ˇzzzzz»suffix
10825 11. to_off«ˇzzzzz»
10826
10827 buf.to_offˇsuffix // newest cursor
10828 "};
10829 let completion_marked_buffer = indoc! {"
10830 1. buf.to_offsuffix
10831 2. buf.to_offsuf
10832 3. buf.to_offfix
10833 4. buf.to_off
10834 5. into_offensive
10835 6. suffix
10836 7. let //
10837 8. aazz
10838 9. buf.to_offzzzzzsuffix
10839 10. buf.zzzzzsuffix
10840 11. to_offzzzzz
10841
10842 buf.<to_off|suffix> // newest cursor
10843 "};
10844 let expected = indoc! {"
10845 1. buf.to_offsetˇ
10846 2. buf.to_offsetˇsuf
10847 3. buf.to_offsetˇfix
10848 4. buf.to_offsetˇ
10849 5. into_offsetˇensive
10850 6. to_offsetˇsuffix
10851 7. let to_offsetˇ //
10852 8. aato_offsetˇzz
10853 9. buf.to_offsetˇ
10854 10. buf.to_offsetˇsuffix
10855 11. to_offsetˇ
10856
10857 buf.to_offsetˇ // newest cursor
10858 "};
10859 cx.set_state(initial_state);
10860 cx.update_editor(|editor, window, cx| {
10861 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10862 });
10863 handle_completion_request_with_insert_and_replace(
10864 &mut cx,
10865 completion_marked_buffer,
10866 vec![(completion_text, completion_text)],
10867 Arc::new(AtomicUsize::new(0)),
10868 )
10869 .await;
10870 cx.condition(|editor, _| editor.context_menu_visible())
10871 .await;
10872 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10873 editor
10874 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10875 .unwrap()
10876 });
10877 cx.assert_editor_state(expected);
10878 handle_resolve_completion_request(&mut cx, None).await;
10879 apply_additional_edits.await.unwrap();
10880
10881 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10882 let completion_text = "foo_and_bar";
10883 let initial_state = indoc! {"
10884 1. ooanbˇ
10885 2. zooanbˇ
10886 3. ooanbˇz
10887 4. zooanbˇz
10888 5. ooanˇ
10889 6. oanbˇ
10890
10891 ooanbˇ
10892 "};
10893 let completion_marked_buffer = indoc! {"
10894 1. ooanb
10895 2. zooanb
10896 3. ooanbz
10897 4. zooanbz
10898 5. ooan
10899 6. oanb
10900
10901 <ooanb|>
10902 "};
10903 let expected = indoc! {"
10904 1. foo_and_barˇ
10905 2. zfoo_and_barˇ
10906 3. foo_and_barˇz
10907 4. zfoo_and_barˇz
10908 5. ooanfoo_and_barˇ
10909 6. oanbfoo_and_barˇ
10910
10911 foo_and_barˇ
10912 "};
10913 cx.set_state(initial_state);
10914 cx.update_editor(|editor, window, cx| {
10915 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10916 });
10917 handle_completion_request_with_insert_and_replace(
10918 &mut cx,
10919 completion_marked_buffer,
10920 vec![(completion_text, completion_text)],
10921 Arc::new(AtomicUsize::new(0)),
10922 )
10923 .await;
10924 cx.condition(|editor, _| editor.context_menu_visible())
10925 .await;
10926 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10927 editor
10928 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10929 .unwrap()
10930 });
10931 cx.assert_editor_state(expected);
10932 handle_resolve_completion_request(&mut cx, None).await;
10933 apply_additional_edits.await.unwrap();
10934
10935 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10936 // (expects the same as if it was inserted at the end)
10937 let completion_text = "foo_and_bar";
10938 let initial_state = indoc! {"
10939 1. ooˇanb
10940 2. zooˇanb
10941 3. ooˇanbz
10942 4. zooˇanbz
10943
10944 ooˇanb
10945 "};
10946 let completion_marked_buffer = indoc! {"
10947 1. ooanb
10948 2. zooanb
10949 3. ooanbz
10950 4. zooanbz
10951
10952 <oo|anb>
10953 "};
10954 let expected = indoc! {"
10955 1. foo_and_barˇ
10956 2. zfoo_and_barˇ
10957 3. foo_and_barˇz
10958 4. zfoo_and_barˇz
10959
10960 foo_and_barˇ
10961 "};
10962 cx.set_state(initial_state);
10963 cx.update_editor(|editor, window, cx| {
10964 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10965 });
10966 handle_completion_request_with_insert_and_replace(
10967 &mut cx,
10968 completion_marked_buffer,
10969 vec![(completion_text, completion_text)],
10970 Arc::new(AtomicUsize::new(0)),
10971 )
10972 .await;
10973 cx.condition(|editor, _| editor.context_menu_visible())
10974 .await;
10975 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10976 editor
10977 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10978 .unwrap()
10979 });
10980 cx.assert_editor_state(expected);
10981 handle_resolve_completion_request(&mut cx, None).await;
10982 apply_additional_edits.await.unwrap();
10983}
10984
10985// This used to crash
10986#[gpui::test]
10987async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10988 init_test(cx, |_| {});
10989
10990 let buffer_text = indoc! {"
10991 fn main() {
10992 10.satu;
10993
10994 //
10995 // separate cursors so they open in different excerpts (manually reproducible)
10996 //
10997
10998 10.satu20;
10999 }
11000 "};
11001 let multibuffer_text_with_selections = indoc! {"
11002 fn main() {
11003 10.satuˇ;
11004
11005 //
11006
11007 //
11008
11009 10.satuˇ20;
11010 }
11011 "};
11012 let expected_multibuffer = indoc! {"
11013 fn main() {
11014 10.saturating_sub()ˇ;
11015
11016 //
11017
11018 //
11019
11020 10.saturating_sub()ˇ;
11021 }
11022 "};
11023
11024 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
11025 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
11026
11027 let fs = FakeFs::new(cx.executor());
11028 fs.insert_tree(
11029 path!("/a"),
11030 json!({
11031 "main.rs": buffer_text,
11032 }),
11033 )
11034 .await;
11035
11036 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11037 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11038 language_registry.add(rust_lang());
11039 let mut fake_servers = language_registry.register_fake_lsp(
11040 "Rust",
11041 FakeLspAdapter {
11042 capabilities: lsp::ServerCapabilities {
11043 completion_provider: Some(lsp::CompletionOptions {
11044 resolve_provider: None,
11045 ..lsp::CompletionOptions::default()
11046 }),
11047 ..lsp::ServerCapabilities::default()
11048 },
11049 ..FakeLspAdapter::default()
11050 },
11051 );
11052 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11053 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11054 let buffer = project
11055 .update(cx, |project, cx| {
11056 project.open_local_buffer(path!("/a/main.rs"), cx)
11057 })
11058 .await
11059 .unwrap();
11060
11061 let multi_buffer = cx.new(|cx| {
11062 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
11063 multi_buffer.push_excerpts(
11064 buffer.clone(),
11065 [ExcerptRange::new(0..first_excerpt_end)],
11066 cx,
11067 );
11068 multi_buffer.push_excerpts(
11069 buffer.clone(),
11070 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
11071 cx,
11072 );
11073 multi_buffer
11074 });
11075
11076 let editor = workspace
11077 .update(cx, |_, window, cx| {
11078 cx.new(|cx| {
11079 Editor::new(
11080 EditorMode::Full {
11081 scale_ui_elements_with_buffer_font_size: false,
11082 show_active_line_background: false,
11083 sized_by_content: false,
11084 },
11085 multi_buffer.clone(),
11086 Some(project.clone()),
11087 window,
11088 cx,
11089 )
11090 })
11091 })
11092 .unwrap();
11093
11094 let pane = workspace
11095 .update(cx, |workspace, _, _| workspace.active_pane().clone())
11096 .unwrap();
11097 pane.update_in(cx, |pane, window, cx| {
11098 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
11099 });
11100
11101 let fake_server = fake_servers.next().await.unwrap();
11102
11103 editor.update_in(cx, |editor, window, cx| {
11104 editor.change_selections(None, window, cx, |s| {
11105 s.select_ranges([
11106 Point::new(1, 11)..Point::new(1, 11),
11107 Point::new(7, 11)..Point::new(7, 11),
11108 ])
11109 });
11110
11111 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
11112 });
11113
11114 editor.update_in(cx, |editor, window, cx| {
11115 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11116 });
11117
11118 fake_server
11119 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11120 let completion_item = lsp::CompletionItem {
11121 label: "saturating_sub()".into(),
11122 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
11123 lsp::InsertReplaceEdit {
11124 new_text: "saturating_sub()".to_owned(),
11125 insert: lsp::Range::new(
11126 lsp::Position::new(7, 7),
11127 lsp::Position::new(7, 11),
11128 ),
11129 replace: lsp::Range::new(
11130 lsp::Position::new(7, 7),
11131 lsp::Position::new(7, 13),
11132 ),
11133 },
11134 )),
11135 ..lsp::CompletionItem::default()
11136 };
11137
11138 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
11139 })
11140 .next()
11141 .await
11142 .unwrap();
11143
11144 cx.condition(&editor, |editor, _| editor.context_menu_visible())
11145 .await;
11146
11147 editor
11148 .update_in(cx, |editor, window, cx| {
11149 editor
11150 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
11151 .unwrap()
11152 })
11153 .await
11154 .unwrap();
11155
11156 editor.update(cx, |editor, cx| {
11157 assert_text_with_selections(editor, expected_multibuffer, cx);
11158 })
11159}
11160
11161#[gpui::test]
11162async fn test_completion(cx: &mut TestAppContext) {
11163 init_test(cx, |_| {});
11164
11165 let mut cx = EditorLspTestContext::new_rust(
11166 lsp::ServerCapabilities {
11167 completion_provider: Some(lsp::CompletionOptions {
11168 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11169 resolve_provider: Some(true),
11170 ..Default::default()
11171 }),
11172 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11173 ..Default::default()
11174 },
11175 cx,
11176 )
11177 .await;
11178 let counter = Arc::new(AtomicUsize::new(0));
11179
11180 cx.set_state(indoc! {"
11181 oneˇ
11182 two
11183 three
11184 "});
11185 cx.simulate_keystroke(".");
11186 handle_completion_request(
11187 &mut cx,
11188 indoc! {"
11189 one.|<>
11190 two
11191 three
11192 "},
11193 vec!["first_completion", "second_completion"],
11194 counter.clone(),
11195 )
11196 .await;
11197 cx.condition(|editor, _| editor.context_menu_visible())
11198 .await;
11199 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11200
11201 let _handler = handle_signature_help_request(
11202 &mut cx,
11203 lsp::SignatureHelp {
11204 signatures: vec![lsp::SignatureInformation {
11205 label: "test signature".to_string(),
11206 documentation: None,
11207 parameters: Some(vec![lsp::ParameterInformation {
11208 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
11209 documentation: None,
11210 }]),
11211 active_parameter: None,
11212 }],
11213 active_signature: None,
11214 active_parameter: None,
11215 },
11216 );
11217 cx.update_editor(|editor, window, cx| {
11218 assert!(
11219 !editor.signature_help_state.is_shown(),
11220 "No signature help was called for"
11221 );
11222 editor.show_signature_help(&ShowSignatureHelp, window, cx);
11223 });
11224 cx.run_until_parked();
11225 cx.update_editor(|editor, _, _| {
11226 assert!(
11227 !editor.signature_help_state.is_shown(),
11228 "No signature help should be shown when completions menu is open"
11229 );
11230 });
11231
11232 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11233 editor.context_menu_next(&Default::default(), window, cx);
11234 editor
11235 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11236 .unwrap()
11237 });
11238 cx.assert_editor_state(indoc! {"
11239 one.second_completionˇ
11240 two
11241 three
11242 "});
11243
11244 handle_resolve_completion_request(
11245 &mut cx,
11246 Some(vec![
11247 (
11248 //This overlaps with the primary completion edit which is
11249 //misbehavior from the LSP spec, test that we filter it out
11250 indoc! {"
11251 one.second_ˇcompletion
11252 two
11253 threeˇ
11254 "},
11255 "overlapping additional edit",
11256 ),
11257 (
11258 indoc! {"
11259 one.second_completion
11260 two
11261 threeˇ
11262 "},
11263 "\nadditional edit",
11264 ),
11265 ]),
11266 )
11267 .await;
11268 apply_additional_edits.await.unwrap();
11269 cx.assert_editor_state(indoc! {"
11270 one.second_completionˇ
11271 two
11272 three
11273 additional edit
11274 "});
11275
11276 cx.set_state(indoc! {"
11277 one.second_completion
11278 twoˇ
11279 threeˇ
11280 additional edit
11281 "});
11282 cx.simulate_keystroke(" ");
11283 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11284 cx.simulate_keystroke("s");
11285 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11286
11287 cx.assert_editor_state(indoc! {"
11288 one.second_completion
11289 two sˇ
11290 three sˇ
11291 additional edit
11292 "});
11293 handle_completion_request(
11294 &mut cx,
11295 indoc! {"
11296 one.second_completion
11297 two s
11298 three <s|>
11299 additional edit
11300 "},
11301 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11302 counter.clone(),
11303 )
11304 .await;
11305 cx.condition(|editor, _| editor.context_menu_visible())
11306 .await;
11307 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
11308
11309 cx.simulate_keystroke("i");
11310
11311 handle_completion_request(
11312 &mut cx,
11313 indoc! {"
11314 one.second_completion
11315 two si
11316 three <si|>
11317 additional edit
11318 "},
11319 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
11320 counter.clone(),
11321 )
11322 .await;
11323 cx.condition(|editor, _| editor.context_menu_visible())
11324 .await;
11325 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
11326
11327 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11328 editor
11329 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11330 .unwrap()
11331 });
11332 cx.assert_editor_state(indoc! {"
11333 one.second_completion
11334 two sixth_completionˇ
11335 three sixth_completionˇ
11336 additional edit
11337 "});
11338
11339 apply_additional_edits.await.unwrap();
11340
11341 update_test_language_settings(&mut cx, |settings| {
11342 settings.defaults.show_completions_on_input = Some(false);
11343 });
11344 cx.set_state("editorˇ");
11345 cx.simulate_keystroke(".");
11346 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11347 cx.simulate_keystrokes("c l o");
11348 cx.assert_editor_state("editor.cloˇ");
11349 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
11350 cx.update_editor(|editor, window, cx| {
11351 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
11352 });
11353 handle_completion_request(
11354 &mut cx,
11355 "editor.<clo|>",
11356 vec!["close", "clobber"],
11357 counter.clone(),
11358 )
11359 .await;
11360 cx.condition(|editor, _| editor.context_menu_visible())
11361 .await;
11362 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
11363
11364 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
11365 editor
11366 .confirm_completion(&ConfirmCompletion::default(), window, cx)
11367 .unwrap()
11368 });
11369 cx.assert_editor_state("editor.closeˇ");
11370 handle_resolve_completion_request(&mut cx, None).await;
11371 apply_additional_edits.await.unwrap();
11372}
11373
11374#[gpui::test]
11375async fn test_word_completion(cx: &mut TestAppContext) {
11376 let lsp_fetch_timeout_ms = 10;
11377 init_test(cx, |language_settings| {
11378 language_settings.defaults.completions = Some(CompletionSettings {
11379 words: WordsCompletionMode::Fallback,
11380 lsp: true,
11381 lsp_fetch_timeout_ms: 10,
11382 lsp_insert_mode: LspInsertMode::Insert,
11383 });
11384 });
11385
11386 let mut cx = EditorLspTestContext::new_rust(
11387 lsp::ServerCapabilities {
11388 completion_provider: Some(lsp::CompletionOptions {
11389 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11390 ..lsp::CompletionOptions::default()
11391 }),
11392 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11393 ..lsp::ServerCapabilities::default()
11394 },
11395 cx,
11396 )
11397 .await;
11398
11399 let throttle_completions = Arc::new(AtomicBool::new(false));
11400
11401 let lsp_throttle_completions = throttle_completions.clone();
11402 let _completion_requests_handler =
11403 cx.lsp
11404 .server
11405 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
11406 let lsp_throttle_completions = lsp_throttle_completions.clone();
11407 let cx = cx.clone();
11408 async move {
11409 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
11410 cx.background_executor()
11411 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
11412 .await;
11413 }
11414 Ok(Some(lsp::CompletionResponse::Array(vec![
11415 lsp::CompletionItem {
11416 label: "first".into(),
11417 ..lsp::CompletionItem::default()
11418 },
11419 lsp::CompletionItem {
11420 label: "last".into(),
11421 ..lsp::CompletionItem::default()
11422 },
11423 ])))
11424 }
11425 });
11426
11427 cx.set_state(indoc! {"
11428 oneˇ
11429 two
11430 three
11431 "});
11432 cx.simulate_keystroke(".");
11433 cx.executor().run_until_parked();
11434 cx.condition(|editor, _| editor.context_menu_visible())
11435 .await;
11436 cx.update_editor(|editor, window, cx| {
11437 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11438 {
11439 assert_eq!(
11440 completion_menu_entries(&menu),
11441 &["first", "last"],
11442 "When LSP server is fast to reply, no fallback word completions are used"
11443 );
11444 } else {
11445 panic!("expected completion menu to be open");
11446 }
11447 editor.cancel(&Cancel, window, cx);
11448 });
11449 cx.executor().run_until_parked();
11450 cx.condition(|editor, _| !editor.context_menu_visible())
11451 .await;
11452
11453 throttle_completions.store(true, atomic::Ordering::Release);
11454 cx.simulate_keystroke(".");
11455 cx.executor()
11456 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11457 cx.executor().run_until_parked();
11458 cx.condition(|editor, _| editor.context_menu_visible())
11459 .await;
11460 cx.update_editor(|editor, _, _| {
11461 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11462 {
11463 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11464 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11465 } else {
11466 panic!("expected completion menu to be open");
11467 }
11468 });
11469}
11470
11471#[gpui::test]
11472async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11473 init_test(cx, |language_settings| {
11474 language_settings.defaults.completions = Some(CompletionSettings {
11475 words: WordsCompletionMode::Enabled,
11476 lsp: true,
11477 lsp_fetch_timeout_ms: 0,
11478 lsp_insert_mode: LspInsertMode::Insert,
11479 });
11480 });
11481
11482 let mut cx = EditorLspTestContext::new_rust(
11483 lsp::ServerCapabilities {
11484 completion_provider: Some(lsp::CompletionOptions {
11485 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11486 ..lsp::CompletionOptions::default()
11487 }),
11488 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11489 ..lsp::ServerCapabilities::default()
11490 },
11491 cx,
11492 )
11493 .await;
11494
11495 let _completion_requests_handler =
11496 cx.lsp
11497 .server
11498 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11499 Ok(Some(lsp::CompletionResponse::Array(vec![
11500 lsp::CompletionItem {
11501 label: "first".into(),
11502 ..lsp::CompletionItem::default()
11503 },
11504 lsp::CompletionItem {
11505 label: "last".into(),
11506 ..lsp::CompletionItem::default()
11507 },
11508 ])))
11509 });
11510
11511 cx.set_state(indoc! {"ˇ
11512 first
11513 last
11514 second
11515 "});
11516 cx.simulate_keystroke(".");
11517 cx.executor().run_until_parked();
11518 cx.condition(|editor, _| editor.context_menu_visible())
11519 .await;
11520 cx.update_editor(|editor, _, _| {
11521 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11522 {
11523 assert_eq!(
11524 completion_menu_entries(&menu),
11525 &["first", "last", "second"],
11526 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11527 );
11528 } else {
11529 panic!("expected completion menu to be open");
11530 }
11531 });
11532}
11533
11534#[gpui::test]
11535async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11536 init_test(cx, |language_settings| {
11537 language_settings.defaults.completions = Some(CompletionSettings {
11538 words: WordsCompletionMode::Disabled,
11539 lsp: true,
11540 lsp_fetch_timeout_ms: 0,
11541 lsp_insert_mode: LspInsertMode::Insert,
11542 });
11543 });
11544
11545 let mut cx = EditorLspTestContext::new_rust(
11546 lsp::ServerCapabilities {
11547 completion_provider: Some(lsp::CompletionOptions {
11548 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11549 ..lsp::CompletionOptions::default()
11550 }),
11551 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11552 ..lsp::ServerCapabilities::default()
11553 },
11554 cx,
11555 )
11556 .await;
11557
11558 let _completion_requests_handler =
11559 cx.lsp
11560 .server
11561 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11562 panic!("LSP completions should not be queried when dealing with word completions")
11563 });
11564
11565 cx.set_state(indoc! {"ˇ
11566 first
11567 last
11568 second
11569 "});
11570 cx.update_editor(|editor, window, cx| {
11571 editor.show_word_completions(&ShowWordCompletions, window, cx);
11572 });
11573 cx.executor().run_until_parked();
11574 cx.condition(|editor, _| editor.context_menu_visible())
11575 .await;
11576 cx.update_editor(|editor, _, _| {
11577 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11578 {
11579 assert_eq!(
11580 completion_menu_entries(&menu),
11581 &["first", "last", "second"],
11582 "`ShowWordCompletions` action should show word completions"
11583 );
11584 } else {
11585 panic!("expected completion menu to be open");
11586 }
11587 });
11588
11589 cx.simulate_keystroke("l");
11590 cx.executor().run_until_parked();
11591 cx.condition(|editor, _| editor.context_menu_visible())
11592 .await;
11593 cx.update_editor(|editor, _, _| {
11594 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11595 {
11596 assert_eq!(
11597 completion_menu_entries(&menu),
11598 &["last"],
11599 "After showing word completions, further editing should filter them and not query the LSP"
11600 );
11601 } else {
11602 panic!("expected completion menu to be open");
11603 }
11604 });
11605}
11606
11607#[gpui::test]
11608async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11609 init_test(cx, |language_settings| {
11610 language_settings.defaults.completions = Some(CompletionSettings {
11611 words: WordsCompletionMode::Fallback,
11612 lsp: false,
11613 lsp_fetch_timeout_ms: 0,
11614 lsp_insert_mode: LspInsertMode::Insert,
11615 });
11616 });
11617
11618 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11619
11620 cx.set_state(indoc! {"ˇ
11621 0_usize
11622 let
11623 33
11624 4.5f32
11625 "});
11626 cx.update_editor(|editor, window, cx| {
11627 editor.show_completions(&ShowCompletions::default(), window, cx);
11628 });
11629 cx.executor().run_until_parked();
11630 cx.condition(|editor, _| editor.context_menu_visible())
11631 .await;
11632 cx.update_editor(|editor, window, cx| {
11633 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11634 {
11635 assert_eq!(
11636 completion_menu_entries(&menu),
11637 &["let"],
11638 "With no digits in the completion query, no digits should be in the word completions"
11639 );
11640 } else {
11641 panic!("expected completion menu to be open");
11642 }
11643 editor.cancel(&Cancel, window, cx);
11644 });
11645
11646 cx.set_state(indoc! {"3ˇ
11647 0_usize
11648 let
11649 3
11650 33.35f32
11651 "});
11652 cx.update_editor(|editor, window, cx| {
11653 editor.show_completions(&ShowCompletions::default(), window, cx);
11654 });
11655 cx.executor().run_until_parked();
11656 cx.condition(|editor, _| editor.context_menu_visible())
11657 .await;
11658 cx.update_editor(|editor, _, _| {
11659 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11660 {
11661 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11662 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11663 } else {
11664 panic!("expected completion menu to be open");
11665 }
11666 });
11667}
11668
11669fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11670 let position = || lsp::Position {
11671 line: params.text_document_position.position.line,
11672 character: params.text_document_position.position.character,
11673 };
11674 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11675 range: lsp::Range {
11676 start: position(),
11677 end: position(),
11678 },
11679 new_text: text.to_string(),
11680 }))
11681}
11682
11683#[gpui::test]
11684async fn test_multiline_completion(cx: &mut TestAppContext) {
11685 init_test(cx, |_| {});
11686
11687 let fs = FakeFs::new(cx.executor());
11688 fs.insert_tree(
11689 path!("/a"),
11690 json!({
11691 "main.ts": "a",
11692 }),
11693 )
11694 .await;
11695
11696 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11697 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11698 let typescript_language = Arc::new(Language::new(
11699 LanguageConfig {
11700 name: "TypeScript".into(),
11701 matcher: LanguageMatcher {
11702 path_suffixes: vec!["ts".to_string()],
11703 ..LanguageMatcher::default()
11704 },
11705 line_comments: vec!["// ".into()],
11706 ..LanguageConfig::default()
11707 },
11708 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11709 ));
11710 language_registry.add(typescript_language.clone());
11711 let mut fake_servers = language_registry.register_fake_lsp(
11712 "TypeScript",
11713 FakeLspAdapter {
11714 capabilities: lsp::ServerCapabilities {
11715 completion_provider: Some(lsp::CompletionOptions {
11716 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11717 ..lsp::CompletionOptions::default()
11718 }),
11719 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11720 ..lsp::ServerCapabilities::default()
11721 },
11722 // Emulate vtsls label generation
11723 label_for_completion: Some(Box::new(|item, _| {
11724 let text = if let Some(description) = item
11725 .label_details
11726 .as_ref()
11727 .and_then(|label_details| label_details.description.as_ref())
11728 {
11729 format!("{} {}", item.label, description)
11730 } else if let Some(detail) = &item.detail {
11731 format!("{} {}", item.label, detail)
11732 } else {
11733 item.label.clone()
11734 };
11735 let len = text.len();
11736 Some(language::CodeLabel {
11737 text,
11738 runs: Vec::new(),
11739 filter_range: 0..len,
11740 })
11741 })),
11742 ..FakeLspAdapter::default()
11743 },
11744 );
11745 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11746 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11747 let worktree_id = workspace
11748 .update(cx, |workspace, _window, cx| {
11749 workspace.project().update(cx, |project, cx| {
11750 project.worktrees(cx).next().unwrap().read(cx).id()
11751 })
11752 })
11753 .unwrap();
11754 let _buffer = project
11755 .update(cx, |project, cx| {
11756 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11757 })
11758 .await
11759 .unwrap();
11760 let editor = workspace
11761 .update(cx, |workspace, window, cx| {
11762 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11763 })
11764 .unwrap()
11765 .await
11766 .unwrap()
11767 .downcast::<Editor>()
11768 .unwrap();
11769 let fake_server = fake_servers.next().await.unwrap();
11770
11771 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11772 let multiline_label_2 = "a\nb\nc\n";
11773 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11774 let multiline_description = "d\ne\nf\n";
11775 let multiline_detail_2 = "g\nh\ni\n";
11776
11777 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11778 move |params, _| async move {
11779 Ok(Some(lsp::CompletionResponse::Array(vec![
11780 lsp::CompletionItem {
11781 label: multiline_label.to_string(),
11782 text_edit: gen_text_edit(¶ms, "new_text_1"),
11783 ..lsp::CompletionItem::default()
11784 },
11785 lsp::CompletionItem {
11786 label: "single line label 1".to_string(),
11787 detail: Some(multiline_detail.to_string()),
11788 text_edit: gen_text_edit(¶ms, "new_text_2"),
11789 ..lsp::CompletionItem::default()
11790 },
11791 lsp::CompletionItem {
11792 label: "single line label 2".to_string(),
11793 label_details: Some(lsp::CompletionItemLabelDetails {
11794 description: Some(multiline_description.to_string()),
11795 detail: None,
11796 }),
11797 text_edit: gen_text_edit(¶ms, "new_text_2"),
11798 ..lsp::CompletionItem::default()
11799 },
11800 lsp::CompletionItem {
11801 label: multiline_label_2.to_string(),
11802 detail: Some(multiline_detail_2.to_string()),
11803 text_edit: gen_text_edit(¶ms, "new_text_3"),
11804 ..lsp::CompletionItem::default()
11805 },
11806 lsp::CompletionItem {
11807 label: "Label with many spaces and \t but without newlines".to_string(),
11808 detail: Some(
11809 "Details with many spaces and \t but without newlines".to_string(),
11810 ),
11811 text_edit: gen_text_edit(¶ms, "new_text_4"),
11812 ..lsp::CompletionItem::default()
11813 },
11814 ])))
11815 },
11816 );
11817
11818 editor.update_in(cx, |editor, window, cx| {
11819 cx.focus_self(window);
11820 editor.move_to_end(&MoveToEnd, window, cx);
11821 editor.handle_input(".", window, cx);
11822 });
11823 cx.run_until_parked();
11824 completion_handle.next().await.unwrap();
11825
11826 editor.update(cx, |editor, _| {
11827 assert!(editor.context_menu_visible());
11828 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11829 {
11830 let completion_labels = menu
11831 .completions
11832 .borrow()
11833 .iter()
11834 .map(|c| c.label.text.clone())
11835 .collect::<Vec<_>>();
11836 assert_eq!(
11837 completion_labels,
11838 &[
11839 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11840 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11841 "single line label 2 d e f ",
11842 "a b c g h i ",
11843 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11844 ],
11845 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11846 );
11847
11848 for completion in menu
11849 .completions
11850 .borrow()
11851 .iter() {
11852 assert_eq!(
11853 completion.label.filter_range,
11854 0..completion.label.text.len(),
11855 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11856 );
11857 }
11858 } else {
11859 panic!("expected completion menu to be open");
11860 }
11861 });
11862}
11863
11864#[gpui::test]
11865async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11866 init_test(cx, |_| {});
11867 let mut cx = EditorLspTestContext::new_rust(
11868 lsp::ServerCapabilities {
11869 completion_provider: Some(lsp::CompletionOptions {
11870 trigger_characters: Some(vec![".".to_string()]),
11871 ..Default::default()
11872 }),
11873 ..Default::default()
11874 },
11875 cx,
11876 )
11877 .await;
11878 cx.lsp
11879 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11880 Ok(Some(lsp::CompletionResponse::Array(vec![
11881 lsp::CompletionItem {
11882 label: "first".into(),
11883 ..Default::default()
11884 },
11885 lsp::CompletionItem {
11886 label: "last".into(),
11887 ..Default::default()
11888 },
11889 ])))
11890 });
11891 cx.set_state("variableˇ");
11892 cx.simulate_keystroke(".");
11893 cx.executor().run_until_parked();
11894
11895 cx.update_editor(|editor, _, _| {
11896 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11897 {
11898 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11899 } else {
11900 panic!("expected completion menu to be open");
11901 }
11902 });
11903
11904 cx.update_editor(|editor, window, cx| {
11905 editor.move_page_down(&MovePageDown::default(), window, cx);
11906 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11907 {
11908 assert!(
11909 menu.selected_item == 1,
11910 "expected PageDown to select the last item from the context menu"
11911 );
11912 } else {
11913 panic!("expected completion menu to stay open after PageDown");
11914 }
11915 });
11916
11917 cx.update_editor(|editor, window, cx| {
11918 editor.move_page_up(&MovePageUp::default(), window, cx);
11919 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11920 {
11921 assert!(
11922 menu.selected_item == 0,
11923 "expected PageUp to select the first item from the context menu"
11924 );
11925 } else {
11926 panic!("expected completion menu to stay open after PageUp");
11927 }
11928 });
11929}
11930
11931#[gpui::test]
11932async fn test_as_is_completions(cx: &mut TestAppContext) {
11933 init_test(cx, |_| {});
11934 let mut cx = EditorLspTestContext::new_rust(
11935 lsp::ServerCapabilities {
11936 completion_provider: Some(lsp::CompletionOptions {
11937 ..Default::default()
11938 }),
11939 ..Default::default()
11940 },
11941 cx,
11942 )
11943 .await;
11944 cx.lsp
11945 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11946 Ok(Some(lsp::CompletionResponse::Array(vec![
11947 lsp::CompletionItem {
11948 label: "unsafe".into(),
11949 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11950 range: lsp::Range {
11951 start: lsp::Position {
11952 line: 1,
11953 character: 2,
11954 },
11955 end: lsp::Position {
11956 line: 1,
11957 character: 3,
11958 },
11959 },
11960 new_text: "unsafe".to_string(),
11961 })),
11962 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11963 ..Default::default()
11964 },
11965 ])))
11966 });
11967 cx.set_state("fn a() {}\n nˇ");
11968 cx.executor().run_until_parked();
11969 cx.update_editor(|editor, window, cx| {
11970 editor.show_completions(
11971 &ShowCompletions {
11972 trigger: Some("\n".into()),
11973 },
11974 window,
11975 cx,
11976 );
11977 });
11978 cx.executor().run_until_parked();
11979
11980 cx.update_editor(|editor, window, cx| {
11981 editor.confirm_completion(&Default::default(), window, cx)
11982 });
11983 cx.executor().run_until_parked();
11984 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11985}
11986
11987#[gpui::test]
11988async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11989 init_test(cx, |_| {});
11990
11991 let mut cx = EditorLspTestContext::new_rust(
11992 lsp::ServerCapabilities {
11993 completion_provider: Some(lsp::CompletionOptions {
11994 trigger_characters: Some(vec![".".to_string()]),
11995 resolve_provider: Some(true),
11996 ..Default::default()
11997 }),
11998 ..Default::default()
11999 },
12000 cx,
12001 )
12002 .await;
12003
12004 cx.set_state("fn main() { let a = 2ˇ; }");
12005 cx.simulate_keystroke(".");
12006 let completion_item = lsp::CompletionItem {
12007 label: "Some".into(),
12008 kind: Some(lsp::CompletionItemKind::SNIPPET),
12009 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
12010 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
12011 kind: lsp::MarkupKind::Markdown,
12012 value: "```rust\nSome(2)\n```".to_string(),
12013 })),
12014 deprecated: Some(false),
12015 sort_text: Some("Some".to_string()),
12016 filter_text: Some("Some".to_string()),
12017 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
12018 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
12019 range: lsp::Range {
12020 start: lsp::Position {
12021 line: 0,
12022 character: 22,
12023 },
12024 end: lsp::Position {
12025 line: 0,
12026 character: 22,
12027 },
12028 },
12029 new_text: "Some(2)".to_string(),
12030 })),
12031 additional_text_edits: Some(vec![lsp::TextEdit {
12032 range: lsp::Range {
12033 start: lsp::Position {
12034 line: 0,
12035 character: 20,
12036 },
12037 end: lsp::Position {
12038 line: 0,
12039 character: 22,
12040 },
12041 },
12042 new_text: "".to_string(),
12043 }]),
12044 ..Default::default()
12045 };
12046
12047 let closure_completion_item = completion_item.clone();
12048 let counter = Arc::new(AtomicUsize::new(0));
12049 let counter_clone = counter.clone();
12050 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
12051 let task_completion_item = closure_completion_item.clone();
12052 counter_clone.fetch_add(1, atomic::Ordering::Release);
12053 async move {
12054 Ok(Some(lsp::CompletionResponse::Array(vec![
12055 task_completion_item,
12056 ])))
12057 }
12058 });
12059
12060 cx.condition(|editor, _| editor.context_menu_visible())
12061 .await;
12062 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
12063 assert!(request.next().await.is_some());
12064 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
12065
12066 cx.simulate_keystrokes("S o m");
12067 cx.condition(|editor, _| editor.context_menu_visible())
12068 .await;
12069 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
12070 assert!(request.next().await.is_some());
12071 assert!(request.next().await.is_some());
12072 assert!(request.next().await.is_some());
12073 request.close();
12074 assert!(request.next().await.is_none());
12075 assert_eq!(
12076 counter.load(atomic::Ordering::Acquire),
12077 4,
12078 "With the completions menu open, only one LSP request should happen per input"
12079 );
12080}
12081
12082#[gpui::test]
12083async fn test_toggle_comment(cx: &mut TestAppContext) {
12084 init_test(cx, |_| {});
12085 let mut cx = EditorTestContext::new(cx).await;
12086 let language = Arc::new(Language::new(
12087 LanguageConfig {
12088 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12089 ..Default::default()
12090 },
12091 Some(tree_sitter_rust::LANGUAGE.into()),
12092 ));
12093 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12094
12095 // If multiple selections intersect a line, the line is only toggled once.
12096 cx.set_state(indoc! {"
12097 fn a() {
12098 «//b();
12099 ˇ»// «c();
12100 //ˇ» d();
12101 }
12102 "});
12103
12104 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12105
12106 cx.assert_editor_state(indoc! {"
12107 fn a() {
12108 «b();
12109 c();
12110 ˇ» d();
12111 }
12112 "});
12113
12114 // The comment prefix is inserted at the same column for every line in a
12115 // selection.
12116 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12117
12118 cx.assert_editor_state(indoc! {"
12119 fn a() {
12120 // «b();
12121 // c();
12122 ˇ»// d();
12123 }
12124 "});
12125
12126 // If a selection ends at the beginning of a line, that line is not toggled.
12127 cx.set_selections_state(indoc! {"
12128 fn a() {
12129 // b();
12130 «// c();
12131 ˇ» // d();
12132 }
12133 "});
12134
12135 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12136
12137 cx.assert_editor_state(indoc! {"
12138 fn a() {
12139 // b();
12140 «c();
12141 ˇ» // d();
12142 }
12143 "});
12144
12145 // If a selection span a single line and is empty, the line is toggled.
12146 cx.set_state(indoc! {"
12147 fn a() {
12148 a();
12149 b();
12150 ˇ
12151 }
12152 "});
12153
12154 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12155
12156 cx.assert_editor_state(indoc! {"
12157 fn a() {
12158 a();
12159 b();
12160 //•ˇ
12161 }
12162 "});
12163
12164 // If a selection span multiple lines, empty lines are not toggled.
12165 cx.set_state(indoc! {"
12166 fn a() {
12167 «a();
12168
12169 c();ˇ»
12170 }
12171 "});
12172
12173 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12174
12175 cx.assert_editor_state(indoc! {"
12176 fn a() {
12177 // «a();
12178
12179 // c();ˇ»
12180 }
12181 "});
12182
12183 // If a selection includes multiple comment prefixes, all lines are uncommented.
12184 cx.set_state(indoc! {"
12185 fn a() {
12186 «// a();
12187 /// b();
12188 //! c();ˇ»
12189 }
12190 "});
12191
12192 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
12193
12194 cx.assert_editor_state(indoc! {"
12195 fn a() {
12196 «a();
12197 b();
12198 c();ˇ»
12199 }
12200 "});
12201}
12202
12203#[gpui::test]
12204async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
12205 init_test(cx, |_| {});
12206 let mut cx = EditorTestContext::new(cx).await;
12207 let language = Arc::new(Language::new(
12208 LanguageConfig {
12209 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
12210 ..Default::default()
12211 },
12212 Some(tree_sitter_rust::LANGUAGE.into()),
12213 ));
12214 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
12215
12216 let toggle_comments = &ToggleComments {
12217 advance_downwards: false,
12218 ignore_indent: true,
12219 };
12220
12221 // If multiple selections intersect a line, the line is only toggled once.
12222 cx.set_state(indoc! {"
12223 fn a() {
12224 // «b();
12225 // c();
12226 // ˇ» d();
12227 }
12228 "});
12229
12230 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12231
12232 cx.assert_editor_state(indoc! {"
12233 fn a() {
12234 «b();
12235 c();
12236 ˇ» d();
12237 }
12238 "});
12239
12240 // The comment prefix is inserted at the beginning of each line
12241 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12242
12243 cx.assert_editor_state(indoc! {"
12244 fn a() {
12245 // «b();
12246 // c();
12247 // ˇ» d();
12248 }
12249 "});
12250
12251 // If a selection ends at the beginning of a line, that line is not toggled.
12252 cx.set_selections_state(indoc! {"
12253 fn a() {
12254 // b();
12255 // «c();
12256 ˇ»// d();
12257 }
12258 "});
12259
12260 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12261
12262 cx.assert_editor_state(indoc! {"
12263 fn a() {
12264 // b();
12265 «c();
12266 ˇ»// d();
12267 }
12268 "});
12269
12270 // If a selection span a single line and is empty, the line is toggled.
12271 cx.set_state(indoc! {"
12272 fn a() {
12273 a();
12274 b();
12275 ˇ
12276 }
12277 "});
12278
12279 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12280
12281 cx.assert_editor_state(indoc! {"
12282 fn a() {
12283 a();
12284 b();
12285 //ˇ
12286 }
12287 "});
12288
12289 // If a selection span multiple lines, empty lines are not toggled.
12290 cx.set_state(indoc! {"
12291 fn a() {
12292 «a();
12293
12294 c();ˇ»
12295 }
12296 "});
12297
12298 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12299
12300 cx.assert_editor_state(indoc! {"
12301 fn a() {
12302 // «a();
12303
12304 // c();ˇ»
12305 }
12306 "});
12307
12308 // If a selection includes multiple comment prefixes, all lines are uncommented.
12309 cx.set_state(indoc! {"
12310 fn a() {
12311 // «a();
12312 /// b();
12313 //! c();ˇ»
12314 }
12315 "});
12316
12317 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
12318
12319 cx.assert_editor_state(indoc! {"
12320 fn a() {
12321 «a();
12322 b();
12323 c();ˇ»
12324 }
12325 "});
12326}
12327
12328#[gpui::test]
12329async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
12330 init_test(cx, |_| {});
12331
12332 let language = Arc::new(Language::new(
12333 LanguageConfig {
12334 line_comments: vec!["// ".into()],
12335 ..Default::default()
12336 },
12337 Some(tree_sitter_rust::LANGUAGE.into()),
12338 ));
12339
12340 let mut cx = EditorTestContext::new(cx).await;
12341
12342 cx.language_registry().add(language.clone());
12343 cx.update_buffer(|buffer, cx| {
12344 buffer.set_language(Some(language), cx);
12345 });
12346
12347 let toggle_comments = &ToggleComments {
12348 advance_downwards: true,
12349 ignore_indent: false,
12350 };
12351
12352 // Single cursor on one line -> advance
12353 // Cursor moves horizontally 3 characters as well on non-blank line
12354 cx.set_state(indoc!(
12355 "fn a() {
12356 ˇdog();
12357 cat();
12358 }"
12359 ));
12360 cx.update_editor(|editor, window, cx| {
12361 editor.toggle_comments(toggle_comments, window, cx);
12362 });
12363 cx.assert_editor_state(indoc!(
12364 "fn a() {
12365 // dog();
12366 catˇ();
12367 }"
12368 ));
12369
12370 // Single selection on one line -> don't advance
12371 cx.set_state(indoc!(
12372 "fn a() {
12373 «dog()ˇ»;
12374 cat();
12375 }"
12376 ));
12377 cx.update_editor(|editor, window, cx| {
12378 editor.toggle_comments(toggle_comments, window, cx);
12379 });
12380 cx.assert_editor_state(indoc!(
12381 "fn a() {
12382 // «dog()ˇ»;
12383 cat();
12384 }"
12385 ));
12386
12387 // Multiple cursors on one line -> advance
12388 cx.set_state(indoc!(
12389 "fn a() {
12390 ˇdˇog();
12391 cat();
12392 }"
12393 ));
12394 cx.update_editor(|editor, window, cx| {
12395 editor.toggle_comments(toggle_comments, window, cx);
12396 });
12397 cx.assert_editor_state(indoc!(
12398 "fn a() {
12399 // dog();
12400 catˇ(ˇ);
12401 }"
12402 ));
12403
12404 // Multiple cursors on one line, with selection -> don't advance
12405 cx.set_state(indoc!(
12406 "fn a() {
12407 ˇdˇog«()ˇ»;
12408 cat();
12409 }"
12410 ));
12411 cx.update_editor(|editor, window, cx| {
12412 editor.toggle_comments(toggle_comments, window, cx);
12413 });
12414 cx.assert_editor_state(indoc!(
12415 "fn a() {
12416 // ˇdˇog«()ˇ»;
12417 cat();
12418 }"
12419 ));
12420
12421 // Single cursor on one line -> advance
12422 // Cursor moves to column 0 on blank line
12423 cx.set_state(indoc!(
12424 "fn a() {
12425 ˇdog();
12426
12427 cat();
12428 }"
12429 ));
12430 cx.update_editor(|editor, window, cx| {
12431 editor.toggle_comments(toggle_comments, window, cx);
12432 });
12433 cx.assert_editor_state(indoc!(
12434 "fn a() {
12435 // dog();
12436 ˇ
12437 cat();
12438 }"
12439 ));
12440
12441 // Single cursor on one line -> advance
12442 // Cursor starts and ends at column 0
12443 cx.set_state(indoc!(
12444 "fn a() {
12445 ˇ dog();
12446 cat();
12447 }"
12448 ));
12449 cx.update_editor(|editor, window, cx| {
12450 editor.toggle_comments(toggle_comments, window, cx);
12451 });
12452 cx.assert_editor_state(indoc!(
12453 "fn a() {
12454 // dog();
12455 ˇ cat();
12456 }"
12457 ));
12458}
12459
12460#[gpui::test]
12461async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12462 init_test(cx, |_| {});
12463
12464 let mut cx = EditorTestContext::new(cx).await;
12465
12466 let html_language = Arc::new(
12467 Language::new(
12468 LanguageConfig {
12469 name: "HTML".into(),
12470 block_comment: Some(("<!-- ".into(), " -->".into())),
12471 ..Default::default()
12472 },
12473 Some(tree_sitter_html::LANGUAGE.into()),
12474 )
12475 .with_injection_query(
12476 r#"
12477 (script_element
12478 (raw_text) @injection.content
12479 (#set! injection.language "javascript"))
12480 "#,
12481 )
12482 .unwrap(),
12483 );
12484
12485 let javascript_language = Arc::new(Language::new(
12486 LanguageConfig {
12487 name: "JavaScript".into(),
12488 line_comments: vec!["// ".into()],
12489 ..Default::default()
12490 },
12491 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12492 ));
12493
12494 cx.language_registry().add(html_language.clone());
12495 cx.language_registry().add(javascript_language.clone());
12496 cx.update_buffer(|buffer, cx| {
12497 buffer.set_language(Some(html_language), cx);
12498 });
12499
12500 // Toggle comments for empty selections
12501 cx.set_state(
12502 &r#"
12503 <p>A</p>ˇ
12504 <p>B</p>ˇ
12505 <p>C</p>ˇ
12506 "#
12507 .unindent(),
12508 );
12509 cx.update_editor(|editor, window, cx| {
12510 editor.toggle_comments(&ToggleComments::default(), window, cx)
12511 });
12512 cx.assert_editor_state(
12513 &r#"
12514 <!-- <p>A</p>ˇ -->
12515 <!-- <p>B</p>ˇ -->
12516 <!-- <p>C</p>ˇ -->
12517 "#
12518 .unindent(),
12519 );
12520 cx.update_editor(|editor, window, cx| {
12521 editor.toggle_comments(&ToggleComments::default(), window, cx)
12522 });
12523 cx.assert_editor_state(
12524 &r#"
12525 <p>A</p>ˇ
12526 <p>B</p>ˇ
12527 <p>C</p>ˇ
12528 "#
12529 .unindent(),
12530 );
12531
12532 // Toggle comments for mixture of empty and non-empty selections, where
12533 // multiple selections occupy a given line.
12534 cx.set_state(
12535 &r#"
12536 <p>A«</p>
12537 <p>ˇ»B</p>ˇ
12538 <p>C«</p>
12539 <p>ˇ»D</p>ˇ
12540 "#
12541 .unindent(),
12542 );
12543
12544 cx.update_editor(|editor, window, cx| {
12545 editor.toggle_comments(&ToggleComments::default(), window, cx)
12546 });
12547 cx.assert_editor_state(
12548 &r#"
12549 <!-- <p>A«</p>
12550 <p>ˇ»B</p>ˇ -->
12551 <!-- <p>C«</p>
12552 <p>ˇ»D</p>ˇ -->
12553 "#
12554 .unindent(),
12555 );
12556 cx.update_editor(|editor, window, cx| {
12557 editor.toggle_comments(&ToggleComments::default(), window, cx)
12558 });
12559 cx.assert_editor_state(
12560 &r#"
12561 <p>A«</p>
12562 <p>ˇ»B</p>ˇ
12563 <p>C«</p>
12564 <p>ˇ»D</p>ˇ
12565 "#
12566 .unindent(),
12567 );
12568
12569 // Toggle comments when different languages are active for different
12570 // selections.
12571 cx.set_state(
12572 &r#"
12573 ˇ<script>
12574 ˇvar x = new Y();
12575 ˇ</script>
12576 "#
12577 .unindent(),
12578 );
12579 cx.executor().run_until_parked();
12580 cx.update_editor(|editor, window, cx| {
12581 editor.toggle_comments(&ToggleComments::default(), window, cx)
12582 });
12583 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12584 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12585 cx.assert_editor_state(
12586 &r#"
12587 <!-- ˇ<script> -->
12588 // ˇvar x = new Y();
12589 <!-- ˇ</script> -->
12590 "#
12591 .unindent(),
12592 );
12593}
12594
12595#[gpui::test]
12596fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12597 init_test(cx, |_| {});
12598
12599 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12600 let multibuffer = cx.new(|cx| {
12601 let mut multibuffer = MultiBuffer::new(ReadWrite);
12602 multibuffer.push_excerpts(
12603 buffer.clone(),
12604 [
12605 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12606 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12607 ],
12608 cx,
12609 );
12610 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12611 multibuffer
12612 });
12613
12614 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12615 editor.update_in(cx, |editor, window, cx| {
12616 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12617 editor.change_selections(None, window, cx, |s| {
12618 s.select_ranges([
12619 Point::new(0, 0)..Point::new(0, 0),
12620 Point::new(1, 0)..Point::new(1, 0),
12621 ])
12622 });
12623
12624 editor.handle_input("X", window, cx);
12625 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12626 assert_eq!(
12627 editor.selections.ranges(cx),
12628 [
12629 Point::new(0, 1)..Point::new(0, 1),
12630 Point::new(1, 1)..Point::new(1, 1),
12631 ]
12632 );
12633
12634 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12635 editor.change_selections(None, window, cx, |s| {
12636 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12637 });
12638 editor.backspace(&Default::default(), window, cx);
12639 assert_eq!(editor.text(cx), "Xa\nbbb");
12640 assert_eq!(
12641 editor.selections.ranges(cx),
12642 [Point::new(1, 0)..Point::new(1, 0)]
12643 );
12644
12645 editor.change_selections(None, window, cx, |s| {
12646 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12647 });
12648 editor.backspace(&Default::default(), window, cx);
12649 assert_eq!(editor.text(cx), "X\nbb");
12650 assert_eq!(
12651 editor.selections.ranges(cx),
12652 [Point::new(0, 1)..Point::new(0, 1)]
12653 );
12654 });
12655}
12656
12657#[gpui::test]
12658fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12659 init_test(cx, |_| {});
12660
12661 let markers = vec![('[', ']').into(), ('(', ')').into()];
12662 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12663 indoc! {"
12664 [aaaa
12665 (bbbb]
12666 cccc)",
12667 },
12668 markers.clone(),
12669 );
12670 let excerpt_ranges = markers.into_iter().map(|marker| {
12671 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12672 ExcerptRange::new(context.clone())
12673 });
12674 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12675 let multibuffer = cx.new(|cx| {
12676 let mut multibuffer = MultiBuffer::new(ReadWrite);
12677 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12678 multibuffer
12679 });
12680
12681 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12682 editor.update_in(cx, |editor, window, cx| {
12683 let (expected_text, selection_ranges) = marked_text_ranges(
12684 indoc! {"
12685 aaaa
12686 bˇbbb
12687 bˇbbˇb
12688 cccc"
12689 },
12690 true,
12691 );
12692 assert_eq!(editor.text(cx), expected_text);
12693 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12694
12695 editor.handle_input("X", window, cx);
12696
12697 let (expected_text, expected_selections) = marked_text_ranges(
12698 indoc! {"
12699 aaaa
12700 bXˇbbXb
12701 bXˇbbXˇb
12702 cccc"
12703 },
12704 false,
12705 );
12706 assert_eq!(editor.text(cx), expected_text);
12707 assert_eq!(editor.selections.ranges(cx), expected_selections);
12708
12709 editor.newline(&Newline, window, cx);
12710 let (expected_text, expected_selections) = marked_text_ranges(
12711 indoc! {"
12712 aaaa
12713 bX
12714 ˇbbX
12715 b
12716 bX
12717 ˇbbX
12718 ˇb
12719 cccc"
12720 },
12721 false,
12722 );
12723 assert_eq!(editor.text(cx), expected_text);
12724 assert_eq!(editor.selections.ranges(cx), expected_selections);
12725 });
12726}
12727
12728#[gpui::test]
12729fn test_refresh_selections(cx: &mut TestAppContext) {
12730 init_test(cx, |_| {});
12731
12732 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12733 let mut excerpt1_id = None;
12734 let multibuffer = cx.new(|cx| {
12735 let mut multibuffer = MultiBuffer::new(ReadWrite);
12736 excerpt1_id = multibuffer
12737 .push_excerpts(
12738 buffer.clone(),
12739 [
12740 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12741 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12742 ],
12743 cx,
12744 )
12745 .into_iter()
12746 .next();
12747 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12748 multibuffer
12749 });
12750
12751 let editor = cx.add_window(|window, cx| {
12752 let mut editor = build_editor(multibuffer.clone(), window, cx);
12753 let snapshot = editor.snapshot(window, cx);
12754 editor.change_selections(None, window, cx, |s| {
12755 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12756 });
12757 editor.begin_selection(
12758 Point::new(2, 1).to_display_point(&snapshot),
12759 true,
12760 1,
12761 window,
12762 cx,
12763 );
12764 assert_eq!(
12765 editor.selections.ranges(cx),
12766 [
12767 Point::new(1, 3)..Point::new(1, 3),
12768 Point::new(2, 1)..Point::new(2, 1),
12769 ]
12770 );
12771 editor
12772 });
12773
12774 // Refreshing selections is a no-op when excerpts haven't changed.
12775 _ = editor.update(cx, |editor, window, cx| {
12776 editor.change_selections(None, window, cx, |s| s.refresh());
12777 assert_eq!(
12778 editor.selections.ranges(cx),
12779 [
12780 Point::new(1, 3)..Point::new(1, 3),
12781 Point::new(2, 1)..Point::new(2, 1),
12782 ]
12783 );
12784 });
12785
12786 multibuffer.update(cx, |multibuffer, cx| {
12787 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12788 });
12789 _ = editor.update(cx, |editor, window, cx| {
12790 // Removing an excerpt causes the first selection to become degenerate.
12791 assert_eq!(
12792 editor.selections.ranges(cx),
12793 [
12794 Point::new(0, 0)..Point::new(0, 0),
12795 Point::new(0, 1)..Point::new(0, 1)
12796 ]
12797 );
12798
12799 // Refreshing selections will relocate the first selection to the original buffer
12800 // location.
12801 editor.change_selections(None, window, cx, |s| s.refresh());
12802 assert_eq!(
12803 editor.selections.ranges(cx),
12804 [
12805 Point::new(0, 1)..Point::new(0, 1),
12806 Point::new(0, 3)..Point::new(0, 3)
12807 ]
12808 );
12809 assert!(editor.selections.pending_anchor().is_some());
12810 });
12811}
12812
12813#[gpui::test]
12814fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12815 init_test(cx, |_| {});
12816
12817 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12818 let mut excerpt1_id = None;
12819 let multibuffer = cx.new(|cx| {
12820 let mut multibuffer = MultiBuffer::new(ReadWrite);
12821 excerpt1_id = multibuffer
12822 .push_excerpts(
12823 buffer.clone(),
12824 [
12825 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12826 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12827 ],
12828 cx,
12829 )
12830 .into_iter()
12831 .next();
12832 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12833 multibuffer
12834 });
12835
12836 let editor = cx.add_window(|window, cx| {
12837 let mut editor = build_editor(multibuffer.clone(), window, cx);
12838 let snapshot = editor.snapshot(window, cx);
12839 editor.begin_selection(
12840 Point::new(1, 3).to_display_point(&snapshot),
12841 false,
12842 1,
12843 window,
12844 cx,
12845 );
12846 assert_eq!(
12847 editor.selections.ranges(cx),
12848 [Point::new(1, 3)..Point::new(1, 3)]
12849 );
12850 editor
12851 });
12852
12853 multibuffer.update(cx, |multibuffer, cx| {
12854 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12855 });
12856 _ = editor.update(cx, |editor, window, cx| {
12857 assert_eq!(
12858 editor.selections.ranges(cx),
12859 [Point::new(0, 0)..Point::new(0, 0)]
12860 );
12861
12862 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12863 editor.change_selections(None, window, cx, |s| s.refresh());
12864 assert_eq!(
12865 editor.selections.ranges(cx),
12866 [Point::new(0, 3)..Point::new(0, 3)]
12867 );
12868 assert!(editor.selections.pending_anchor().is_some());
12869 });
12870}
12871
12872#[gpui::test]
12873async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12874 init_test(cx, |_| {});
12875
12876 let language = Arc::new(
12877 Language::new(
12878 LanguageConfig {
12879 brackets: BracketPairConfig {
12880 pairs: vec![
12881 BracketPair {
12882 start: "{".to_string(),
12883 end: "}".to_string(),
12884 close: true,
12885 surround: true,
12886 newline: true,
12887 },
12888 BracketPair {
12889 start: "/* ".to_string(),
12890 end: " */".to_string(),
12891 close: true,
12892 surround: true,
12893 newline: true,
12894 },
12895 ],
12896 ..Default::default()
12897 },
12898 ..Default::default()
12899 },
12900 Some(tree_sitter_rust::LANGUAGE.into()),
12901 )
12902 .with_indents_query("")
12903 .unwrap(),
12904 );
12905
12906 let text = concat!(
12907 "{ }\n", //
12908 " x\n", //
12909 " /* */\n", //
12910 "x\n", //
12911 "{{} }\n", //
12912 );
12913
12914 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12915 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12916 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12917 editor
12918 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12919 .await;
12920
12921 editor.update_in(cx, |editor, window, cx| {
12922 editor.change_selections(None, window, cx, |s| {
12923 s.select_display_ranges([
12924 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12925 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12926 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12927 ])
12928 });
12929 editor.newline(&Newline, window, cx);
12930
12931 assert_eq!(
12932 editor.buffer().read(cx).read(cx).text(),
12933 concat!(
12934 "{ \n", // Suppress rustfmt
12935 "\n", //
12936 "}\n", //
12937 " x\n", //
12938 " /* \n", //
12939 " \n", //
12940 " */\n", //
12941 "x\n", //
12942 "{{} \n", //
12943 "}\n", //
12944 )
12945 );
12946 });
12947}
12948
12949#[gpui::test]
12950fn test_highlighted_ranges(cx: &mut TestAppContext) {
12951 init_test(cx, |_| {});
12952
12953 let editor = cx.add_window(|window, cx| {
12954 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12955 build_editor(buffer.clone(), window, cx)
12956 });
12957
12958 _ = editor.update(cx, |editor, window, cx| {
12959 struct Type1;
12960 struct Type2;
12961
12962 let buffer = editor.buffer.read(cx).snapshot(cx);
12963
12964 let anchor_range =
12965 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12966
12967 editor.highlight_background::<Type1>(
12968 &[
12969 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12970 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12971 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12972 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12973 ],
12974 |_| Hsla::red(),
12975 cx,
12976 );
12977 editor.highlight_background::<Type2>(
12978 &[
12979 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12980 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12981 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12982 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12983 ],
12984 |_| Hsla::green(),
12985 cx,
12986 );
12987
12988 let snapshot = editor.snapshot(window, cx);
12989 let mut highlighted_ranges = editor.background_highlights_in_range(
12990 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12991 &snapshot,
12992 cx.theme().colors(),
12993 );
12994 // Enforce a consistent ordering based on color without relying on the ordering of the
12995 // highlight's `TypeId` which is non-executor.
12996 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12997 assert_eq!(
12998 highlighted_ranges,
12999 &[
13000 (
13001 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
13002 Hsla::red(),
13003 ),
13004 (
13005 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13006 Hsla::red(),
13007 ),
13008 (
13009 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
13010 Hsla::green(),
13011 ),
13012 (
13013 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
13014 Hsla::green(),
13015 ),
13016 ]
13017 );
13018 assert_eq!(
13019 editor.background_highlights_in_range(
13020 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
13021 &snapshot,
13022 cx.theme().colors(),
13023 ),
13024 &[(
13025 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
13026 Hsla::red(),
13027 )]
13028 );
13029 });
13030}
13031
13032#[gpui::test]
13033async fn test_following(cx: &mut TestAppContext) {
13034 init_test(cx, |_| {});
13035
13036 let fs = FakeFs::new(cx.executor());
13037 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13038
13039 let buffer = project.update(cx, |project, cx| {
13040 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
13041 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
13042 });
13043 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
13044 let follower = cx.update(|cx| {
13045 cx.open_window(
13046 WindowOptions {
13047 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
13048 gpui::Point::new(px(0.), px(0.)),
13049 gpui::Point::new(px(10.), px(80.)),
13050 ))),
13051 ..Default::default()
13052 },
13053 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
13054 )
13055 .unwrap()
13056 });
13057
13058 let is_still_following = Rc::new(RefCell::new(true));
13059 let follower_edit_event_count = Rc::new(RefCell::new(0));
13060 let pending_update = Rc::new(RefCell::new(None));
13061 let leader_entity = leader.root(cx).unwrap();
13062 let follower_entity = follower.root(cx).unwrap();
13063 _ = follower.update(cx, {
13064 let update = pending_update.clone();
13065 let is_still_following = is_still_following.clone();
13066 let follower_edit_event_count = follower_edit_event_count.clone();
13067 |_, window, cx| {
13068 cx.subscribe_in(
13069 &leader_entity,
13070 window,
13071 move |_, leader, event, window, cx| {
13072 leader.read(cx).add_event_to_update_proto(
13073 event,
13074 &mut update.borrow_mut(),
13075 window,
13076 cx,
13077 );
13078 },
13079 )
13080 .detach();
13081
13082 cx.subscribe_in(
13083 &follower_entity,
13084 window,
13085 move |_, _, event: &EditorEvent, _window, _cx| {
13086 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
13087 *is_still_following.borrow_mut() = false;
13088 }
13089
13090 if let EditorEvent::BufferEdited = event {
13091 *follower_edit_event_count.borrow_mut() += 1;
13092 }
13093 },
13094 )
13095 .detach();
13096 }
13097 });
13098
13099 // Update the selections only
13100 _ = leader.update(cx, |leader, window, cx| {
13101 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13102 });
13103 follower
13104 .update(cx, |follower, window, cx| {
13105 follower.apply_update_proto(
13106 &project,
13107 pending_update.borrow_mut().take().unwrap(),
13108 window,
13109 cx,
13110 )
13111 })
13112 .unwrap()
13113 .await
13114 .unwrap();
13115 _ = follower.update(cx, |follower, _, cx| {
13116 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
13117 });
13118 assert!(*is_still_following.borrow());
13119 assert_eq!(*follower_edit_event_count.borrow(), 0);
13120
13121 // Update the scroll position only
13122 _ = leader.update(cx, |leader, window, cx| {
13123 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13124 });
13125 follower
13126 .update(cx, |follower, window, cx| {
13127 follower.apply_update_proto(
13128 &project,
13129 pending_update.borrow_mut().take().unwrap(),
13130 window,
13131 cx,
13132 )
13133 })
13134 .unwrap()
13135 .await
13136 .unwrap();
13137 assert_eq!(
13138 follower
13139 .update(cx, |follower, _, cx| follower.scroll_position(cx))
13140 .unwrap(),
13141 gpui::Point::new(1.5, 3.5)
13142 );
13143 assert!(*is_still_following.borrow());
13144 assert_eq!(*follower_edit_event_count.borrow(), 0);
13145
13146 // Update the selections and scroll position. The follower's scroll position is updated
13147 // via autoscroll, not via the leader's exact scroll position.
13148 _ = leader.update(cx, |leader, window, cx| {
13149 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
13150 leader.request_autoscroll(Autoscroll::newest(), cx);
13151 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
13152 });
13153 follower
13154 .update(cx, |follower, window, cx| {
13155 follower.apply_update_proto(
13156 &project,
13157 pending_update.borrow_mut().take().unwrap(),
13158 window,
13159 cx,
13160 )
13161 })
13162 .unwrap()
13163 .await
13164 .unwrap();
13165 _ = follower.update(cx, |follower, _, cx| {
13166 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
13167 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
13168 });
13169 assert!(*is_still_following.borrow());
13170
13171 // Creating a pending selection that precedes another selection
13172 _ = leader.update(cx, |leader, window, cx| {
13173 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
13174 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
13175 });
13176 follower
13177 .update(cx, |follower, window, cx| {
13178 follower.apply_update_proto(
13179 &project,
13180 pending_update.borrow_mut().take().unwrap(),
13181 window,
13182 cx,
13183 )
13184 })
13185 .unwrap()
13186 .await
13187 .unwrap();
13188 _ = follower.update(cx, |follower, _, cx| {
13189 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
13190 });
13191 assert!(*is_still_following.borrow());
13192
13193 // Extend the pending selection so that it surrounds another selection
13194 _ = leader.update(cx, |leader, window, cx| {
13195 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
13196 });
13197 follower
13198 .update(cx, |follower, window, cx| {
13199 follower.apply_update_proto(
13200 &project,
13201 pending_update.borrow_mut().take().unwrap(),
13202 window,
13203 cx,
13204 )
13205 })
13206 .unwrap()
13207 .await
13208 .unwrap();
13209 _ = follower.update(cx, |follower, _, cx| {
13210 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
13211 });
13212
13213 // Scrolling locally breaks the follow
13214 _ = follower.update(cx, |follower, window, cx| {
13215 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
13216 follower.set_scroll_anchor(
13217 ScrollAnchor {
13218 anchor: top_anchor,
13219 offset: gpui::Point::new(0.0, 0.5),
13220 },
13221 window,
13222 cx,
13223 );
13224 });
13225 assert!(!(*is_still_following.borrow()));
13226}
13227
13228#[gpui::test]
13229async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
13230 init_test(cx, |_| {});
13231
13232 let fs = FakeFs::new(cx.executor());
13233 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
13234 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13235 let pane = workspace
13236 .update(cx, |workspace, _, _| workspace.active_pane().clone())
13237 .unwrap();
13238
13239 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
13240
13241 let leader = pane.update_in(cx, |_, window, cx| {
13242 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
13243 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
13244 });
13245
13246 // Start following the editor when it has no excerpts.
13247 let mut state_message =
13248 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13249 let workspace_entity = workspace.root(cx).unwrap();
13250 let follower_1 = cx
13251 .update_window(*workspace.deref(), |_, window, cx| {
13252 Editor::from_state_proto(
13253 workspace_entity,
13254 ViewId {
13255 creator: CollaboratorId::PeerId(PeerId::default()),
13256 id: 0,
13257 },
13258 &mut state_message,
13259 window,
13260 cx,
13261 )
13262 })
13263 .unwrap()
13264 .unwrap()
13265 .await
13266 .unwrap();
13267
13268 let update_message = Rc::new(RefCell::new(None));
13269 follower_1.update_in(cx, {
13270 let update = update_message.clone();
13271 |_, window, cx| {
13272 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
13273 leader.read(cx).add_event_to_update_proto(
13274 event,
13275 &mut update.borrow_mut(),
13276 window,
13277 cx,
13278 );
13279 })
13280 .detach();
13281 }
13282 });
13283
13284 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
13285 (
13286 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
13287 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
13288 )
13289 });
13290
13291 // Insert some excerpts.
13292 leader.update(cx, |leader, cx| {
13293 leader.buffer.update(cx, |multibuffer, cx| {
13294 multibuffer.set_excerpts_for_path(
13295 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
13296 buffer_1.clone(),
13297 vec![
13298 Point::row_range(0..3),
13299 Point::row_range(1..6),
13300 Point::row_range(12..15),
13301 ],
13302 0,
13303 cx,
13304 );
13305 multibuffer.set_excerpts_for_path(
13306 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
13307 buffer_2.clone(),
13308 vec![Point::row_range(0..6), Point::row_range(8..12)],
13309 0,
13310 cx,
13311 );
13312 });
13313 });
13314
13315 // Apply the update of adding the excerpts.
13316 follower_1
13317 .update_in(cx, |follower, window, cx| {
13318 follower.apply_update_proto(
13319 &project,
13320 update_message.borrow().clone().unwrap(),
13321 window,
13322 cx,
13323 )
13324 })
13325 .await
13326 .unwrap();
13327 assert_eq!(
13328 follower_1.update(cx, |editor, cx| editor.text(cx)),
13329 leader.update(cx, |editor, cx| editor.text(cx))
13330 );
13331 update_message.borrow_mut().take();
13332
13333 // Start following separately after it already has excerpts.
13334 let mut state_message =
13335 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
13336 let workspace_entity = workspace.root(cx).unwrap();
13337 let follower_2 = cx
13338 .update_window(*workspace.deref(), |_, window, cx| {
13339 Editor::from_state_proto(
13340 workspace_entity,
13341 ViewId {
13342 creator: CollaboratorId::PeerId(PeerId::default()),
13343 id: 0,
13344 },
13345 &mut state_message,
13346 window,
13347 cx,
13348 )
13349 })
13350 .unwrap()
13351 .unwrap()
13352 .await
13353 .unwrap();
13354 assert_eq!(
13355 follower_2.update(cx, |editor, cx| editor.text(cx)),
13356 leader.update(cx, |editor, cx| editor.text(cx))
13357 );
13358
13359 // Remove some excerpts.
13360 leader.update(cx, |leader, cx| {
13361 leader.buffer.update(cx, |multibuffer, cx| {
13362 let excerpt_ids = multibuffer.excerpt_ids();
13363 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
13364 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
13365 });
13366 });
13367
13368 // Apply the update of removing the excerpts.
13369 follower_1
13370 .update_in(cx, |follower, window, cx| {
13371 follower.apply_update_proto(
13372 &project,
13373 update_message.borrow().clone().unwrap(),
13374 window,
13375 cx,
13376 )
13377 })
13378 .await
13379 .unwrap();
13380 follower_2
13381 .update_in(cx, |follower, window, cx| {
13382 follower.apply_update_proto(
13383 &project,
13384 update_message.borrow().clone().unwrap(),
13385 window,
13386 cx,
13387 )
13388 })
13389 .await
13390 .unwrap();
13391 update_message.borrow_mut().take();
13392 assert_eq!(
13393 follower_1.update(cx, |editor, cx| editor.text(cx)),
13394 leader.update(cx, |editor, cx| editor.text(cx))
13395 );
13396}
13397
13398#[gpui::test]
13399async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13400 init_test(cx, |_| {});
13401
13402 let mut cx = EditorTestContext::new(cx).await;
13403 let lsp_store =
13404 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
13405
13406 cx.set_state(indoc! {"
13407 ˇfn func(abc def: i32) -> u32 {
13408 }
13409 "});
13410
13411 cx.update(|_, cx| {
13412 lsp_store.update(cx, |lsp_store, cx| {
13413 lsp_store
13414 .update_diagnostics(
13415 LanguageServerId(0),
13416 lsp::PublishDiagnosticsParams {
13417 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
13418 version: None,
13419 diagnostics: vec![
13420 lsp::Diagnostic {
13421 range: lsp::Range::new(
13422 lsp::Position::new(0, 11),
13423 lsp::Position::new(0, 12),
13424 ),
13425 severity: Some(lsp::DiagnosticSeverity::ERROR),
13426 ..Default::default()
13427 },
13428 lsp::Diagnostic {
13429 range: lsp::Range::new(
13430 lsp::Position::new(0, 12),
13431 lsp::Position::new(0, 15),
13432 ),
13433 severity: Some(lsp::DiagnosticSeverity::ERROR),
13434 ..Default::default()
13435 },
13436 lsp::Diagnostic {
13437 range: lsp::Range::new(
13438 lsp::Position::new(0, 25),
13439 lsp::Position::new(0, 28),
13440 ),
13441 severity: Some(lsp::DiagnosticSeverity::ERROR),
13442 ..Default::default()
13443 },
13444 ],
13445 },
13446 &[],
13447 cx,
13448 )
13449 .unwrap()
13450 });
13451 });
13452
13453 executor.run_until_parked();
13454
13455 cx.update_editor(|editor, window, cx| {
13456 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13457 });
13458
13459 cx.assert_editor_state(indoc! {"
13460 fn func(abc def: i32) -> ˇu32 {
13461 }
13462 "});
13463
13464 cx.update_editor(|editor, window, cx| {
13465 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13466 });
13467
13468 cx.assert_editor_state(indoc! {"
13469 fn func(abc ˇdef: i32) -> u32 {
13470 }
13471 "});
13472
13473 cx.update_editor(|editor, window, cx| {
13474 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13475 });
13476
13477 cx.assert_editor_state(indoc! {"
13478 fn func(abcˇ def: i32) -> u32 {
13479 }
13480 "});
13481
13482 cx.update_editor(|editor, window, cx| {
13483 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13484 });
13485
13486 cx.assert_editor_state(indoc! {"
13487 fn func(abc def: i32) -> ˇu32 {
13488 }
13489 "});
13490}
13491
13492#[gpui::test]
13493async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13494 init_test(cx, |_| {});
13495
13496 let mut cx = EditorTestContext::new(cx).await;
13497
13498 let diff_base = r#"
13499 use some::mod;
13500
13501 const A: u32 = 42;
13502
13503 fn main() {
13504 println!("hello");
13505
13506 println!("world");
13507 }
13508 "#
13509 .unindent();
13510
13511 // Edits are modified, removed, modified, added
13512 cx.set_state(
13513 &r#"
13514 use some::modified;
13515
13516 ˇ
13517 fn main() {
13518 println!("hello there");
13519
13520 println!("around the");
13521 println!("world");
13522 }
13523 "#
13524 .unindent(),
13525 );
13526
13527 cx.set_head_text(&diff_base);
13528 executor.run_until_parked();
13529
13530 cx.update_editor(|editor, window, cx| {
13531 //Wrap around the bottom of the buffer
13532 for _ in 0..3 {
13533 editor.go_to_next_hunk(&GoToHunk, window, cx);
13534 }
13535 });
13536
13537 cx.assert_editor_state(
13538 &r#"
13539 ˇuse some::modified;
13540
13541
13542 fn main() {
13543 println!("hello there");
13544
13545 println!("around the");
13546 println!("world");
13547 }
13548 "#
13549 .unindent(),
13550 );
13551
13552 cx.update_editor(|editor, window, cx| {
13553 //Wrap around the top of the buffer
13554 for _ in 0..2 {
13555 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13556 }
13557 });
13558
13559 cx.assert_editor_state(
13560 &r#"
13561 use some::modified;
13562
13563
13564 fn main() {
13565 ˇ println!("hello there");
13566
13567 println!("around the");
13568 println!("world");
13569 }
13570 "#
13571 .unindent(),
13572 );
13573
13574 cx.update_editor(|editor, window, cx| {
13575 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13576 });
13577
13578 cx.assert_editor_state(
13579 &r#"
13580 use some::modified;
13581
13582 ˇ
13583 fn main() {
13584 println!("hello there");
13585
13586 println!("around the");
13587 println!("world");
13588 }
13589 "#
13590 .unindent(),
13591 );
13592
13593 cx.update_editor(|editor, window, cx| {
13594 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13595 });
13596
13597 cx.assert_editor_state(
13598 &r#"
13599 ˇuse some::modified;
13600
13601
13602 fn main() {
13603 println!("hello there");
13604
13605 println!("around the");
13606 println!("world");
13607 }
13608 "#
13609 .unindent(),
13610 );
13611
13612 cx.update_editor(|editor, window, cx| {
13613 for _ in 0..2 {
13614 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13615 }
13616 });
13617
13618 cx.assert_editor_state(
13619 &r#"
13620 use some::modified;
13621
13622
13623 fn main() {
13624 ˇ println!("hello there");
13625
13626 println!("around the");
13627 println!("world");
13628 }
13629 "#
13630 .unindent(),
13631 );
13632
13633 cx.update_editor(|editor, window, cx| {
13634 editor.fold(&Fold, window, cx);
13635 });
13636
13637 cx.update_editor(|editor, window, cx| {
13638 editor.go_to_next_hunk(&GoToHunk, window, cx);
13639 });
13640
13641 cx.assert_editor_state(
13642 &r#"
13643 ˇuse some::modified;
13644
13645
13646 fn main() {
13647 println!("hello there");
13648
13649 println!("around the");
13650 println!("world");
13651 }
13652 "#
13653 .unindent(),
13654 );
13655}
13656
13657#[test]
13658fn test_split_words() {
13659 fn split(text: &str) -> Vec<&str> {
13660 split_words(text).collect()
13661 }
13662
13663 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13664 assert_eq!(split("hello_world"), &["hello_", "world"]);
13665 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13666 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13667 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13668 assert_eq!(split("helloworld"), &["helloworld"]);
13669
13670 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13671}
13672
13673#[gpui::test]
13674async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13675 init_test(cx, |_| {});
13676
13677 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13678 let mut assert = |before, after| {
13679 let _state_context = cx.set_state(before);
13680 cx.run_until_parked();
13681 cx.update_editor(|editor, window, cx| {
13682 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13683 });
13684 cx.run_until_parked();
13685 cx.assert_editor_state(after);
13686 };
13687
13688 // Outside bracket jumps to outside of matching bracket
13689 assert("console.logˇ(var);", "console.log(var)ˇ;");
13690 assert("console.log(var)ˇ;", "console.logˇ(var);");
13691
13692 // Inside bracket jumps to inside of matching bracket
13693 assert("console.log(ˇvar);", "console.log(varˇ);");
13694 assert("console.log(varˇ);", "console.log(ˇvar);");
13695
13696 // When outside a bracket and inside, favor jumping to the inside bracket
13697 assert(
13698 "console.log('foo', [1, 2, 3]ˇ);",
13699 "console.log(ˇ'foo', [1, 2, 3]);",
13700 );
13701 assert(
13702 "console.log(ˇ'foo', [1, 2, 3]);",
13703 "console.log('foo', [1, 2, 3]ˇ);",
13704 );
13705
13706 // Bias forward if two options are equally likely
13707 assert(
13708 "let result = curried_fun()ˇ();",
13709 "let result = curried_fun()()ˇ;",
13710 );
13711
13712 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13713 assert(
13714 indoc! {"
13715 function test() {
13716 console.log('test')ˇ
13717 }"},
13718 indoc! {"
13719 function test() {
13720 console.logˇ('test')
13721 }"},
13722 );
13723}
13724
13725#[gpui::test]
13726async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13727 init_test(cx, |_| {});
13728
13729 let fs = FakeFs::new(cx.executor());
13730 fs.insert_tree(
13731 path!("/a"),
13732 json!({
13733 "main.rs": "fn main() { let a = 5; }",
13734 "other.rs": "// Test file",
13735 }),
13736 )
13737 .await;
13738 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13739
13740 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13741 language_registry.add(Arc::new(Language::new(
13742 LanguageConfig {
13743 name: "Rust".into(),
13744 matcher: LanguageMatcher {
13745 path_suffixes: vec!["rs".to_string()],
13746 ..Default::default()
13747 },
13748 brackets: BracketPairConfig {
13749 pairs: vec![BracketPair {
13750 start: "{".to_string(),
13751 end: "}".to_string(),
13752 close: true,
13753 surround: true,
13754 newline: true,
13755 }],
13756 disabled_scopes_by_bracket_ix: Vec::new(),
13757 },
13758 ..Default::default()
13759 },
13760 Some(tree_sitter_rust::LANGUAGE.into()),
13761 )));
13762 let mut fake_servers = language_registry.register_fake_lsp(
13763 "Rust",
13764 FakeLspAdapter {
13765 capabilities: lsp::ServerCapabilities {
13766 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13767 first_trigger_character: "{".to_string(),
13768 more_trigger_character: None,
13769 }),
13770 ..Default::default()
13771 },
13772 ..Default::default()
13773 },
13774 );
13775
13776 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13777
13778 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13779
13780 let worktree_id = workspace
13781 .update(cx, |workspace, _, cx| {
13782 workspace.project().update(cx, |project, cx| {
13783 project.worktrees(cx).next().unwrap().read(cx).id()
13784 })
13785 })
13786 .unwrap();
13787
13788 let buffer = project
13789 .update(cx, |project, cx| {
13790 project.open_local_buffer(path!("/a/main.rs"), cx)
13791 })
13792 .await
13793 .unwrap();
13794 let editor_handle = workspace
13795 .update(cx, |workspace, window, cx| {
13796 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13797 })
13798 .unwrap()
13799 .await
13800 .unwrap()
13801 .downcast::<Editor>()
13802 .unwrap();
13803
13804 cx.executor().start_waiting();
13805 let fake_server = fake_servers.next().await.unwrap();
13806
13807 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13808 |params, _| async move {
13809 assert_eq!(
13810 params.text_document_position.text_document.uri,
13811 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13812 );
13813 assert_eq!(
13814 params.text_document_position.position,
13815 lsp::Position::new(0, 21),
13816 );
13817
13818 Ok(Some(vec![lsp::TextEdit {
13819 new_text: "]".to_string(),
13820 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13821 }]))
13822 },
13823 );
13824
13825 editor_handle.update_in(cx, |editor, window, cx| {
13826 window.focus(&editor.focus_handle(cx));
13827 editor.change_selections(None, window, cx, |s| {
13828 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13829 });
13830 editor.handle_input("{", window, cx);
13831 });
13832
13833 cx.executor().run_until_parked();
13834
13835 buffer.update(cx, |buffer, _| {
13836 assert_eq!(
13837 buffer.text(),
13838 "fn main() { let a = {5}; }",
13839 "No extra braces from on type formatting should appear in the buffer"
13840 )
13841 });
13842}
13843
13844#[gpui::test]
13845async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13846 init_test(cx, |_| {});
13847
13848 let fs = FakeFs::new(cx.executor());
13849 fs.insert_tree(
13850 path!("/a"),
13851 json!({
13852 "main.rs": "fn main() { let a = 5; }",
13853 "other.rs": "// Test file",
13854 }),
13855 )
13856 .await;
13857
13858 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13859
13860 let server_restarts = Arc::new(AtomicUsize::new(0));
13861 let closure_restarts = Arc::clone(&server_restarts);
13862 let language_server_name = "test language server";
13863 let language_name: LanguageName = "Rust".into();
13864
13865 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13866 language_registry.add(Arc::new(Language::new(
13867 LanguageConfig {
13868 name: language_name.clone(),
13869 matcher: LanguageMatcher {
13870 path_suffixes: vec!["rs".to_string()],
13871 ..Default::default()
13872 },
13873 ..Default::default()
13874 },
13875 Some(tree_sitter_rust::LANGUAGE.into()),
13876 )));
13877 let mut fake_servers = language_registry.register_fake_lsp(
13878 "Rust",
13879 FakeLspAdapter {
13880 name: language_server_name,
13881 initialization_options: Some(json!({
13882 "testOptionValue": true
13883 })),
13884 initializer: Some(Box::new(move |fake_server| {
13885 let task_restarts = Arc::clone(&closure_restarts);
13886 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13887 task_restarts.fetch_add(1, atomic::Ordering::Release);
13888 futures::future::ready(Ok(()))
13889 });
13890 })),
13891 ..Default::default()
13892 },
13893 );
13894
13895 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13896 let _buffer = project
13897 .update(cx, |project, cx| {
13898 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13899 })
13900 .await
13901 .unwrap();
13902 let _fake_server = fake_servers.next().await.unwrap();
13903 update_test_language_settings(cx, |language_settings| {
13904 language_settings.languages.insert(
13905 language_name.clone(),
13906 LanguageSettingsContent {
13907 tab_size: NonZeroU32::new(8),
13908 ..Default::default()
13909 },
13910 );
13911 });
13912 cx.executor().run_until_parked();
13913 assert_eq!(
13914 server_restarts.load(atomic::Ordering::Acquire),
13915 0,
13916 "Should not restart LSP server on an unrelated change"
13917 );
13918
13919 update_test_project_settings(cx, |project_settings| {
13920 project_settings.lsp.insert(
13921 "Some other server name".into(),
13922 LspSettings {
13923 binary: None,
13924 settings: None,
13925 initialization_options: Some(json!({
13926 "some other init value": false
13927 })),
13928 enable_lsp_tasks: false,
13929 },
13930 );
13931 });
13932 cx.executor().run_until_parked();
13933 assert_eq!(
13934 server_restarts.load(atomic::Ordering::Acquire),
13935 0,
13936 "Should not restart LSP server on an unrelated LSP settings change"
13937 );
13938
13939 update_test_project_settings(cx, |project_settings| {
13940 project_settings.lsp.insert(
13941 language_server_name.into(),
13942 LspSettings {
13943 binary: None,
13944 settings: None,
13945 initialization_options: Some(json!({
13946 "anotherInitValue": false
13947 })),
13948 enable_lsp_tasks: false,
13949 },
13950 );
13951 });
13952 cx.executor().run_until_parked();
13953 assert_eq!(
13954 server_restarts.load(atomic::Ordering::Acquire),
13955 1,
13956 "Should restart LSP server on a related LSP settings change"
13957 );
13958
13959 update_test_project_settings(cx, |project_settings| {
13960 project_settings.lsp.insert(
13961 language_server_name.into(),
13962 LspSettings {
13963 binary: None,
13964 settings: None,
13965 initialization_options: Some(json!({
13966 "anotherInitValue": false
13967 })),
13968 enable_lsp_tasks: false,
13969 },
13970 );
13971 });
13972 cx.executor().run_until_parked();
13973 assert_eq!(
13974 server_restarts.load(atomic::Ordering::Acquire),
13975 1,
13976 "Should not restart LSP server on a related LSP settings change that is the same"
13977 );
13978
13979 update_test_project_settings(cx, |project_settings| {
13980 project_settings.lsp.insert(
13981 language_server_name.into(),
13982 LspSettings {
13983 binary: None,
13984 settings: None,
13985 initialization_options: None,
13986 enable_lsp_tasks: false,
13987 },
13988 );
13989 });
13990 cx.executor().run_until_parked();
13991 assert_eq!(
13992 server_restarts.load(atomic::Ordering::Acquire),
13993 2,
13994 "Should restart LSP server on another related LSP settings change"
13995 );
13996}
13997
13998#[gpui::test]
13999async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
14000 init_test(cx, |_| {});
14001
14002 let mut cx = EditorLspTestContext::new_rust(
14003 lsp::ServerCapabilities {
14004 completion_provider: Some(lsp::CompletionOptions {
14005 trigger_characters: Some(vec![".".to_string()]),
14006 resolve_provider: Some(true),
14007 ..Default::default()
14008 }),
14009 ..Default::default()
14010 },
14011 cx,
14012 )
14013 .await;
14014
14015 cx.set_state("fn main() { let a = 2ˇ; }");
14016 cx.simulate_keystroke(".");
14017 let completion_item = lsp::CompletionItem {
14018 label: "some".into(),
14019 kind: Some(lsp::CompletionItemKind::SNIPPET),
14020 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
14021 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
14022 kind: lsp::MarkupKind::Markdown,
14023 value: "```rust\nSome(2)\n```".to_string(),
14024 })),
14025 deprecated: Some(false),
14026 sort_text: Some("fffffff2".to_string()),
14027 filter_text: Some("some".to_string()),
14028 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
14029 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14030 range: lsp::Range {
14031 start: lsp::Position {
14032 line: 0,
14033 character: 22,
14034 },
14035 end: lsp::Position {
14036 line: 0,
14037 character: 22,
14038 },
14039 },
14040 new_text: "Some(2)".to_string(),
14041 })),
14042 additional_text_edits: Some(vec![lsp::TextEdit {
14043 range: lsp::Range {
14044 start: lsp::Position {
14045 line: 0,
14046 character: 20,
14047 },
14048 end: lsp::Position {
14049 line: 0,
14050 character: 22,
14051 },
14052 },
14053 new_text: "".to_string(),
14054 }]),
14055 ..Default::default()
14056 };
14057
14058 let closure_completion_item = completion_item.clone();
14059 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14060 let task_completion_item = closure_completion_item.clone();
14061 async move {
14062 Ok(Some(lsp::CompletionResponse::Array(vec![
14063 task_completion_item,
14064 ])))
14065 }
14066 });
14067
14068 request.next().await;
14069
14070 cx.condition(|editor, _| editor.context_menu_visible())
14071 .await;
14072 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
14073 editor
14074 .confirm_completion(&ConfirmCompletion::default(), window, cx)
14075 .unwrap()
14076 });
14077 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
14078
14079 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
14080 let task_completion_item = completion_item.clone();
14081 async move { Ok(task_completion_item) }
14082 })
14083 .next()
14084 .await
14085 .unwrap();
14086 apply_additional_edits.await.unwrap();
14087 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
14088}
14089
14090#[gpui::test]
14091async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
14092 init_test(cx, |_| {});
14093
14094 let mut cx = EditorLspTestContext::new_rust(
14095 lsp::ServerCapabilities {
14096 completion_provider: Some(lsp::CompletionOptions {
14097 trigger_characters: Some(vec![".".to_string()]),
14098 resolve_provider: Some(true),
14099 ..Default::default()
14100 }),
14101 ..Default::default()
14102 },
14103 cx,
14104 )
14105 .await;
14106
14107 cx.set_state("fn main() { let a = 2ˇ; }");
14108 cx.simulate_keystroke(".");
14109
14110 let item1 = lsp::CompletionItem {
14111 label: "method id()".to_string(),
14112 filter_text: Some("id".to_string()),
14113 detail: None,
14114 documentation: None,
14115 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14116 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14117 new_text: ".id".to_string(),
14118 })),
14119 ..lsp::CompletionItem::default()
14120 };
14121
14122 let item2 = lsp::CompletionItem {
14123 label: "other".to_string(),
14124 filter_text: Some("other".to_string()),
14125 detail: None,
14126 documentation: None,
14127 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14128 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14129 new_text: ".other".to_string(),
14130 })),
14131 ..lsp::CompletionItem::default()
14132 };
14133
14134 let item1 = item1.clone();
14135 cx.set_request_handler::<lsp::request::Completion, _, _>({
14136 let item1 = item1.clone();
14137 move |_, _, _| {
14138 let item1 = item1.clone();
14139 let item2 = item2.clone();
14140 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
14141 }
14142 })
14143 .next()
14144 .await;
14145
14146 cx.condition(|editor, _| editor.context_menu_visible())
14147 .await;
14148 cx.update_editor(|editor, _, _| {
14149 let context_menu = editor.context_menu.borrow_mut();
14150 let context_menu = context_menu
14151 .as_ref()
14152 .expect("Should have the context menu deployed");
14153 match context_menu {
14154 CodeContextMenu::Completions(completions_menu) => {
14155 let completions = completions_menu.completions.borrow_mut();
14156 assert_eq!(
14157 completions
14158 .iter()
14159 .map(|completion| &completion.label.text)
14160 .collect::<Vec<_>>(),
14161 vec!["method id()", "other"]
14162 )
14163 }
14164 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14165 }
14166 });
14167
14168 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
14169 let item1 = item1.clone();
14170 move |_, item_to_resolve, _| {
14171 let item1 = item1.clone();
14172 async move {
14173 if item1 == item_to_resolve {
14174 Ok(lsp::CompletionItem {
14175 label: "method id()".to_string(),
14176 filter_text: Some("id".to_string()),
14177 detail: Some("Now resolved!".to_string()),
14178 documentation: Some(lsp::Documentation::String("Docs".to_string())),
14179 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14180 range: lsp::Range::new(
14181 lsp::Position::new(0, 22),
14182 lsp::Position::new(0, 22),
14183 ),
14184 new_text: ".id".to_string(),
14185 })),
14186 ..lsp::CompletionItem::default()
14187 })
14188 } else {
14189 Ok(item_to_resolve)
14190 }
14191 }
14192 }
14193 })
14194 .next()
14195 .await
14196 .unwrap();
14197 cx.run_until_parked();
14198
14199 cx.update_editor(|editor, window, cx| {
14200 editor.context_menu_next(&Default::default(), window, cx);
14201 });
14202
14203 cx.update_editor(|editor, _, _| {
14204 let context_menu = editor.context_menu.borrow_mut();
14205 let context_menu = context_menu
14206 .as_ref()
14207 .expect("Should have the context menu deployed");
14208 match context_menu {
14209 CodeContextMenu::Completions(completions_menu) => {
14210 let completions = completions_menu.completions.borrow_mut();
14211 assert_eq!(
14212 completions
14213 .iter()
14214 .map(|completion| &completion.label.text)
14215 .collect::<Vec<_>>(),
14216 vec!["method id() Now resolved!", "other"],
14217 "Should update first completion label, but not second as the filter text did not match."
14218 );
14219 }
14220 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14221 }
14222 });
14223}
14224
14225#[gpui::test]
14226async fn test_context_menus_hide_hover_popover(cx: &mut gpui::TestAppContext) {
14227 init_test(cx, |_| {});
14228 let mut cx = EditorLspTestContext::new_rust(
14229 lsp::ServerCapabilities {
14230 hover_provider: Some(lsp::HoverProviderCapability::Simple(true)),
14231 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
14232 completion_provider: Some(lsp::CompletionOptions {
14233 resolve_provider: Some(true),
14234 ..Default::default()
14235 }),
14236 ..Default::default()
14237 },
14238 cx,
14239 )
14240 .await;
14241 cx.set_state(indoc! {"
14242 struct TestStruct {
14243 field: i32
14244 }
14245
14246 fn mainˇ() {
14247 let unused_var = 42;
14248 let test_struct = TestStruct { field: 42 };
14249 }
14250 "});
14251 let symbol_range = cx.lsp_range(indoc! {"
14252 struct TestStruct {
14253 field: i32
14254 }
14255
14256 «fn main»() {
14257 let unused_var = 42;
14258 let test_struct = TestStruct { field: 42 };
14259 }
14260 "});
14261 let mut hover_requests =
14262 cx.set_request_handler::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
14263 Ok(Some(lsp::Hover {
14264 contents: lsp::HoverContents::Markup(lsp::MarkupContent {
14265 kind: lsp::MarkupKind::Markdown,
14266 value: "Function documentation".to_string(),
14267 }),
14268 range: Some(symbol_range),
14269 }))
14270 });
14271
14272 // Case 1: Test that code action menu hide hover popover
14273 cx.dispatch_action(Hover);
14274 hover_requests.next().await;
14275 cx.condition(|editor, _| editor.hover_state.visible()).await;
14276 let mut code_action_requests = cx.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
14277 move |_, _, _| async move {
14278 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
14279 lsp::CodeAction {
14280 title: "Remove unused variable".to_string(),
14281 kind: Some(CodeActionKind::QUICKFIX),
14282 edit: Some(lsp::WorkspaceEdit {
14283 changes: Some(
14284 [(
14285 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
14286 vec![lsp::TextEdit {
14287 range: lsp::Range::new(
14288 lsp::Position::new(5, 4),
14289 lsp::Position::new(5, 27),
14290 ),
14291 new_text: "".to_string(),
14292 }],
14293 )]
14294 .into_iter()
14295 .collect(),
14296 ),
14297 ..Default::default()
14298 }),
14299 ..Default::default()
14300 },
14301 )]))
14302 },
14303 );
14304 cx.update_editor(|editor, window, cx| {
14305 editor.toggle_code_actions(
14306 &ToggleCodeActions {
14307 deployed_from: None,
14308 quick_launch: false,
14309 },
14310 window,
14311 cx,
14312 );
14313 });
14314 code_action_requests.next().await;
14315 cx.run_until_parked();
14316 cx.condition(|editor, _| editor.context_menu_visible())
14317 .await;
14318 cx.update_editor(|editor, _, _| {
14319 assert!(
14320 !editor.hover_state.visible(),
14321 "Hover popover should be hidden when code action menu is shown"
14322 );
14323 // Hide code actions
14324 editor.context_menu.take();
14325 });
14326
14327 // Case 2: Test that code completions hide hover popover
14328 cx.dispatch_action(Hover);
14329 hover_requests.next().await;
14330 cx.condition(|editor, _| editor.hover_state.visible()).await;
14331 let counter = Arc::new(AtomicUsize::new(0));
14332 let mut completion_requests =
14333 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14334 let counter = counter.clone();
14335 async move {
14336 counter.fetch_add(1, atomic::Ordering::Release);
14337 Ok(Some(lsp::CompletionResponse::Array(vec![
14338 lsp::CompletionItem {
14339 label: "main".into(),
14340 kind: Some(lsp::CompletionItemKind::FUNCTION),
14341 detail: Some("() -> ()".to_string()),
14342 ..Default::default()
14343 },
14344 lsp::CompletionItem {
14345 label: "TestStruct".into(),
14346 kind: Some(lsp::CompletionItemKind::STRUCT),
14347 detail: Some("struct TestStruct".to_string()),
14348 ..Default::default()
14349 },
14350 ])))
14351 }
14352 });
14353 cx.update_editor(|editor, window, cx| {
14354 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
14355 });
14356 completion_requests.next().await;
14357 cx.condition(|editor, _| editor.context_menu_visible())
14358 .await;
14359 cx.update_editor(|editor, _, _| {
14360 assert!(
14361 !editor.hover_state.visible(),
14362 "Hover popover should be hidden when completion menu is shown"
14363 );
14364 });
14365}
14366
14367#[gpui::test]
14368async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
14369 init_test(cx, |_| {});
14370
14371 let mut cx = EditorLspTestContext::new_rust(
14372 lsp::ServerCapabilities {
14373 completion_provider: Some(lsp::CompletionOptions {
14374 trigger_characters: Some(vec![".".to_string()]),
14375 resolve_provider: Some(true),
14376 ..Default::default()
14377 }),
14378 ..Default::default()
14379 },
14380 cx,
14381 )
14382 .await;
14383
14384 cx.set_state("fn main() { let a = 2ˇ; }");
14385 cx.simulate_keystroke(".");
14386
14387 let unresolved_item_1 = lsp::CompletionItem {
14388 label: "id".to_string(),
14389 filter_text: Some("id".to_string()),
14390 detail: None,
14391 documentation: None,
14392 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14393 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14394 new_text: ".id".to_string(),
14395 })),
14396 ..lsp::CompletionItem::default()
14397 };
14398 let resolved_item_1 = lsp::CompletionItem {
14399 additional_text_edits: Some(vec![lsp::TextEdit {
14400 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14401 new_text: "!!".to_string(),
14402 }]),
14403 ..unresolved_item_1.clone()
14404 };
14405 let unresolved_item_2 = lsp::CompletionItem {
14406 label: "other".to_string(),
14407 filter_text: Some("other".to_string()),
14408 detail: None,
14409 documentation: None,
14410 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
14411 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
14412 new_text: ".other".to_string(),
14413 })),
14414 ..lsp::CompletionItem::default()
14415 };
14416 let resolved_item_2 = lsp::CompletionItem {
14417 additional_text_edits: Some(vec![lsp::TextEdit {
14418 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
14419 new_text: "??".to_string(),
14420 }]),
14421 ..unresolved_item_2.clone()
14422 };
14423
14424 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
14425 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
14426 cx.lsp
14427 .server
14428 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14429 let unresolved_item_1 = unresolved_item_1.clone();
14430 let resolved_item_1 = resolved_item_1.clone();
14431 let unresolved_item_2 = unresolved_item_2.clone();
14432 let resolved_item_2 = resolved_item_2.clone();
14433 let resolve_requests_1 = resolve_requests_1.clone();
14434 let resolve_requests_2 = resolve_requests_2.clone();
14435 move |unresolved_request, _| {
14436 let unresolved_item_1 = unresolved_item_1.clone();
14437 let resolved_item_1 = resolved_item_1.clone();
14438 let unresolved_item_2 = unresolved_item_2.clone();
14439 let resolved_item_2 = resolved_item_2.clone();
14440 let resolve_requests_1 = resolve_requests_1.clone();
14441 let resolve_requests_2 = resolve_requests_2.clone();
14442 async move {
14443 if unresolved_request == unresolved_item_1 {
14444 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
14445 Ok(resolved_item_1.clone())
14446 } else if unresolved_request == unresolved_item_2 {
14447 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
14448 Ok(resolved_item_2.clone())
14449 } else {
14450 panic!("Unexpected completion item {unresolved_request:?}")
14451 }
14452 }
14453 }
14454 })
14455 .detach();
14456
14457 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14458 let unresolved_item_1 = unresolved_item_1.clone();
14459 let unresolved_item_2 = unresolved_item_2.clone();
14460 async move {
14461 Ok(Some(lsp::CompletionResponse::Array(vec![
14462 unresolved_item_1,
14463 unresolved_item_2,
14464 ])))
14465 }
14466 })
14467 .next()
14468 .await;
14469
14470 cx.condition(|editor, _| editor.context_menu_visible())
14471 .await;
14472 cx.update_editor(|editor, _, _| {
14473 let context_menu = editor.context_menu.borrow_mut();
14474 let context_menu = context_menu
14475 .as_ref()
14476 .expect("Should have the context menu deployed");
14477 match context_menu {
14478 CodeContextMenu::Completions(completions_menu) => {
14479 let completions = completions_menu.completions.borrow_mut();
14480 assert_eq!(
14481 completions
14482 .iter()
14483 .map(|completion| &completion.label.text)
14484 .collect::<Vec<_>>(),
14485 vec!["id", "other"]
14486 )
14487 }
14488 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
14489 }
14490 });
14491 cx.run_until_parked();
14492
14493 cx.update_editor(|editor, window, cx| {
14494 editor.context_menu_next(&ContextMenuNext, window, cx);
14495 });
14496 cx.run_until_parked();
14497 cx.update_editor(|editor, window, cx| {
14498 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14499 });
14500 cx.run_until_parked();
14501 cx.update_editor(|editor, window, cx| {
14502 editor.context_menu_next(&ContextMenuNext, window, cx);
14503 });
14504 cx.run_until_parked();
14505 cx.update_editor(|editor, window, cx| {
14506 editor
14507 .compose_completion(&ComposeCompletion::default(), window, cx)
14508 .expect("No task returned")
14509 })
14510 .await
14511 .expect("Completion failed");
14512 cx.run_until_parked();
14513
14514 cx.update_editor(|editor, _, cx| {
14515 assert_eq!(
14516 resolve_requests_1.load(atomic::Ordering::Acquire),
14517 1,
14518 "Should always resolve once despite multiple selections"
14519 );
14520 assert_eq!(
14521 resolve_requests_2.load(atomic::Ordering::Acquire),
14522 1,
14523 "Should always resolve once after multiple selections and applying the completion"
14524 );
14525 assert_eq!(
14526 editor.text(cx),
14527 "fn main() { let a = ??.other; }",
14528 "Should use resolved data when applying the completion"
14529 );
14530 });
14531}
14532
14533#[gpui::test]
14534async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
14535 init_test(cx, |_| {});
14536
14537 let item_0 = lsp::CompletionItem {
14538 label: "abs".into(),
14539 insert_text: Some("abs".into()),
14540 data: Some(json!({ "very": "special"})),
14541 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
14542 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
14543 lsp::InsertReplaceEdit {
14544 new_text: "abs".to_string(),
14545 insert: lsp::Range::default(),
14546 replace: lsp::Range::default(),
14547 },
14548 )),
14549 ..lsp::CompletionItem::default()
14550 };
14551 let items = iter::once(item_0.clone())
14552 .chain((11..51).map(|i| lsp::CompletionItem {
14553 label: format!("item_{}", i),
14554 insert_text: Some(format!("item_{}", i)),
14555 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
14556 ..lsp::CompletionItem::default()
14557 }))
14558 .collect::<Vec<_>>();
14559
14560 let default_commit_characters = vec!["?".to_string()];
14561 let default_data = json!({ "default": "data"});
14562 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
14563 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
14564 let default_edit_range = lsp::Range {
14565 start: lsp::Position {
14566 line: 0,
14567 character: 5,
14568 },
14569 end: lsp::Position {
14570 line: 0,
14571 character: 5,
14572 },
14573 };
14574
14575 let mut cx = EditorLspTestContext::new_rust(
14576 lsp::ServerCapabilities {
14577 completion_provider: Some(lsp::CompletionOptions {
14578 trigger_characters: Some(vec![".".to_string()]),
14579 resolve_provider: Some(true),
14580 ..Default::default()
14581 }),
14582 ..Default::default()
14583 },
14584 cx,
14585 )
14586 .await;
14587
14588 cx.set_state("fn main() { let a = 2ˇ; }");
14589 cx.simulate_keystroke(".");
14590
14591 let completion_data = default_data.clone();
14592 let completion_characters = default_commit_characters.clone();
14593 let completion_items = items.clone();
14594 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14595 let default_data = completion_data.clone();
14596 let default_commit_characters = completion_characters.clone();
14597 let items = completion_items.clone();
14598 async move {
14599 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14600 items,
14601 item_defaults: Some(lsp::CompletionListItemDefaults {
14602 data: Some(default_data.clone()),
14603 commit_characters: Some(default_commit_characters.clone()),
14604 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14605 default_edit_range,
14606 )),
14607 insert_text_format: Some(default_insert_text_format),
14608 insert_text_mode: Some(default_insert_text_mode),
14609 }),
14610 ..lsp::CompletionList::default()
14611 })))
14612 }
14613 })
14614 .next()
14615 .await;
14616
14617 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14618 cx.lsp
14619 .server
14620 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14621 let closure_resolved_items = resolved_items.clone();
14622 move |item_to_resolve, _| {
14623 let closure_resolved_items = closure_resolved_items.clone();
14624 async move {
14625 closure_resolved_items.lock().push(item_to_resolve.clone());
14626 Ok(item_to_resolve)
14627 }
14628 }
14629 })
14630 .detach();
14631
14632 cx.condition(|editor, _| editor.context_menu_visible())
14633 .await;
14634 cx.run_until_parked();
14635 cx.update_editor(|editor, _, _| {
14636 let menu = editor.context_menu.borrow_mut();
14637 match menu.as_ref().expect("should have the completions menu") {
14638 CodeContextMenu::Completions(completions_menu) => {
14639 assert_eq!(
14640 completions_menu
14641 .entries
14642 .borrow()
14643 .iter()
14644 .map(|mat| mat.string.clone())
14645 .collect::<Vec<String>>(),
14646 items
14647 .iter()
14648 .map(|completion| completion.label.clone())
14649 .collect::<Vec<String>>()
14650 );
14651 }
14652 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14653 }
14654 });
14655 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14656 // with 4 from the end.
14657 assert_eq!(
14658 *resolved_items.lock(),
14659 [&items[0..16], &items[items.len() - 4..items.len()]]
14660 .concat()
14661 .iter()
14662 .cloned()
14663 .map(|mut item| {
14664 if item.data.is_none() {
14665 item.data = Some(default_data.clone());
14666 }
14667 item
14668 })
14669 .collect::<Vec<lsp::CompletionItem>>(),
14670 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14671 );
14672 resolved_items.lock().clear();
14673
14674 cx.update_editor(|editor, window, cx| {
14675 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14676 });
14677 cx.run_until_parked();
14678 // Completions that have already been resolved are skipped.
14679 assert_eq!(
14680 *resolved_items.lock(),
14681 items[items.len() - 16..items.len() - 4]
14682 .iter()
14683 .cloned()
14684 .map(|mut item| {
14685 if item.data.is_none() {
14686 item.data = Some(default_data.clone());
14687 }
14688 item
14689 })
14690 .collect::<Vec<lsp::CompletionItem>>()
14691 );
14692 resolved_items.lock().clear();
14693}
14694
14695#[gpui::test]
14696async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14697 init_test(cx, |_| {});
14698
14699 let mut cx = EditorLspTestContext::new(
14700 Language::new(
14701 LanguageConfig {
14702 matcher: LanguageMatcher {
14703 path_suffixes: vec!["jsx".into()],
14704 ..Default::default()
14705 },
14706 overrides: [(
14707 "element".into(),
14708 LanguageConfigOverride {
14709 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14710 ..Default::default()
14711 },
14712 )]
14713 .into_iter()
14714 .collect(),
14715 ..Default::default()
14716 },
14717 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14718 )
14719 .with_override_query("(jsx_self_closing_element) @element")
14720 .unwrap(),
14721 lsp::ServerCapabilities {
14722 completion_provider: Some(lsp::CompletionOptions {
14723 trigger_characters: Some(vec![":".to_string()]),
14724 ..Default::default()
14725 }),
14726 ..Default::default()
14727 },
14728 cx,
14729 )
14730 .await;
14731
14732 cx.lsp
14733 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14734 Ok(Some(lsp::CompletionResponse::Array(vec![
14735 lsp::CompletionItem {
14736 label: "bg-blue".into(),
14737 ..Default::default()
14738 },
14739 lsp::CompletionItem {
14740 label: "bg-red".into(),
14741 ..Default::default()
14742 },
14743 lsp::CompletionItem {
14744 label: "bg-yellow".into(),
14745 ..Default::default()
14746 },
14747 ])))
14748 });
14749
14750 cx.set_state(r#"<p class="bgˇ" />"#);
14751
14752 // Trigger completion when typing a dash, because the dash is an extra
14753 // word character in the 'element' scope, which contains the cursor.
14754 cx.simulate_keystroke("-");
14755 cx.executor().run_until_parked();
14756 cx.update_editor(|editor, _, _| {
14757 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14758 {
14759 assert_eq!(
14760 completion_menu_entries(&menu),
14761 &["bg-red", "bg-blue", "bg-yellow"]
14762 );
14763 } else {
14764 panic!("expected completion menu to be open");
14765 }
14766 });
14767
14768 cx.simulate_keystroke("l");
14769 cx.executor().run_until_parked();
14770 cx.update_editor(|editor, _, _| {
14771 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14772 {
14773 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14774 } else {
14775 panic!("expected completion menu to be open");
14776 }
14777 });
14778
14779 // When filtering completions, consider the character after the '-' to
14780 // be the start of a subword.
14781 cx.set_state(r#"<p class="yelˇ" />"#);
14782 cx.simulate_keystroke("l");
14783 cx.executor().run_until_parked();
14784 cx.update_editor(|editor, _, _| {
14785 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14786 {
14787 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14788 } else {
14789 panic!("expected completion menu to be open");
14790 }
14791 });
14792}
14793
14794fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14795 let entries = menu.entries.borrow();
14796 entries.iter().map(|mat| mat.string.clone()).collect()
14797}
14798
14799#[gpui::test]
14800async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14801 init_test(cx, |settings| {
14802 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14803 FormatterList(vec![Formatter::Prettier].into()),
14804 ))
14805 });
14806
14807 let fs = FakeFs::new(cx.executor());
14808 fs.insert_file(path!("/file.ts"), Default::default()).await;
14809
14810 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14811 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14812
14813 language_registry.add(Arc::new(Language::new(
14814 LanguageConfig {
14815 name: "TypeScript".into(),
14816 matcher: LanguageMatcher {
14817 path_suffixes: vec!["ts".to_string()],
14818 ..Default::default()
14819 },
14820 ..Default::default()
14821 },
14822 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14823 )));
14824 update_test_language_settings(cx, |settings| {
14825 settings.defaults.prettier = Some(PrettierSettings {
14826 allowed: true,
14827 ..PrettierSettings::default()
14828 });
14829 });
14830
14831 let test_plugin = "test_plugin";
14832 let _ = language_registry.register_fake_lsp(
14833 "TypeScript",
14834 FakeLspAdapter {
14835 prettier_plugins: vec![test_plugin],
14836 ..Default::default()
14837 },
14838 );
14839
14840 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14841 let buffer = project
14842 .update(cx, |project, cx| {
14843 project.open_local_buffer(path!("/file.ts"), cx)
14844 })
14845 .await
14846 .unwrap();
14847
14848 let buffer_text = "one\ntwo\nthree\n";
14849 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14850 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14851 editor.update_in(cx, |editor, window, cx| {
14852 editor.set_text(buffer_text, window, cx)
14853 });
14854
14855 editor
14856 .update_in(cx, |editor, window, cx| {
14857 editor.perform_format(
14858 project.clone(),
14859 FormatTrigger::Manual,
14860 FormatTarget::Buffers,
14861 window,
14862 cx,
14863 )
14864 })
14865 .unwrap()
14866 .await;
14867 assert_eq!(
14868 editor.update(cx, |editor, cx| editor.text(cx)),
14869 buffer_text.to_string() + prettier_format_suffix,
14870 "Test prettier formatting was not applied to the original buffer text",
14871 );
14872
14873 update_test_language_settings(cx, |settings| {
14874 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14875 });
14876 let format = editor.update_in(cx, |editor, window, cx| {
14877 editor.perform_format(
14878 project.clone(),
14879 FormatTrigger::Manual,
14880 FormatTarget::Buffers,
14881 window,
14882 cx,
14883 )
14884 });
14885 format.await.unwrap();
14886 assert_eq!(
14887 editor.update(cx, |editor, cx| editor.text(cx)),
14888 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14889 "Autoformatting (via test prettier) was not applied to the original buffer text",
14890 );
14891}
14892
14893#[gpui::test]
14894async fn test_addition_reverts(cx: &mut TestAppContext) {
14895 init_test(cx, |_| {});
14896 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14897 let base_text = indoc! {r#"
14898 struct Row;
14899 struct Row1;
14900 struct Row2;
14901
14902 struct Row4;
14903 struct Row5;
14904 struct Row6;
14905
14906 struct Row8;
14907 struct Row9;
14908 struct Row10;"#};
14909
14910 // When addition hunks are not adjacent to carets, no hunk revert is performed
14911 assert_hunk_revert(
14912 indoc! {r#"struct Row;
14913 struct Row1;
14914 struct Row1.1;
14915 struct Row1.2;
14916 struct Row2;ˇ
14917
14918 struct Row4;
14919 struct Row5;
14920 struct Row6;
14921
14922 struct Row8;
14923 ˇstruct Row9;
14924 struct Row9.1;
14925 struct Row9.2;
14926 struct Row9.3;
14927 struct Row10;"#},
14928 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14929 indoc! {r#"struct Row;
14930 struct Row1;
14931 struct Row1.1;
14932 struct Row1.2;
14933 struct Row2;ˇ
14934
14935 struct Row4;
14936 struct Row5;
14937 struct Row6;
14938
14939 struct Row8;
14940 ˇstruct Row9;
14941 struct Row9.1;
14942 struct Row9.2;
14943 struct Row9.3;
14944 struct Row10;"#},
14945 base_text,
14946 &mut cx,
14947 );
14948 // Same for selections
14949 assert_hunk_revert(
14950 indoc! {r#"struct Row;
14951 struct Row1;
14952 struct Row2;
14953 struct Row2.1;
14954 struct Row2.2;
14955 «ˇ
14956 struct Row4;
14957 struct» Row5;
14958 «struct Row6;
14959 ˇ»
14960 struct Row9.1;
14961 struct Row9.2;
14962 struct Row9.3;
14963 struct Row8;
14964 struct Row9;
14965 struct Row10;"#},
14966 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14967 indoc! {r#"struct Row;
14968 struct Row1;
14969 struct Row2;
14970 struct Row2.1;
14971 struct Row2.2;
14972 «ˇ
14973 struct Row4;
14974 struct» Row5;
14975 «struct Row6;
14976 ˇ»
14977 struct Row9.1;
14978 struct Row9.2;
14979 struct Row9.3;
14980 struct Row8;
14981 struct Row9;
14982 struct Row10;"#},
14983 base_text,
14984 &mut cx,
14985 );
14986
14987 // When carets and selections intersect the addition hunks, those are reverted.
14988 // Adjacent carets got merged.
14989 assert_hunk_revert(
14990 indoc! {r#"struct Row;
14991 ˇ// something on the top
14992 struct Row1;
14993 struct Row2;
14994 struct Roˇw3.1;
14995 struct Row2.2;
14996 struct Row2.3;ˇ
14997
14998 struct Row4;
14999 struct ˇRow5.1;
15000 struct Row5.2;
15001 struct «Rowˇ»5.3;
15002 struct Row5;
15003 struct Row6;
15004 ˇ
15005 struct Row9.1;
15006 struct «Rowˇ»9.2;
15007 struct «ˇRow»9.3;
15008 struct Row8;
15009 struct Row9;
15010 «ˇ// something on bottom»
15011 struct Row10;"#},
15012 vec![
15013 DiffHunkStatusKind::Added,
15014 DiffHunkStatusKind::Added,
15015 DiffHunkStatusKind::Added,
15016 DiffHunkStatusKind::Added,
15017 DiffHunkStatusKind::Added,
15018 ],
15019 indoc! {r#"struct Row;
15020 ˇstruct Row1;
15021 struct Row2;
15022 ˇ
15023 struct Row4;
15024 ˇstruct Row5;
15025 struct Row6;
15026 ˇ
15027 ˇstruct Row8;
15028 struct Row9;
15029 ˇstruct Row10;"#},
15030 base_text,
15031 &mut cx,
15032 );
15033}
15034
15035#[gpui::test]
15036async fn test_modification_reverts(cx: &mut TestAppContext) {
15037 init_test(cx, |_| {});
15038 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15039 let base_text = indoc! {r#"
15040 struct Row;
15041 struct Row1;
15042 struct Row2;
15043
15044 struct Row4;
15045 struct Row5;
15046 struct Row6;
15047
15048 struct Row8;
15049 struct Row9;
15050 struct Row10;"#};
15051
15052 // Modification hunks behave the same as the addition ones.
15053 assert_hunk_revert(
15054 indoc! {r#"struct Row;
15055 struct Row1;
15056 struct Row33;
15057 ˇ
15058 struct Row4;
15059 struct Row5;
15060 struct Row6;
15061 ˇ
15062 struct Row99;
15063 struct Row9;
15064 struct Row10;"#},
15065 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15066 indoc! {r#"struct Row;
15067 struct Row1;
15068 struct Row33;
15069 ˇ
15070 struct Row4;
15071 struct Row5;
15072 struct Row6;
15073 ˇ
15074 struct Row99;
15075 struct Row9;
15076 struct Row10;"#},
15077 base_text,
15078 &mut cx,
15079 );
15080 assert_hunk_revert(
15081 indoc! {r#"struct Row;
15082 struct Row1;
15083 struct Row33;
15084 «ˇ
15085 struct Row4;
15086 struct» Row5;
15087 «struct Row6;
15088 ˇ»
15089 struct Row99;
15090 struct Row9;
15091 struct Row10;"#},
15092 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
15093 indoc! {r#"struct Row;
15094 struct Row1;
15095 struct Row33;
15096 «ˇ
15097 struct Row4;
15098 struct» Row5;
15099 «struct Row6;
15100 ˇ»
15101 struct Row99;
15102 struct Row9;
15103 struct Row10;"#},
15104 base_text,
15105 &mut cx,
15106 );
15107
15108 assert_hunk_revert(
15109 indoc! {r#"ˇstruct Row1.1;
15110 struct Row1;
15111 «ˇstr»uct Row22;
15112
15113 struct ˇRow44;
15114 struct Row5;
15115 struct «Rˇ»ow66;ˇ
15116
15117 «struˇ»ct Row88;
15118 struct Row9;
15119 struct Row1011;ˇ"#},
15120 vec![
15121 DiffHunkStatusKind::Modified,
15122 DiffHunkStatusKind::Modified,
15123 DiffHunkStatusKind::Modified,
15124 DiffHunkStatusKind::Modified,
15125 DiffHunkStatusKind::Modified,
15126 DiffHunkStatusKind::Modified,
15127 ],
15128 indoc! {r#"struct Row;
15129 ˇstruct Row1;
15130 struct Row2;
15131 ˇ
15132 struct Row4;
15133 ˇstruct Row5;
15134 struct Row6;
15135 ˇ
15136 struct Row8;
15137 ˇstruct Row9;
15138 struct Row10;ˇ"#},
15139 base_text,
15140 &mut cx,
15141 );
15142}
15143
15144#[gpui::test]
15145async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
15146 init_test(cx, |_| {});
15147 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15148 let base_text = indoc! {r#"
15149 one
15150
15151 two
15152 three
15153 "#};
15154
15155 cx.set_head_text(base_text);
15156 cx.set_state("\nˇ\n");
15157 cx.executor().run_until_parked();
15158 cx.update_editor(|editor, _window, cx| {
15159 editor.expand_selected_diff_hunks(cx);
15160 });
15161 cx.executor().run_until_parked();
15162 cx.update_editor(|editor, window, cx| {
15163 editor.backspace(&Default::default(), window, cx);
15164 });
15165 cx.run_until_parked();
15166 cx.assert_state_with_diff(
15167 indoc! {r#"
15168
15169 - two
15170 - threeˇ
15171 +
15172 "#}
15173 .to_string(),
15174 );
15175}
15176
15177#[gpui::test]
15178async fn test_deletion_reverts(cx: &mut TestAppContext) {
15179 init_test(cx, |_| {});
15180 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
15181 let base_text = indoc! {r#"struct Row;
15182struct Row1;
15183struct Row2;
15184
15185struct Row4;
15186struct Row5;
15187struct Row6;
15188
15189struct Row8;
15190struct Row9;
15191struct Row10;"#};
15192
15193 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
15194 assert_hunk_revert(
15195 indoc! {r#"struct Row;
15196 struct Row2;
15197
15198 ˇstruct Row4;
15199 struct Row5;
15200 struct Row6;
15201 ˇ
15202 struct Row8;
15203 struct Row10;"#},
15204 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15205 indoc! {r#"struct Row;
15206 struct Row2;
15207
15208 ˇstruct Row4;
15209 struct Row5;
15210 struct Row6;
15211 ˇ
15212 struct Row8;
15213 struct Row10;"#},
15214 base_text,
15215 &mut cx,
15216 );
15217 assert_hunk_revert(
15218 indoc! {r#"struct Row;
15219 struct Row2;
15220
15221 «ˇstruct Row4;
15222 struct» Row5;
15223 «struct Row6;
15224 ˇ»
15225 struct Row8;
15226 struct Row10;"#},
15227 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15228 indoc! {r#"struct Row;
15229 struct Row2;
15230
15231 «ˇstruct Row4;
15232 struct» Row5;
15233 «struct Row6;
15234 ˇ»
15235 struct Row8;
15236 struct Row10;"#},
15237 base_text,
15238 &mut cx,
15239 );
15240
15241 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
15242 assert_hunk_revert(
15243 indoc! {r#"struct Row;
15244 ˇstruct Row2;
15245
15246 struct Row4;
15247 struct Row5;
15248 struct Row6;
15249
15250 struct Row8;ˇ
15251 struct Row10;"#},
15252 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
15253 indoc! {r#"struct Row;
15254 struct Row1;
15255 ˇstruct Row2;
15256
15257 struct Row4;
15258 struct Row5;
15259 struct Row6;
15260
15261 struct Row8;ˇ
15262 struct Row9;
15263 struct Row10;"#},
15264 base_text,
15265 &mut cx,
15266 );
15267 assert_hunk_revert(
15268 indoc! {r#"struct Row;
15269 struct Row2«ˇ;
15270 struct Row4;
15271 struct» Row5;
15272 «struct Row6;
15273
15274 struct Row8;ˇ»
15275 struct Row10;"#},
15276 vec![
15277 DiffHunkStatusKind::Deleted,
15278 DiffHunkStatusKind::Deleted,
15279 DiffHunkStatusKind::Deleted,
15280 ],
15281 indoc! {r#"struct Row;
15282 struct Row1;
15283 struct Row2«ˇ;
15284
15285 struct Row4;
15286 struct» Row5;
15287 «struct Row6;
15288
15289 struct Row8;ˇ»
15290 struct Row9;
15291 struct Row10;"#},
15292 base_text,
15293 &mut cx,
15294 );
15295}
15296
15297#[gpui::test]
15298async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
15299 init_test(cx, |_| {});
15300
15301 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
15302 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
15303 let base_text_3 =
15304 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
15305
15306 let text_1 = edit_first_char_of_every_line(base_text_1);
15307 let text_2 = edit_first_char_of_every_line(base_text_2);
15308 let text_3 = edit_first_char_of_every_line(base_text_3);
15309
15310 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
15311 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
15312 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
15313
15314 let multibuffer = cx.new(|cx| {
15315 let mut multibuffer = MultiBuffer::new(ReadWrite);
15316 multibuffer.push_excerpts(
15317 buffer_1.clone(),
15318 [
15319 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15320 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15321 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15322 ],
15323 cx,
15324 );
15325 multibuffer.push_excerpts(
15326 buffer_2.clone(),
15327 [
15328 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15329 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15330 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15331 ],
15332 cx,
15333 );
15334 multibuffer.push_excerpts(
15335 buffer_3.clone(),
15336 [
15337 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15338 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15339 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15340 ],
15341 cx,
15342 );
15343 multibuffer
15344 });
15345
15346 let fs = FakeFs::new(cx.executor());
15347 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
15348 let (editor, cx) = cx
15349 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
15350 editor.update_in(cx, |editor, _window, cx| {
15351 for (buffer, diff_base) in [
15352 (buffer_1.clone(), base_text_1),
15353 (buffer_2.clone(), base_text_2),
15354 (buffer_3.clone(), base_text_3),
15355 ] {
15356 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15357 editor
15358 .buffer
15359 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15360 }
15361 });
15362 cx.executor().run_until_parked();
15363
15364 editor.update_in(cx, |editor, window, cx| {
15365 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}");
15366 editor.select_all(&SelectAll, window, cx);
15367 editor.git_restore(&Default::default(), window, cx);
15368 });
15369 cx.executor().run_until_parked();
15370
15371 // When all ranges are selected, all buffer hunks are reverted.
15372 editor.update(cx, |editor, cx| {
15373 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");
15374 });
15375 buffer_1.update(cx, |buffer, _| {
15376 assert_eq!(buffer.text(), base_text_1);
15377 });
15378 buffer_2.update(cx, |buffer, _| {
15379 assert_eq!(buffer.text(), base_text_2);
15380 });
15381 buffer_3.update(cx, |buffer, _| {
15382 assert_eq!(buffer.text(), base_text_3);
15383 });
15384
15385 editor.update_in(cx, |editor, window, cx| {
15386 editor.undo(&Default::default(), window, cx);
15387 });
15388
15389 editor.update_in(cx, |editor, window, cx| {
15390 editor.change_selections(None, window, cx, |s| {
15391 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
15392 });
15393 editor.git_restore(&Default::default(), window, cx);
15394 });
15395
15396 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
15397 // but not affect buffer_2 and its related excerpts.
15398 editor.update(cx, |editor, cx| {
15399 assert_eq!(
15400 editor.text(cx),
15401 "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}"
15402 );
15403 });
15404 buffer_1.update(cx, |buffer, _| {
15405 assert_eq!(buffer.text(), base_text_1);
15406 });
15407 buffer_2.update(cx, |buffer, _| {
15408 assert_eq!(
15409 buffer.text(),
15410 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
15411 );
15412 });
15413 buffer_3.update(cx, |buffer, _| {
15414 assert_eq!(
15415 buffer.text(),
15416 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
15417 );
15418 });
15419
15420 fn edit_first_char_of_every_line(text: &str) -> String {
15421 text.split('\n')
15422 .map(|line| format!("X{}", &line[1..]))
15423 .collect::<Vec<_>>()
15424 .join("\n")
15425 }
15426}
15427
15428#[gpui::test]
15429async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
15430 init_test(cx, |_| {});
15431
15432 let cols = 4;
15433 let rows = 10;
15434 let sample_text_1 = sample_text(rows, cols, 'a');
15435 assert_eq!(
15436 sample_text_1,
15437 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
15438 );
15439 let sample_text_2 = sample_text(rows, cols, 'l');
15440 assert_eq!(
15441 sample_text_2,
15442 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
15443 );
15444 let sample_text_3 = sample_text(rows, cols, 'v');
15445 assert_eq!(
15446 sample_text_3,
15447 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
15448 );
15449
15450 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
15451 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
15452 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
15453
15454 let multi_buffer = cx.new(|cx| {
15455 let mut multibuffer = MultiBuffer::new(ReadWrite);
15456 multibuffer.push_excerpts(
15457 buffer_1.clone(),
15458 [
15459 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15460 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15461 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15462 ],
15463 cx,
15464 );
15465 multibuffer.push_excerpts(
15466 buffer_2.clone(),
15467 [
15468 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15469 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15470 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15471 ],
15472 cx,
15473 );
15474 multibuffer.push_excerpts(
15475 buffer_3.clone(),
15476 [
15477 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15478 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15479 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
15480 ],
15481 cx,
15482 );
15483 multibuffer
15484 });
15485
15486 let fs = FakeFs::new(cx.executor());
15487 fs.insert_tree(
15488 "/a",
15489 json!({
15490 "main.rs": sample_text_1,
15491 "other.rs": sample_text_2,
15492 "lib.rs": sample_text_3,
15493 }),
15494 )
15495 .await;
15496 let project = Project::test(fs, ["/a".as_ref()], cx).await;
15497 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
15498 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
15499 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
15500 Editor::new(
15501 EditorMode::full(),
15502 multi_buffer,
15503 Some(project.clone()),
15504 window,
15505 cx,
15506 )
15507 });
15508 let multibuffer_item_id = workspace
15509 .update(cx, |workspace, window, cx| {
15510 assert!(
15511 workspace.active_item(cx).is_none(),
15512 "active item should be None before the first item is added"
15513 );
15514 workspace.add_item_to_active_pane(
15515 Box::new(multi_buffer_editor.clone()),
15516 None,
15517 true,
15518 window,
15519 cx,
15520 );
15521 let active_item = workspace
15522 .active_item(cx)
15523 .expect("should have an active item after adding the multi buffer");
15524 assert!(
15525 !active_item.is_singleton(cx),
15526 "A multi buffer was expected to active after adding"
15527 );
15528 active_item.item_id()
15529 })
15530 .unwrap();
15531 cx.executor().run_until_parked();
15532
15533 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15534 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15535 s.select_ranges(Some(1..2))
15536 });
15537 editor.open_excerpts(&OpenExcerpts, window, cx);
15538 });
15539 cx.executor().run_until_parked();
15540 let first_item_id = workspace
15541 .update(cx, |workspace, window, cx| {
15542 let active_item = workspace
15543 .active_item(cx)
15544 .expect("should have an active item after navigating into the 1st buffer");
15545 let first_item_id = active_item.item_id();
15546 assert_ne!(
15547 first_item_id, multibuffer_item_id,
15548 "Should navigate into the 1st buffer and activate it"
15549 );
15550 assert!(
15551 active_item.is_singleton(cx),
15552 "New active item should be a singleton buffer"
15553 );
15554 assert_eq!(
15555 active_item
15556 .act_as::<Editor>(cx)
15557 .expect("should have navigated into an editor for the 1st buffer")
15558 .read(cx)
15559 .text(cx),
15560 sample_text_1
15561 );
15562
15563 workspace
15564 .go_back(workspace.active_pane().downgrade(), window, cx)
15565 .detach_and_log_err(cx);
15566
15567 first_item_id
15568 })
15569 .unwrap();
15570 cx.executor().run_until_parked();
15571 workspace
15572 .update(cx, |workspace, _, cx| {
15573 let active_item = workspace
15574 .active_item(cx)
15575 .expect("should have an active item after navigating back");
15576 assert_eq!(
15577 active_item.item_id(),
15578 multibuffer_item_id,
15579 "Should navigate back to the multi buffer"
15580 );
15581 assert!(!active_item.is_singleton(cx));
15582 })
15583 .unwrap();
15584
15585 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15586 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15587 s.select_ranges(Some(39..40))
15588 });
15589 editor.open_excerpts(&OpenExcerpts, window, cx);
15590 });
15591 cx.executor().run_until_parked();
15592 let second_item_id = workspace
15593 .update(cx, |workspace, window, cx| {
15594 let active_item = workspace
15595 .active_item(cx)
15596 .expect("should have an active item after navigating into the 2nd buffer");
15597 let second_item_id = active_item.item_id();
15598 assert_ne!(
15599 second_item_id, multibuffer_item_id,
15600 "Should navigate away from the multibuffer"
15601 );
15602 assert_ne!(
15603 second_item_id, first_item_id,
15604 "Should navigate into the 2nd buffer and activate it"
15605 );
15606 assert!(
15607 active_item.is_singleton(cx),
15608 "New active item should be a singleton buffer"
15609 );
15610 assert_eq!(
15611 active_item
15612 .act_as::<Editor>(cx)
15613 .expect("should have navigated into an editor")
15614 .read(cx)
15615 .text(cx),
15616 sample_text_2
15617 );
15618
15619 workspace
15620 .go_back(workspace.active_pane().downgrade(), window, cx)
15621 .detach_and_log_err(cx);
15622
15623 second_item_id
15624 })
15625 .unwrap();
15626 cx.executor().run_until_parked();
15627 workspace
15628 .update(cx, |workspace, _, cx| {
15629 let active_item = workspace
15630 .active_item(cx)
15631 .expect("should have an active item after navigating back from the 2nd buffer");
15632 assert_eq!(
15633 active_item.item_id(),
15634 multibuffer_item_id,
15635 "Should navigate back from the 2nd buffer to the multi buffer"
15636 );
15637 assert!(!active_item.is_singleton(cx));
15638 })
15639 .unwrap();
15640
15641 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15642 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15643 s.select_ranges(Some(70..70))
15644 });
15645 editor.open_excerpts(&OpenExcerpts, window, cx);
15646 });
15647 cx.executor().run_until_parked();
15648 workspace
15649 .update(cx, |workspace, window, cx| {
15650 let active_item = workspace
15651 .active_item(cx)
15652 .expect("should have an active item after navigating into the 3rd buffer");
15653 let third_item_id = active_item.item_id();
15654 assert_ne!(
15655 third_item_id, multibuffer_item_id,
15656 "Should navigate into the 3rd buffer and activate it"
15657 );
15658 assert_ne!(third_item_id, first_item_id);
15659 assert_ne!(third_item_id, second_item_id);
15660 assert!(
15661 active_item.is_singleton(cx),
15662 "New active item should be a singleton buffer"
15663 );
15664 assert_eq!(
15665 active_item
15666 .act_as::<Editor>(cx)
15667 .expect("should have navigated into an editor")
15668 .read(cx)
15669 .text(cx),
15670 sample_text_3
15671 );
15672
15673 workspace
15674 .go_back(workspace.active_pane().downgrade(), window, cx)
15675 .detach_and_log_err(cx);
15676 })
15677 .unwrap();
15678 cx.executor().run_until_parked();
15679 workspace
15680 .update(cx, |workspace, _, cx| {
15681 let active_item = workspace
15682 .active_item(cx)
15683 .expect("should have an active item after navigating back from the 3rd buffer");
15684 assert_eq!(
15685 active_item.item_id(),
15686 multibuffer_item_id,
15687 "Should navigate back from the 3rd buffer to the multi buffer"
15688 );
15689 assert!(!active_item.is_singleton(cx));
15690 })
15691 .unwrap();
15692}
15693
15694#[gpui::test]
15695async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15696 init_test(cx, |_| {});
15697
15698 let mut cx = EditorTestContext::new(cx).await;
15699
15700 let diff_base = r#"
15701 use some::mod;
15702
15703 const A: u32 = 42;
15704
15705 fn main() {
15706 println!("hello");
15707
15708 println!("world");
15709 }
15710 "#
15711 .unindent();
15712
15713 cx.set_state(
15714 &r#"
15715 use some::modified;
15716
15717 ˇ
15718 fn main() {
15719 println!("hello there");
15720
15721 println!("around the");
15722 println!("world");
15723 }
15724 "#
15725 .unindent(),
15726 );
15727
15728 cx.set_head_text(&diff_base);
15729 executor.run_until_parked();
15730
15731 cx.update_editor(|editor, window, cx| {
15732 editor.go_to_next_hunk(&GoToHunk, window, cx);
15733 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15734 });
15735 executor.run_until_parked();
15736 cx.assert_state_with_diff(
15737 r#"
15738 use some::modified;
15739
15740
15741 fn main() {
15742 - println!("hello");
15743 + ˇ println!("hello there");
15744
15745 println!("around the");
15746 println!("world");
15747 }
15748 "#
15749 .unindent(),
15750 );
15751
15752 cx.update_editor(|editor, window, cx| {
15753 for _ in 0..2 {
15754 editor.go_to_next_hunk(&GoToHunk, window, cx);
15755 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15756 }
15757 });
15758 executor.run_until_parked();
15759 cx.assert_state_with_diff(
15760 r#"
15761 - use some::mod;
15762 + ˇuse some::modified;
15763
15764
15765 fn main() {
15766 - println!("hello");
15767 + println!("hello there");
15768
15769 + println!("around the");
15770 println!("world");
15771 }
15772 "#
15773 .unindent(),
15774 );
15775
15776 cx.update_editor(|editor, window, cx| {
15777 editor.go_to_next_hunk(&GoToHunk, window, cx);
15778 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15779 });
15780 executor.run_until_parked();
15781 cx.assert_state_with_diff(
15782 r#"
15783 - use some::mod;
15784 + use some::modified;
15785
15786 - const A: u32 = 42;
15787 ˇ
15788 fn main() {
15789 - println!("hello");
15790 + println!("hello there");
15791
15792 + println!("around the");
15793 println!("world");
15794 }
15795 "#
15796 .unindent(),
15797 );
15798
15799 cx.update_editor(|editor, window, cx| {
15800 editor.cancel(&Cancel, window, cx);
15801 });
15802
15803 cx.assert_state_with_diff(
15804 r#"
15805 use some::modified;
15806
15807 ˇ
15808 fn main() {
15809 println!("hello there");
15810
15811 println!("around the");
15812 println!("world");
15813 }
15814 "#
15815 .unindent(),
15816 );
15817}
15818
15819#[gpui::test]
15820async fn test_diff_base_change_with_expanded_diff_hunks(
15821 executor: BackgroundExecutor,
15822 cx: &mut TestAppContext,
15823) {
15824 init_test(cx, |_| {});
15825
15826 let mut cx = EditorTestContext::new(cx).await;
15827
15828 let diff_base = r#"
15829 use some::mod1;
15830 use some::mod2;
15831
15832 const A: u32 = 42;
15833 const B: u32 = 42;
15834 const C: u32 = 42;
15835
15836 fn main() {
15837 println!("hello");
15838
15839 println!("world");
15840 }
15841 "#
15842 .unindent();
15843
15844 cx.set_state(
15845 &r#"
15846 use some::mod2;
15847
15848 const A: u32 = 42;
15849 const C: u32 = 42;
15850
15851 fn main(ˇ) {
15852 //println!("hello");
15853
15854 println!("world");
15855 //
15856 //
15857 }
15858 "#
15859 .unindent(),
15860 );
15861
15862 cx.set_head_text(&diff_base);
15863 executor.run_until_parked();
15864
15865 cx.update_editor(|editor, window, cx| {
15866 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15867 });
15868 executor.run_until_parked();
15869 cx.assert_state_with_diff(
15870 r#"
15871 - use some::mod1;
15872 use some::mod2;
15873
15874 const A: u32 = 42;
15875 - const B: u32 = 42;
15876 const C: u32 = 42;
15877
15878 fn main(ˇ) {
15879 - println!("hello");
15880 + //println!("hello");
15881
15882 println!("world");
15883 + //
15884 + //
15885 }
15886 "#
15887 .unindent(),
15888 );
15889
15890 cx.set_head_text("new diff base!");
15891 executor.run_until_parked();
15892 cx.assert_state_with_diff(
15893 r#"
15894 - new diff base!
15895 + use some::mod2;
15896 +
15897 + const A: u32 = 42;
15898 + const C: u32 = 42;
15899 +
15900 + fn main(ˇ) {
15901 + //println!("hello");
15902 +
15903 + println!("world");
15904 + //
15905 + //
15906 + }
15907 "#
15908 .unindent(),
15909 );
15910}
15911
15912#[gpui::test]
15913async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15914 init_test(cx, |_| {});
15915
15916 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15917 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15918 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15919 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15920 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15921 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15922
15923 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15924 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15925 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15926
15927 let multi_buffer = cx.new(|cx| {
15928 let mut multibuffer = MultiBuffer::new(ReadWrite);
15929 multibuffer.push_excerpts(
15930 buffer_1.clone(),
15931 [
15932 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15933 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15934 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15935 ],
15936 cx,
15937 );
15938 multibuffer.push_excerpts(
15939 buffer_2.clone(),
15940 [
15941 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15942 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15943 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15944 ],
15945 cx,
15946 );
15947 multibuffer.push_excerpts(
15948 buffer_3.clone(),
15949 [
15950 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15951 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15952 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15953 ],
15954 cx,
15955 );
15956 multibuffer
15957 });
15958
15959 let editor =
15960 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15961 editor
15962 .update(cx, |editor, _window, cx| {
15963 for (buffer, diff_base) in [
15964 (buffer_1.clone(), file_1_old),
15965 (buffer_2.clone(), file_2_old),
15966 (buffer_3.clone(), file_3_old),
15967 ] {
15968 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15969 editor
15970 .buffer
15971 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15972 }
15973 })
15974 .unwrap();
15975
15976 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15977 cx.run_until_parked();
15978
15979 cx.assert_editor_state(
15980 &"
15981 ˇaaa
15982 ccc
15983 ddd
15984
15985 ggg
15986 hhh
15987
15988
15989 lll
15990 mmm
15991 NNN
15992
15993 qqq
15994 rrr
15995
15996 uuu
15997 111
15998 222
15999 333
16000
16001 666
16002 777
16003
16004 000
16005 !!!"
16006 .unindent(),
16007 );
16008
16009 cx.update_editor(|editor, window, cx| {
16010 editor.select_all(&SelectAll, window, cx);
16011 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
16012 });
16013 cx.executor().run_until_parked();
16014
16015 cx.assert_state_with_diff(
16016 "
16017 «aaa
16018 - bbb
16019 ccc
16020 ddd
16021
16022 ggg
16023 hhh
16024
16025
16026 lll
16027 mmm
16028 - nnn
16029 + NNN
16030
16031 qqq
16032 rrr
16033
16034 uuu
16035 111
16036 222
16037 333
16038
16039 + 666
16040 777
16041
16042 000
16043 !!!ˇ»"
16044 .unindent(),
16045 );
16046}
16047
16048#[gpui::test]
16049async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
16050 init_test(cx, |_| {});
16051
16052 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
16053 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
16054
16055 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
16056 let multi_buffer = cx.new(|cx| {
16057 let mut multibuffer = MultiBuffer::new(ReadWrite);
16058 multibuffer.push_excerpts(
16059 buffer.clone(),
16060 [
16061 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
16062 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
16063 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
16064 ],
16065 cx,
16066 );
16067 multibuffer
16068 });
16069
16070 let editor =
16071 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
16072 editor
16073 .update(cx, |editor, _window, cx| {
16074 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
16075 editor
16076 .buffer
16077 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
16078 })
16079 .unwrap();
16080
16081 let mut cx = EditorTestContext::for_editor(editor, cx).await;
16082 cx.run_until_parked();
16083
16084 cx.update_editor(|editor, window, cx| {
16085 editor.expand_all_diff_hunks(&Default::default(), window, cx)
16086 });
16087 cx.executor().run_until_parked();
16088
16089 // When the start of a hunk coincides with the start of its excerpt,
16090 // the hunk is expanded. When the start of a a hunk is earlier than
16091 // the start of its excerpt, the hunk is not expanded.
16092 cx.assert_state_with_diff(
16093 "
16094 ˇaaa
16095 - bbb
16096 + BBB
16097
16098 - ddd
16099 - eee
16100 + DDD
16101 + EEE
16102 fff
16103
16104 iii
16105 "
16106 .unindent(),
16107 );
16108}
16109
16110#[gpui::test]
16111async fn test_edits_around_expanded_insertion_hunks(
16112 executor: BackgroundExecutor,
16113 cx: &mut TestAppContext,
16114) {
16115 init_test(cx, |_| {});
16116
16117 let mut cx = EditorTestContext::new(cx).await;
16118
16119 let diff_base = r#"
16120 use some::mod1;
16121 use some::mod2;
16122
16123 const A: u32 = 42;
16124
16125 fn main() {
16126 println!("hello");
16127
16128 println!("world");
16129 }
16130 "#
16131 .unindent();
16132 executor.run_until_parked();
16133 cx.set_state(
16134 &r#"
16135 use some::mod1;
16136 use some::mod2;
16137
16138 const A: u32 = 42;
16139 const B: u32 = 42;
16140 const C: u32 = 42;
16141 ˇ
16142
16143 fn main() {
16144 println!("hello");
16145
16146 println!("world");
16147 }
16148 "#
16149 .unindent(),
16150 );
16151
16152 cx.set_head_text(&diff_base);
16153 executor.run_until_parked();
16154
16155 cx.update_editor(|editor, window, cx| {
16156 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16157 });
16158 executor.run_until_parked();
16159
16160 cx.assert_state_with_diff(
16161 r#"
16162 use some::mod1;
16163 use some::mod2;
16164
16165 const A: u32 = 42;
16166 + const B: u32 = 42;
16167 + const C: u32 = 42;
16168 + ˇ
16169
16170 fn main() {
16171 println!("hello");
16172
16173 println!("world");
16174 }
16175 "#
16176 .unindent(),
16177 );
16178
16179 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
16180 executor.run_until_parked();
16181
16182 cx.assert_state_with_diff(
16183 r#"
16184 use some::mod1;
16185 use some::mod2;
16186
16187 const A: u32 = 42;
16188 + const B: u32 = 42;
16189 + const C: u32 = 42;
16190 + const D: u32 = 42;
16191 + ˇ
16192
16193 fn main() {
16194 println!("hello");
16195
16196 println!("world");
16197 }
16198 "#
16199 .unindent(),
16200 );
16201
16202 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
16203 executor.run_until_parked();
16204
16205 cx.assert_state_with_diff(
16206 r#"
16207 use some::mod1;
16208 use some::mod2;
16209
16210 const A: u32 = 42;
16211 + const B: u32 = 42;
16212 + const C: u32 = 42;
16213 + const D: u32 = 42;
16214 + const E: u32 = 42;
16215 + ˇ
16216
16217 fn main() {
16218 println!("hello");
16219
16220 println!("world");
16221 }
16222 "#
16223 .unindent(),
16224 );
16225
16226 cx.update_editor(|editor, window, cx| {
16227 editor.delete_line(&DeleteLine, window, cx);
16228 });
16229 executor.run_until_parked();
16230
16231 cx.assert_state_with_diff(
16232 r#"
16233 use some::mod1;
16234 use some::mod2;
16235
16236 const A: u32 = 42;
16237 + const B: u32 = 42;
16238 + const C: u32 = 42;
16239 + const D: u32 = 42;
16240 + const E: u32 = 42;
16241 ˇ
16242 fn main() {
16243 println!("hello");
16244
16245 println!("world");
16246 }
16247 "#
16248 .unindent(),
16249 );
16250
16251 cx.update_editor(|editor, window, cx| {
16252 editor.move_up(&MoveUp, window, cx);
16253 editor.delete_line(&DeleteLine, window, cx);
16254 editor.move_up(&MoveUp, window, cx);
16255 editor.delete_line(&DeleteLine, window, cx);
16256 editor.move_up(&MoveUp, window, cx);
16257 editor.delete_line(&DeleteLine, window, cx);
16258 });
16259 executor.run_until_parked();
16260 cx.assert_state_with_diff(
16261 r#"
16262 use some::mod1;
16263 use some::mod2;
16264
16265 const A: u32 = 42;
16266 + const B: u32 = 42;
16267 ˇ
16268 fn main() {
16269 println!("hello");
16270
16271 println!("world");
16272 }
16273 "#
16274 .unindent(),
16275 );
16276
16277 cx.update_editor(|editor, window, cx| {
16278 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
16279 editor.delete_line(&DeleteLine, window, cx);
16280 });
16281 executor.run_until_parked();
16282 cx.assert_state_with_diff(
16283 r#"
16284 ˇ
16285 fn main() {
16286 println!("hello");
16287
16288 println!("world");
16289 }
16290 "#
16291 .unindent(),
16292 );
16293}
16294
16295#[gpui::test]
16296async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
16297 init_test(cx, |_| {});
16298
16299 let mut cx = EditorTestContext::new(cx).await;
16300 cx.set_head_text(indoc! { "
16301 one
16302 two
16303 three
16304 four
16305 five
16306 "
16307 });
16308 cx.set_state(indoc! { "
16309 one
16310 ˇthree
16311 five
16312 "});
16313 cx.run_until_parked();
16314 cx.update_editor(|editor, window, cx| {
16315 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16316 });
16317 cx.assert_state_with_diff(
16318 indoc! { "
16319 one
16320 - two
16321 ˇthree
16322 - four
16323 five
16324 "}
16325 .to_string(),
16326 );
16327 cx.update_editor(|editor, window, cx| {
16328 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16329 });
16330
16331 cx.assert_state_with_diff(
16332 indoc! { "
16333 one
16334 ˇthree
16335 five
16336 "}
16337 .to_string(),
16338 );
16339
16340 cx.set_state(indoc! { "
16341 one
16342 ˇTWO
16343 three
16344 four
16345 five
16346 "});
16347 cx.run_until_parked();
16348 cx.update_editor(|editor, window, cx| {
16349 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16350 });
16351
16352 cx.assert_state_with_diff(
16353 indoc! { "
16354 one
16355 - two
16356 + ˇTWO
16357 three
16358 four
16359 five
16360 "}
16361 .to_string(),
16362 );
16363 cx.update_editor(|editor, window, cx| {
16364 editor.move_up(&Default::default(), window, cx);
16365 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
16366 });
16367 cx.assert_state_with_diff(
16368 indoc! { "
16369 one
16370 ˇTWO
16371 three
16372 four
16373 five
16374 "}
16375 .to_string(),
16376 );
16377}
16378
16379#[gpui::test]
16380async fn test_edits_around_expanded_deletion_hunks(
16381 executor: BackgroundExecutor,
16382 cx: &mut TestAppContext,
16383) {
16384 init_test(cx, |_| {});
16385
16386 let mut cx = EditorTestContext::new(cx).await;
16387
16388 let diff_base = r#"
16389 use some::mod1;
16390 use some::mod2;
16391
16392 const A: u32 = 42;
16393 const B: u32 = 42;
16394 const C: u32 = 42;
16395
16396
16397 fn main() {
16398 println!("hello");
16399
16400 println!("world");
16401 }
16402 "#
16403 .unindent();
16404 executor.run_until_parked();
16405 cx.set_state(
16406 &r#"
16407 use some::mod1;
16408 use some::mod2;
16409
16410 ˇconst B: u32 = 42;
16411 const C: u32 = 42;
16412
16413
16414 fn main() {
16415 println!("hello");
16416
16417 println!("world");
16418 }
16419 "#
16420 .unindent(),
16421 );
16422
16423 cx.set_head_text(&diff_base);
16424 executor.run_until_parked();
16425
16426 cx.update_editor(|editor, window, cx| {
16427 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16428 });
16429 executor.run_until_parked();
16430
16431 cx.assert_state_with_diff(
16432 r#"
16433 use some::mod1;
16434 use some::mod2;
16435
16436 - const A: u32 = 42;
16437 ˇconst B: u32 = 42;
16438 const C: u32 = 42;
16439
16440
16441 fn main() {
16442 println!("hello");
16443
16444 println!("world");
16445 }
16446 "#
16447 .unindent(),
16448 );
16449
16450 cx.update_editor(|editor, window, cx| {
16451 editor.delete_line(&DeleteLine, window, cx);
16452 });
16453 executor.run_until_parked();
16454 cx.assert_state_with_diff(
16455 r#"
16456 use some::mod1;
16457 use some::mod2;
16458
16459 - const A: u32 = 42;
16460 - const B: u32 = 42;
16461 ˇconst C: u32 = 42;
16462
16463
16464 fn main() {
16465 println!("hello");
16466
16467 println!("world");
16468 }
16469 "#
16470 .unindent(),
16471 );
16472
16473 cx.update_editor(|editor, window, cx| {
16474 editor.delete_line(&DeleteLine, window, cx);
16475 });
16476 executor.run_until_parked();
16477 cx.assert_state_with_diff(
16478 r#"
16479 use some::mod1;
16480 use some::mod2;
16481
16482 - const A: u32 = 42;
16483 - const B: u32 = 42;
16484 - const C: u32 = 42;
16485 ˇ
16486
16487 fn main() {
16488 println!("hello");
16489
16490 println!("world");
16491 }
16492 "#
16493 .unindent(),
16494 );
16495
16496 cx.update_editor(|editor, window, cx| {
16497 editor.handle_input("replacement", window, cx);
16498 });
16499 executor.run_until_parked();
16500 cx.assert_state_with_diff(
16501 r#"
16502 use some::mod1;
16503 use some::mod2;
16504
16505 - const A: u32 = 42;
16506 - const B: u32 = 42;
16507 - const C: u32 = 42;
16508 -
16509 + replacementˇ
16510
16511 fn main() {
16512 println!("hello");
16513
16514 println!("world");
16515 }
16516 "#
16517 .unindent(),
16518 );
16519}
16520
16521#[gpui::test]
16522async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16523 init_test(cx, |_| {});
16524
16525 let mut cx = EditorTestContext::new(cx).await;
16526
16527 let base_text = r#"
16528 one
16529 two
16530 three
16531 four
16532 five
16533 "#
16534 .unindent();
16535 executor.run_until_parked();
16536 cx.set_state(
16537 &r#"
16538 one
16539 two
16540 fˇour
16541 five
16542 "#
16543 .unindent(),
16544 );
16545
16546 cx.set_head_text(&base_text);
16547 executor.run_until_parked();
16548
16549 cx.update_editor(|editor, window, cx| {
16550 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16551 });
16552 executor.run_until_parked();
16553
16554 cx.assert_state_with_diff(
16555 r#"
16556 one
16557 two
16558 - three
16559 fˇour
16560 five
16561 "#
16562 .unindent(),
16563 );
16564
16565 cx.update_editor(|editor, window, cx| {
16566 editor.backspace(&Backspace, window, cx);
16567 editor.backspace(&Backspace, window, cx);
16568 });
16569 executor.run_until_parked();
16570 cx.assert_state_with_diff(
16571 r#"
16572 one
16573 two
16574 - threeˇ
16575 - four
16576 + our
16577 five
16578 "#
16579 .unindent(),
16580 );
16581}
16582
16583#[gpui::test]
16584async fn test_edit_after_expanded_modification_hunk(
16585 executor: BackgroundExecutor,
16586 cx: &mut TestAppContext,
16587) {
16588 init_test(cx, |_| {});
16589
16590 let mut cx = EditorTestContext::new(cx).await;
16591
16592 let diff_base = r#"
16593 use some::mod1;
16594 use some::mod2;
16595
16596 const A: u32 = 42;
16597 const B: u32 = 42;
16598 const C: u32 = 42;
16599 const D: u32 = 42;
16600
16601
16602 fn main() {
16603 println!("hello");
16604
16605 println!("world");
16606 }"#
16607 .unindent();
16608
16609 cx.set_state(
16610 &r#"
16611 use some::mod1;
16612 use some::mod2;
16613
16614 const A: u32 = 42;
16615 const B: u32 = 42;
16616 const C: u32 = 43ˇ
16617 const D: u32 = 42;
16618
16619
16620 fn main() {
16621 println!("hello");
16622
16623 println!("world");
16624 }"#
16625 .unindent(),
16626 );
16627
16628 cx.set_head_text(&diff_base);
16629 executor.run_until_parked();
16630 cx.update_editor(|editor, window, cx| {
16631 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16632 });
16633 executor.run_until_parked();
16634
16635 cx.assert_state_with_diff(
16636 r#"
16637 use some::mod1;
16638 use some::mod2;
16639
16640 const A: u32 = 42;
16641 const B: u32 = 42;
16642 - const C: u32 = 42;
16643 + const C: u32 = 43ˇ
16644 const D: u32 = 42;
16645
16646
16647 fn main() {
16648 println!("hello");
16649
16650 println!("world");
16651 }"#
16652 .unindent(),
16653 );
16654
16655 cx.update_editor(|editor, window, cx| {
16656 editor.handle_input("\nnew_line\n", window, cx);
16657 });
16658 executor.run_until_parked();
16659
16660 cx.assert_state_with_diff(
16661 r#"
16662 use some::mod1;
16663 use some::mod2;
16664
16665 const A: u32 = 42;
16666 const B: u32 = 42;
16667 - const C: u32 = 42;
16668 + const C: u32 = 43
16669 + new_line
16670 + ˇ
16671 const D: u32 = 42;
16672
16673
16674 fn main() {
16675 println!("hello");
16676
16677 println!("world");
16678 }"#
16679 .unindent(),
16680 );
16681}
16682
16683#[gpui::test]
16684async fn test_stage_and_unstage_added_file_hunk(
16685 executor: BackgroundExecutor,
16686 cx: &mut TestAppContext,
16687) {
16688 init_test(cx, |_| {});
16689
16690 let mut cx = EditorTestContext::new(cx).await;
16691 cx.update_editor(|editor, _, cx| {
16692 editor.set_expand_all_diff_hunks(cx);
16693 });
16694
16695 let working_copy = r#"
16696 ˇfn main() {
16697 println!("hello, world!");
16698 }
16699 "#
16700 .unindent();
16701
16702 cx.set_state(&working_copy);
16703 executor.run_until_parked();
16704
16705 cx.assert_state_with_diff(
16706 r#"
16707 + ˇfn main() {
16708 + println!("hello, world!");
16709 + }
16710 "#
16711 .unindent(),
16712 );
16713 cx.assert_index_text(None);
16714
16715 cx.update_editor(|editor, window, cx| {
16716 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16717 });
16718 executor.run_until_parked();
16719 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16720 cx.assert_state_with_diff(
16721 r#"
16722 + ˇfn main() {
16723 + println!("hello, world!");
16724 + }
16725 "#
16726 .unindent(),
16727 );
16728
16729 cx.update_editor(|editor, window, cx| {
16730 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16731 });
16732 executor.run_until_parked();
16733 cx.assert_index_text(None);
16734}
16735
16736async fn setup_indent_guides_editor(
16737 text: &str,
16738 cx: &mut TestAppContext,
16739) -> (BufferId, EditorTestContext) {
16740 init_test(cx, |_| {});
16741
16742 let mut cx = EditorTestContext::new(cx).await;
16743
16744 let buffer_id = cx.update_editor(|editor, window, cx| {
16745 editor.set_text(text, window, cx);
16746 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16747
16748 buffer_ids[0]
16749 });
16750
16751 (buffer_id, cx)
16752}
16753
16754fn assert_indent_guides(
16755 range: Range<u32>,
16756 expected: Vec<IndentGuide>,
16757 active_indices: Option<Vec<usize>>,
16758 cx: &mut EditorTestContext,
16759) {
16760 let indent_guides = cx.update_editor(|editor, window, cx| {
16761 let snapshot = editor.snapshot(window, cx).display_snapshot;
16762 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16763 editor,
16764 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16765 true,
16766 &snapshot,
16767 cx,
16768 );
16769
16770 indent_guides.sort_by(|a, b| {
16771 a.depth.cmp(&b.depth).then(
16772 a.start_row
16773 .cmp(&b.start_row)
16774 .then(a.end_row.cmp(&b.end_row)),
16775 )
16776 });
16777 indent_guides
16778 });
16779
16780 if let Some(expected) = active_indices {
16781 let active_indices = cx.update_editor(|editor, window, cx| {
16782 let snapshot = editor.snapshot(window, cx).display_snapshot;
16783 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16784 });
16785
16786 assert_eq!(
16787 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16788 expected,
16789 "Active indent guide indices do not match"
16790 );
16791 }
16792
16793 assert_eq!(indent_guides, expected, "Indent guides do not match");
16794}
16795
16796fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16797 IndentGuide {
16798 buffer_id,
16799 start_row: MultiBufferRow(start_row),
16800 end_row: MultiBufferRow(end_row),
16801 depth,
16802 tab_size: 4,
16803 settings: IndentGuideSettings {
16804 enabled: true,
16805 line_width: 1,
16806 active_line_width: 1,
16807 ..Default::default()
16808 },
16809 }
16810}
16811
16812#[gpui::test]
16813async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16814 let (buffer_id, mut cx) = setup_indent_guides_editor(
16815 &"
16816 fn main() {
16817 let a = 1;
16818 }"
16819 .unindent(),
16820 cx,
16821 )
16822 .await;
16823
16824 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16825}
16826
16827#[gpui::test]
16828async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16829 let (buffer_id, mut cx) = setup_indent_guides_editor(
16830 &"
16831 fn main() {
16832 let a = 1;
16833 let b = 2;
16834 }"
16835 .unindent(),
16836 cx,
16837 )
16838 .await;
16839
16840 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16841}
16842
16843#[gpui::test]
16844async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16845 let (buffer_id, mut cx) = setup_indent_guides_editor(
16846 &"
16847 fn main() {
16848 let a = 1;
16849 if a == 3 {
16850 let b = 2;
16851 } else {
16852 let c = 3;
16853 }
16854 }"
16855 .unindent(),
16856 cx,
16857 )
16858 .await;
16859
16860 assert_indent_guides(
16861 0..8,
16862 vec![
16863 indent_guide(buffer_id, 1, 6, 0),
16864 indent_guide(buffer_id, 3, 3, 1),
16865 indent_guide(buffer_id, 5, 5, 1),
16866 ],
16867 None,
16868 &mut cx,
16869 );
16870}
16871
16872#[gpui::test]
16873async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16874 let (buffer_id, mut cx) = setup_indent_guides_editor(
16875 &"
16876 fn main() {
16877 let a = 1;
16878 let b = 2;
16879 let c = 3;
16880 }"
16881 .unindent(),
16882 cx,
16883 )
16884 .await;
16885
16886 assert_indent_guides(
16887 0..5,
16888 vec![
16889 indent_guide(buffer_id, 1, 3, 0),
16890 indent_guide(buffer_id, 2, 2, 1),
16891 ],
16892 None,
16893 &mut cx,
16894 );
16895}
16896
16897#[gpui::test]
16898async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16899 let (buffer_id, mut cx) = setup_indent_guides_editor(
16900 &"
16901 fn main() {
16902 let a = 1;
16903
16904 let c = 3;
16905 }"
16906 .unindent(),
16907 cx,
16908 )
16909 .await;
16910
16911 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16912}
16913
16914#[gpui::test]
16915async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16916 let (buffer_id, mut cx) = setup_indent_guides_editor(
16917 &"
16918 fn main() {
16919 let a = 1;
16920
16921 let c = 3;
16922
16923 if a == 3 {
16924 let b = 2;
16925 } else {
16926 let c = 3;
16927 }
16928 }"
16929 .unindent(),
16930 cx,
16931 )
16932 .await;
16933
16934 assert_indent_guides(
16935 0..11,
16936 vec![
16937 indent_guide(buffer_id, 1, 9, 0),
16938 indent_guide(buffer_id, 6, 6, 1),
16939 indent_guide(buffer_id, 8, 8, 1),
16940 ],
16941 None,
16942 &mut cx,
16943 );
16944}
16945
16946#[gpui::test]
16947async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16948 let (buffer_id, mut cx) = setup_indent_guides_editor(
16949 &"
16950 fn main() {
16951 let a = 1;
16952
16953 let c = 3;
16954
16955 if a == 3 {
16956 let b = 2;
16957 } else {
16958 let c = 3;
16959 }
16960 }"
16961 .unindent(),
16962 cx,
16963 )
16964 .await;
16965
16966 assert_indent_guides(
16967 1..11,
16968 vec![
16969 indent_guide(buffer_id, 1, 9, 0),
16970 indent_guide(buffer_id, 6, 6, 1),
16971 indent_guide(buffer_id, 8, 8, 1),
16972 ],
16973 None,
16974 &mut cx,
16975 );
16976}
16977
16978#[gpui::test]
16979async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16980 let (buffer_id, mut cx) = setup_indent_guides_editor(
16981 &"
16982 fn main() {
16983 let a = 1;
16984
16985 let c = 3;
16986
16987 if a == 3 {
16988 let b = 2;
16989 } else {
16990 let c = 3;
16991 }
16992 }"
16993 .unindent(),
16994 cx,
16995 )
16996 .await;
16997
16998 assert_indent_guides(
16999 1..10,
17000 vec![
17001 indent_guide(buffer_id, 1, 9, 0),
17002 indent_guide(buffer_id, 6, 6, 1),
17003 indent_guide(buffer_id, 8, 8, 1),
17004 ],
17005 None,
17006 &mut cx,
17007 );
17008}
17009
17010#[gpui::test]
17011async fn test_indent_guide_with_folds(cx: &mut TestAppContext) {
17012 let (buffer_id, mut cx) = setup_indent_guides_editor(
17013 &"
17014 fn main() {
17015 if a {
17016 b(
17017 c,
17018 d,
17019 )
17020 } else {
17021 e(
17022 f
17023 )
17024 }
17025 }"
17026 .unindent(),
17027 cx,
17028 )
17029 .await;
17030
17031 assert_indent_guides(
17032 0..11,
17033 vec![
17034 indent_guide(buffer_id, 1, 10, 0),
17035 indent_guide(buffer_id, 2, 5, 1),
17036 indent_guide(buffer_id, 7, 9, 1),
17037 indent_guide(buffer_id, 3, 4, 2),
17038 indent_guide(buffer_id, 8, 8, 2),
17039 ],
17040 None,
17041 &mut cx,
17042 );
17043
17044 cx.update_editor(|editor, window, cx| {
17045 editor.fold_at(MultiBufferRow(2), window, cx);
17046 assert_eq!(
17047 editor.display_text(cx),
17048 "
17049 fn main() {
17050 if a {
17051 b(⋯
17052 )
17053 } else {
17054 e(
17055 f
17056 )
17057 }
17058 }"
17059 .unindent()
17060 );
17061 });
17062
17063 assert_indent_guides(
17064 0..11,
17065 vec![
17066 indent_guide(buffer_id, 1, 10, 0),
17067 indent_guide(buffer_id, 2, 5, 1),
17068 indent_guide(buffer_id, 7, 9, 1),
17069 indent_guide(buffer_id, 8, 8, 2),
17070 ],
17071 None,
17072 &mut cx,
17073 );
17074}
17075
17076#[gpui::test]
17077async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
17078 let (buffer_id, mut cx) = setup_indent_guides_editor(
17079 &"
17080 block1
17081 block2
17082 block3
17083 block4
17084 block2
17085 block1
17086 block1"
17087 .unindent(),
17088 cx,
17089 )
17090 .await;
17091
17092 assert_indent_guides(
17093 1..10,
17094 vec![
17095 indent_guide(buffer_id, 1, 4, 0),
17096 indent_guide(buffer_id, 2, 3, 1),
17097 indent_guide(buffer_id, 3, 3, 2),
17098 ],
17099 None,
17100 &mut cx,
17101 );
17102}
17103
17104#[gpui::test]
17105async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
17106 let (buffer_id, mut cx) = setup_indent_guides_editor(
17107 &"
17108 block1
17109 block2
17110 block3
17111
17112 block1
17113 block1"
17114 .unindent(),
17115 cx,
17116 )
17117 .await;
17118
17119 assert_indent_guides(
17120 0..6,
17121 vec![
17122 indent_guide(buffer_id, 1, 2, 0),
17123 indent_guide(buffer_id, 2, 2, 1),
17124 ],
17125 None,
17126 &mut cx,
17127 );
17128}
17129
17130#[gpui::test]
17131async fn test_indent_guide_ignored_only_whitespace_lines(cx: &mut TestAppContext) {
17132 let (buffer_id, mut cx) = setup_indent_guides_editor(
17133 &"
17134 function component() {
17135 \treturn (
17136 \t\t\t
17137 \t\t<div>
17138 \t\t\t<abc></abc>
17139 \t\t</div>
17140 \t)
17141 }"
17142 .unindent(),
17143 cx,
17144 )
17145 .await;
17146
17147 assert_indent_guides(
17148 0..8,
17149 vec![
17150 indent_guide(buffer_id, 1, 6, 0),
17151 indent_guide(buffer_id, 2, 5, 1),
17152 indent_guide(buffer_id, 4, 4, 2),
17153 ],
17154 None,
17155 &mut cx,
17156 );
17157}
17158
17159#[gpui::test]
17160async fn test_indent_guide_fallback_to_next_non_entirely_whitespace_line(cx: &mut TestAppContext) {
17161 let (buffer_id, mut cx) = setup_indent_guides_editor(
17162 &"
17163 function component() {
17164 \treturn (
17165 \t
17166 \t\t<div>
17167 \t\t\t<abc></abc>
17168 \t\t</div>
17169 \t)
17170 }"
17171 .unindent(),
17172 cx,
17173 )
17174 .await;
17175
17176 assert_indent_guides(
17177 0..8,
17178 vec![
17179 indent_guide(buffer_id, 1, 6, 0),
17180 indent_guide(buffer_id, 2, 5, 1),
17181 indent_guide(buffer_id, 4, 4, 2),
17182 ],
17183 None,
17184 &mut cx,
17185 );
17186}
17187
17188#[gpui::test]
17189async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
17190 let (buffer_id, mut cx) = setup_indent_guides_editor(
17191 &"
17192 block1
17193
17194
17195
17196 block2
17197 "
17198 .unindent(),
17199 cx,
17200 )
17201 .await;
17202
17203 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
17204}
17205
17206#[gpui::test]
17207async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
17208 let (buffer_id, mut cx) = setup_indent_guides_editor(
17209 &"
17210 def a:
17211 \tb = 3
17212 \tif True:
17213 \t\tc = 4
17214 \t\td = 5
17215 \tprint(b)
17216 "
17217 .unindent(),
17218 cx,
17219 )
17220 .await;
17221
17222 assert_indent_guides(
17223 0..6,
17224 vec![
17225 indent_guide(buffer_id, 1, 5, 0),
17226 indent_guide(buffer_id, 3, 4, 1),
17227 ],
17228 None,
17229 &mut cx,
17230 );
17231}
17232
17233#[gpui::test]
17234async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
17235 let (buffer_id, mut cx) = setup_indent_guides_editor(
17236 &"
17237 fn main() {
17238 let a = 1;
17239 }"
17240 .unindent(),
17241 cx,
17242 )
17243 .await;
17244
17245 cx.update_editor(|editor, window, cx| {
17246 editor.change_selections(None, window, cx, |s| {
17247 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17248 });
17249 });
17250
17251 assert_indent_guides(
17252 0..3,
17253 vec![indent_guide(buffer_id, 1, 1, 0)],
17254 Some(vec![0]),
17255 &mut cx,
17256 );
17257}
17258
17259#[gpui::test]
17260async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
17261 let (buffer_id, mut cx) = setup_indent_guides_editor(
17262 &"
17263 fn main() {
17264 if 1 == 2 {
17265 let a = 1;
17266 }
17267 }"
17268 .unindent(),
17269 cx,
17270 )
17271 .await;
17272
17273 cx.update_editor(|editor, window, cx| {
17274 editor.change_selections(None, window, cx, |s| {
17275 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17276 });
17277 });
17278
17279 assert_indent_guides(
17280 0..4,
17281 vec![
17282 indent_guide(buffer_id, 1, 3, 0),
17283 indent_guide(buffer_id, 2, 2, 1),
17284 ],
17285 Some(vec![1]),
17286 &mut cx,
17287 );
17288
17289 cx.update_editor(|editor, window, cx| {
17290 editor.change_selections(None, window, cx, |s| {
17291 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17292 });
17293 });
17294
17295 assert_indent_guides(
17296 0..4,
17297 vec![
17298 indent_guide(buffer_id, 1, 3, 0),
17299 indent_guide(buffer_id, 2, 2, 1),
17300 ],
17301 Some(vec![1]),
17302 &mut cx,
17303 );
17304
17305 cx.update_editor(|editor, window, cx| {
17306 editor.change_selections(None, window, cx, |s| {
17307 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
17308 });
17309 });
17310
17311 assert_indent_guides(
17312 0..4,
17313 vec![
17314 indent_guide(buffer_id, 1, 3, 0),
17315 indent_guide(buffer_id, 2, 2, 1),
17316 ],
17317 Some(vec![0]),
17318 &mut cx,
17319 );
17320}
17321
17322#[gpui::test]
17323async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
17324 let (buffer_id, mut cx) = setup_indent_guides_editor(
17325 &"
17326 fn main() {
17327 let a = 1;
17328
17329 let b = 2;
17330 }"
17331 .unindent(),
17332 cx,
17333 )
17334 .await;
17335
17336 cx.update_editor(|editor, window, cx| {
17337 editor.change_selections(None, window, cx, |s| {
17338 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
17339 });
17340 });
17341
17342 assert_indent_guides(
17343 0..5,
17344 vec![indent_guide(buffer_id, 1, 3, 0)],
17345 Some(vec![0]),
17346 &mut cx,
17347 );
17348}
17349
17350#[gpui::test]
17351async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
17352 let (buffer_id, mut cx) = setup_indent_guides_editor(
17353 &"
17354 def m:
17355 a = 1
17356 pass"
17357 .unindent(),
17358 cx,
17359 )
17360 .await;
17361
17362 cx.update_editor(|editor, window, cx| {
17363 editor.change_selections(None, window, cx, |s| {
17364 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
17365 });
17366 });
17367
17368 assert_indent_guides(
17369 0..3,
17370 vec![indent_guide(buffer_id, 1, 2, 0)],
17371 Some(vec![0]),
17372 &mut cx,
17373 );
17374}
17375
17376#[gpui::test]
17377async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
17378 init_test(cx, |_| {});
17379 let mut cx = EditorTestContext::new(cx).await;
17380 let text = indoc! {
17381 "
17382 impl A {
17383 fn b() {
17384 0;
17385 3;
17386 5;
17387 6;
17388 7;
17389 }
17390 }
17391 "
17392 };
17393 let base_text = indoc! {
17394 "
17395 impl A {
17396 fn b() {
17397 0;
17398 1;
17399 2;
17400 3;
17401 4;
17402 }
17403 fn c() {
17404 5;
17405 6;
17406 7;
17407 }
17408 }
17409 "
17410 };
17411
17412 cx.update_editor(|editor, window, cx| {
17413 editor.set_text(text, window, cx);
17414
17415 editor.buffer().update(cx, |multibuffer, cx| {
17416 let buffer = multibuffer.as_singleton().unwrap();
17417 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
17418
17419 multibuffer.set_all_diff_hunks_expanded(cx);
17420 multibuffer.add_diff(diff, cx);
17421
17422 buffer.read(cx).remote_id()
17423 })
17424 });
17425 cx.run_until_parked();
17426
17427 cx.assert_state_with_diff(
17428 indoc! { "
17429 impl A {
17430 fn b() {
17431 0;
17432 - 1;
17433 - 2;
17434 3;
17435 - 4;
17436 - }
17437 - fn c() {
17438 5;
17439 6;
17440 7;
17441 }
17442 }
17443 ˇ"
17444 }
17445 .to_string(),
17446 );
17447
17448 let mut actual_guides = cx.update_editor(|editor, window, cx| {
17449 editor
17450 .snapshot(window, cx)
17451 .buffer_snapshot
17452 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
17453 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
17454 .collect::<Vec<_>>()
17455 });
17456 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
17457 assert_eq!(
17458 actual_guides,
17459 vec![
17460 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
17461 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
17462 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
17463 ]
17464 );
17465}
17466
17467#[gpui::test]
17468async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
17469 init_test(cx, |_| {});
17470 let mut cx = EditorTestContext::new(cx).await;
17471
17472 let diff_base = r#"
17473 a
17474 b
17475 c
17476 "#
17477 .unindent();
17478
17479 cx.set_state(
17480 &r#"
17481 ˇA
17482 b
17483 C
17484 "#
17485 .unindent(),
17486 );
17487 cx.set_head_text(&diff_base);
17488 cx.update_editor(|editor, window, cx| {
17489 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17490 });
17491 executor.run_until_parked();
17492
17493 let both_hunks_expanded = r#"
17494 - a
17495 + ˇA
17496 b
17497 - c
17498 + C
17499 "#
17500 .unindent();
17501
17502 cx.assert_state_with_diff(both_hunks_expanded.clone());
17503
17504 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17505 let snapshot = editor.snapshot(window, cx);
17506 let hunks = editor
17507 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17508 .collect::<Vec<_>>();
17509 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17510 let buffer_id = hunks[0].buffer_id;
17511 hunks
17512 .into_iter()
17513 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17514 .collect::<Vec<_>>()
17515 });
17516 assert_eq!(hunk_ranges.len(), 2);
17517
17518 cx.update_editor(|editor, _, cx| {
17519 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17520 });
17521 executor.run_until_parked();
17522
17523 let second_hunk_expanded = r#"
17524 ˇA
17525 b
17526 - c
17527 + C
17528 "#
17529 .unindent();
17530
17531 cx.assert_state_with_diff(second_hunk_expanded);
17532
17533 cx.update_editor(|editor, _, cx| {
17534 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17535 });
17536 executor.run_until_parked();
17537
17538 cx.assert_state_with_diff(both_hunks_expanded.clone());
17539
17540 cx.update_editor(|editor, _, cx| {
17541 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17542 });
17543 executor.run_until_parked();
17544
17545 let first_hunk_expanded = r#"
17546 - a
17547 + ˇA
17548 b
17549 C
17550 "#
17551 .unindent();
17552
17553 cx.assert_state_with_diff(first_hunk_expanded);
17554
17555 cx.update_editor(|editor, _, cx| {
17556 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17557 });
17558 executor.run_until_parked();
17559
17560 cx.assert_state_with_diff(both_hunks_expanded);
17561
17562 cx.set_state(
17563 &r#"
17564 ˇA
17565 b
17566 "#
17567 .unindent(),
17568 );
17569 cx.run_until_parked();
17570
17571 // TODO this cursor position seems bad
17572 cx.assert_state_with_diff(
17573 r#"
17574 - ˇa
17575 + A
17576 b
17577 "#
17578 .unindent(),
17579 );
17580
17581 cx.update_editor(|editor, window, cx| {
17582 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17583 });
17584
17585 cx.assert_state_with_diff(
17586 r#"
17587 - ˇa
17588 + A
17589 b
17590 - c
17591 "#
17592 .unindent(),
17593 );
17594
17595 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17596 let snapshot = editor.snapshot(window, cx);
17597 let hunks = editor
17598 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17599 .collect::<Vec<_>>();
17600 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17601 let buffer_id = hunks[0].buffer_id;
17602 hunks
17603 .into_iter()
17604 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17605 .collect::<Vec<_>>()
17606 });
17607 assert_eq!(hunk_ranges.len(), 2);
17608
17609 cx.update_editor(|editor, _, cx| {
17610 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
17611 });
17612 executor.run_until_parked();
17613
17614 cx.assert_state_with_diff(
17615 r#"
17616 - ˇa
17617 + A
17618 b
17619 "#
17620 .unindent(),
17621 );
17622}
17623
17624#[gpui::test]
17625async fn test_toggle_deletion_hunk_at_start_of_file(
17626 executor: BackgroundExecutor,
17627 cx: &mut TestAppContext,
17628) {
17629 init_test(cx, |_| {});
17630 let mut cx = EditorTestContext::new(cx).await;
17631
17632 let diff_base = r#"
17633 a
17634 b
17635 c
17636 "#
17637 .unindent();
17638
17639 cx.set_state(
17640 &r#"
17641 ˇb
17642 c
17643 "#
17644 .unindent(),
17645 );
17646 cx.set_head_text(&diff_base);
17647 cx.update_editor(|editor, window, cx| {
17648 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
17649 });
17650 executor.run_until_parked();
17651
17652 let hunk_expanded = r#"
17653 - a
17654 ˇb
17655 c
17656 "#
17657 .unindent();
17658
17659 cx.assert_state_with_diff(hunk_expanded.clone());
17660
17661 let hunk_ranges = cx.update_editor(|editor, window, cx| {
17662 let snapshot = editor.snapshot(window, cx);
17663 let hunks = editor
17664 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17665 .collect::<Vec<_>>();
17666 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
17667 let buffer_id = hunks[0].buffer_id;
17668 hunks
17669 .into_iter()
17670 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
17671 .collect::<Vec<_>>()
17672 });
17673 assert_eq!(hunk_ranges.len(), 1);
17674
17675 cx.update_editor(|editor, _, cx| {
17676 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17677 });
17678 executor.run_until_parked();
17679
17680 let hunk_collapsed = r#"
17681 ˇb
17682 c
17683 "#
17684 .unindent();
17685
17686 cx.assert_state_with_diff(hunk_collapsed);
17687
17688 cx.update_editor(|editor, _, cx| {
17689 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
17690 });
17691 executor.run_until_parked();
17692
17693 cx.assert_state_with_diff(hunk_expanded.clone());
17694}
17695
17696#[gpui::test]
17697async fn test_display_diff_hunks(cx: &mut TestAppContext) {
17698 init_test(cx, |_| {});
17699
17700 let fs = FakeFs::new(cx.executor());
17701 fs.insert_tree(
17702 path!("/test"),
17703 json!({
17704 ".git": {},
17705 "file-1": "ONE\n",
17706 "file-2": "TWO\n",
17707 "file-3": "THREE\n",
17708 }),
17709 )
17710 .await;
17711
17712 fs.set_head_for_repo(
17713 path!("/test/.git").as_ref(),
17714 &[
17715 ("file-1".into(), "one\n".into()),
17716 ("file-2".into(), "two\n".into()),
17717 ("file-3".into(), "three\n".into()),
17718 ],
17719 );
17720
17721 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17722 let mut buffers = vec![];
17723 for i in 1..=3 {
17724 let buffer = project
17725 .update(cx, |project, cx| {
17726 let path = format!(path!("/test/file-{}"), i);
17727 project.open_local_buffer(path, cx)
17728 })
17729 .await
17730 .unwrap();
17731 buffers.push(buffer);
17732 }
17733
17734 let multibuffer = cx.new(|cx| {
17735 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17736 multibuffer.set_all_diff_hunks_expanded(cx);
17737 for buffer in &buffers {
17738 let snapshot = buffer.read(cx).snapshot();
17739 multibuffer.set_excerpts_for_path(
17740 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17741 buffer.clone(),
17742 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17743 DEFAULT_MULTIBUFFER_CONTEXT,
17744 cx,
17745 );
17746 }
17747 multibuffer
17748 });
17749
17750 let editor = cx.add_window(|window, cx| {
17751 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17752 });
17753 cx.run_until_parked();
17754
17755 let snapshot = editor
17756 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17757 .unwrap();
17758 let hunks = snapshot
17759 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17760 .map(|hunk| match hunk {
17761 DisplayDiffHunk::Unfolded {
17762 display_row_range, ..
17763 } => display_row_range,
17764 DisplayDiffHunk::Folded { .. } => unreachable!(),
17765 })
17766 .collect::<Vec<_>>();
17767 assert_eq!(
17768 hunks,
17769 [
17770 DisplayRow(2)..DisplayRow(4),
17771 DisplayRow(7)..DisplayRow(9),
17772 DisplayRow(12)..DisplayRow(14),
17773 ]
17774 );
17775}
17776
17777#[gpui::test]
17778async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17779 init_test(cx, |_| {});
17780
17781 let mut cx = EditorTestContext::new(cx).await;
17782 cx.set_head_text(indoc! { "
17783 one
17784 two
17785 three
17786 four
17787 five
17788 "
17789 });
17790 cx.set_index_text(indoc! { "
17791 one
17792 two
17793 three
17794 four
17795 five
17796 "
17797 });
17798 cx.set_state(indoc! {"
17799 one
17800 TWO
17801 ˇTHREE
17802 FOUR
17803 five
17804 "});
17805 cx.run_until_parked();
17806 cx.update_editor(|editor, window, cx| {
17807 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17808 });
17809 cx.run_until_parked();
17810 cx.assert_index_text(Some(indoc! {"
17811 one
17812 TWO
17813 THREE
17814 FOUR
17815 five
17816 "}));
17817 cx.set_state(indoc! { "
17818 one
17819 TWO
17820 ˇTHREE-HUNDRED
17821 FOUR
17822 five
17823 "});
17824 cx.run_until_parked();
17825 cx.update_editor(|editor, window, cx| {
17826 let snapshot = editor.snapshot(window, cx);
17827 let hunks = editor
17828 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17829 .collect::<Vec<_>>();
17830 assert_eq!(hunks.len(), 1);
17831 assert_eq!(
17832 hunks[0].status(),
17833 DiffHunkStatus {
17834 kind: DiffHunkStatusKind::Modified,
17835 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17836 }
17837 );
17838
17839 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17840 });
17841 cx.run_until_parked();
17842 cx.assert_index_text(Some(indoc! {"
17843 one
17844 TWO
17845 THREE-HUNDRED
17846 FOUR
17847 five
17848 "}));
17849}
17850
17851#[gpui::test]
17852fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17853 init_test(cx, |_| {});
17854
17855 let editor = cx.add_window(|window, cx| {
17856 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17857 build_editor(buffer, window, cx)
17858 });
17859
17860 let render_args = Arc::new(Mutex::new(None));
17861 let snapshot = editor
17862 .update(cx, |editor, window, cx| {
17863 let snapshot = editor.buffer().read(cx).snapshot(cx);
17864 let range =
17865 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17866
17867 struct RenderArgs {
17868 row: MultiBufferRow,
17869 folded: bool,
17870 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17871 }
17872
17873 let crease = Crease::inline(
17874 range,
17875 FoldPlaceholder::test(),
17876 {
17877 let toggle_callback = render_args.clone();
17878 move |row, folded, callback, _window, _cx| {
17879 *toggle_callback.lock() = Some(RenderArgs {
17880 row,
17881 folded,
17882 callback,
17883 });
17884 div()
17885 }
17886 },
17887 |_row, _folded, _window, _cx| div(),
17888 );
17889
17890 editor.insert_creases(Some(crease), cx);
17891 let snapshot = editor.snapshot(window, cx);
17892 let _div = snapshot.render_crease_toggle(
17893 MultiBufferRow(1),
17894 false,
17895 cx.entity().clone(),
17896 window,
17897 cx,
17898 );
17899 snapshot
17900 })
17901 .unwrap();
17902
17903 let render_args = render_args.lock().take().unwrap();
17904 assert_eq!(render_args.row, MultiBufferRow(1));
17905 assert!(!render_args.folded);
17906 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17907
17908 cx.update_window(*editor, |_, window, cx| {
17909 (render_args.callback)(true, window, cx)
17910 })
17911 .unwrap();
17912 let snapshot = editor
17913 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17914 .unwrap();
17915 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17916
17917 cx.update_window(*editor, |_, window, cx| {
17918 (render_args.callback)(false, window, cx)
17919 })
17920 .unwrap();
17921 let snapshot = editor
17922 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17923 .unwrap();
17924 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17925}
17926
17927#[gpui::test]
17928async fn test_input_text(cx: &mut TestAppContext) {
17929 init_test(cx, |_| {});
17930 let mut cx = EditorTestContext::new(cx).await;
17931
17932 cx.set_state(
17933 &r#"ˇone
17934 two
17935
17936 three
17937 fourˇ
17938 five
17939
17940 siˇx"#
17941 .unindent(),
17942 );
17943
17944 cx.dispatch_action(HandleInput(String::new()));
17945 cx.assert_editor_state(
17946 &r#"ˇone
17947 two
17948
17949 three
17950 fourˇ
17951 five
17952
17953 siˇx"#
17954 .unindent(),
17955 );
17956
17957 cx.dispatch_action(HandleInput("AAAA".to_string()));
17958 cx.assert_editor_state(
17959 &r#"AAAAˇone
17960 two
17961
17962 three
17963 fourAAAAˇ
17964 five
17965
17966 siAAAAˇx"#
17967 .unindent(),
17968 );
17969}
17970
17971#[gpui::test]
17972async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17973 init_test(cx, |_| {});
17974
17975 let mut cx = EditorTestContext::new(cx).await;
17976 cx.set_state(
17977 r#"let foo = 1;
17978let foo = 2;
17979let foo = 3;
17980let fooˇ = 4;
17981let foo = 5;
17982let foo = 6;
17983let foo = 7;
17984let foo = 8;
17985let foo = 9;
17986let foo = 10;
17987let foo = 11;
17988let foo = 12;
17989let foo = 13;
17990let foo = 14;
17991let foo = 15;"#,
17992 );
17993
17994 cx.update_editor(|e, window, cx| {
17995 assert_eq!(
17996 e.next_scroll_position,
17997 NextScrollCursorCenterTopBottom::Center,
17998 "Default next scroll direction is center",
17999 );
18000
18001 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18002 assert_eq!(
18003 e.next_scroll_position,
18004 NextScrollCursorCenterTopBottom::Top,
18005 "After center, next scroll direction should be top",
18006 );
18007
18008 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18009 assert_eq!(
18010 e.next_scroll_position,
18011 NextScrollCursorCenterTopBottom::Bottom,
18012 "After top, next scroll direction should be bottom",
18013 );
18014
18015 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18016 assert_eq!(
18017 e.next_scroll_position,
18018 NextScrollCursorCenterTopBottom::Center,
18019 "After bottom, scrolling should start over",
18020 );
18021
18022 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
18023 assert_eq!(
18024 e.next_scroll_position,
18025 NextScrollCursorCenterTopBottom::Top,
18026 "Scrolling continues if retriggered fast enough"
18027 );
18028 });
18029
18030 cx.executor()
18031 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
18032 cx.executor().run_until_parked();
18033 cx.update_editor(|e, _, _| {
18034 assert_eq!(
18035 e.next_scroll_position,
18036 NextScrollCursorCenterTopBottom::Center,
18037 "If scrolling is not triggered fast enough, it should reset"
18038 );
18039 });
18040}
18041
18042#[gpui::test]
18043async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
18044 init_test(cx, |_| {});
18045 let mut cx = EditorLspTestContext::new_rust(
18046 lsp::ServerCapabilities {
18047 definition_provider: Some(lsp::OneOf::Left(true)),
18048 references_provider: Some(lsp::OneOf::Left(true)),
18049 ..lsp::ServerCapabilities::default()
18050 },
18051 cx,
18052 )
18053 .await;
18054
18055 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
18056 let go_to_definition = cx
18057 .lsp
18058 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18059 move |params, _| async move {
18060 if empty_go_to_definition {
18061 Ok(None)
18062 } else {
18063 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
18064 uri: params.text_document_position_params.text_document.uri,
18065 range: lsp::Range::new(
18066 lsp::Position::new(4, 3),
18067 lsp::Position::new(4, 6),
18068 ),
18069 })))
18070 }
18071 },
18072 );
18073 let references = cx
18074 .lsp
18075 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
18076 Ok(Some(vec![lsp::Location {
18077 uri: params.text_document_position.text_document.uri,
18078 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
18079 }]))
18080 });
18081 (go_to_definition, references)
18082 };
18083
18084 cx.set_state(
18085 &r#"fn one() {
18086 let mut a = ˇtwo();
18087 }
18088
18089 fn two() {}"#
18090 .unindent(),
18091 );
18092 set_up_lsp_handlers(false, &mut cx);
18093 let navigated = cx
18094 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18095 .await
18096 .expect("Failed to navigate to definition");
18097 assert_eq!(
18098 navigated,
18099 Navigated::Yes,
18100 "Should have navigated to definition from the GetDefinition response"
18101 );
18102 cx.assert_editor_state(
18103 &r#"fn one() {
18104 let mut a = two();
18105 }
18106
18107 fn «twoˇ»() {}"#
18108 .unindent(),
18109 );
18110
18111 let editors = cx.update_workspace(|workspace, _, cx| {
18112 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18113 });
18114 cx.update_editor(|_, _, test_editor_cx| {
18115 assert_eq!(
18116 editors.len(),
18117 1,
18118 "Initially, only one, test, editor should be open in the workspace"
18119 );
18120 assert_eq!(
18121 test_editor_cx.entity(),
18122 editors.last().expect("Asserted len is 1").clone()
18123 );
18124 });
18125
18126 set_up_lsp_handlers(true, &mut cx);
18127 let navigated = cx
18128 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18129 .await
18130 .expect("Failed to navigate to lookup references");
18131 assert_eq!(
18132 navigated,
18133 Navigated::Yes,
18134 "Should have navigated to references as a fallback after empty GoToDefinition response"
18135 );
18136 // We should not change the selections in the existing file,
18137 // if opening another milti buffer with the references
18138 cx.assert_editor_state(
18139 &r#"fn one() {
18140 let mut a = two();
18141 }
18142
18143 fn «twoˇ»() {}"#
18144 .unindent(),
18145 );
18146 let editors = cx.update_workspace(|workspace, _, cx| {
18147 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18148 });
18149 cx.update_editor(|_, _, test_editor_cx| {
18150 assert_eq!(
18151 editors.len(),
18152 2,
18153 "After falling back to references search, we open a new editor with the results"
18154 );
18155 let references_fallback_text = editors
18156 .into_iter()
18157 .find(|new_editor| *new_editor != test_editor_cx.entity())
18158 .expect("Should have one non-test editor now")
18159 .read(test_editor_cx)
18160 .text(test_editor_cx);
18161 assert_eq!(
18162 references_fallback_text, "fn one() {\n let mut a = two();\n}",
18163 "Should use the range from the references response and not the GoToDefinition one"
18164 );
18165 });
18166}
18167
18168#[gpui::test]
18169async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
18170 init_test(cx, |_| {});
18171 cx.update(|cx| {
18172 let mut editor_settings = EditorSettings::get_global(cx).clone();
18173 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
18174 EditorSettings::override_global(editor_settings, cx);
18175 });
18176 let mut cx = EditorLspTestContext::new_rust(
18177 lsp::ServerCapabilities {
18178 definition_provider: Some(lsp::OneOf::Left(true)),
18179 references_provider: Some(lsp::OneOf::Left(true)),
18180 ..lsp::ServerCapabilities::default()
18181 },
18182 cx,
18183 )
18184 .await;
18185 let original_state = r#"fn one() {
18186 let mut a = ˇtwo();
18187 }
18188
18189 fn two() {}"#
18190 .unindent();
18191 cx.set_state(&original_state);
18192
18193 let mut go_to_definition = cx
18194 .lsp
18195 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
18196 move |_, _| async move { Ok(None) },
18197 );
18198 let _references = cx
18199 .lsp
18200 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
18201 panic!("Should not call for references with no go to definition fallback")
18202 });
18203
18204 let navigated = cx
18205 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
18206 .await
18207 .expect("Failed to navigate to lookup references");
18208 go_to_definition
18209 .next()
18210 .await
18211 .expect("Should have called the go_to_definition handler");
18212
18213 assert_eq!(
18214 navigated,
18215 Navigated::No,
18216 "Should have navigated to references as a fallback after empty GoToDefinition response"
18217 );
18218 cx.assert_editor_state(&original_state);
18219 let editors = cx.update_workspace(|workspace, _, cx| {
18220 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
18221 });
18222 cx.update_editor(|_, _, _| {
18223 assert_eq!(
18224 editors.len(),
18225 1,
18226 "After unsuccessful fallback, no other editor should have been opened"
18227 );
18228 });
18229}
18230
18231#[gpui::test]
18232async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
18233 init_test(cx, |_| {});
18234
18235 let language = Arc::new(Language::new(
18236 LanguageConfig::default(),
18237 Some(tree_sitter_rust::LANGUAGE.into()),
18238 ));
18239
18240 let text = r#"
18241 #[cfg(test)]
18242 mod tests() {
18243 #[test]
18244 fn runnable_1() {
18245 let a = 1;
18246 }
18247
18248 #[test]
18249 fn runnable_2() {
18250 let a = 1;
18251 let b = 2;
18252 }
18253 }
18254 "#
18255 .unindent();
18256
18257 let fs = FakeFs::new(cx.executor());
18258 fs.insert_file("/file.rs", Default::default()).await;
18259
18260 let project = Project::test(fs, ["/a".as_ref()], cx).await;
18261 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18262 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18263 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
18264 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
18265
18266 let editor = cx.new_window_entity(|window, cx| {
18267 Editor::new(
18268 EditorMode::full(),
18269 multi_buffer,
18270 Some(project.clone()),
18271 window,
18272 cx,
18273 )
18274 });
18275
18276 editor.update_in(cx, |editor, window, cx| {
18277 let snapshot = editor.buffer().read(cx).snapshot(cx);
18278 editor.tasks.insert(
18279 (buffer.read(cx).remote_id(), 3),
18280 RunnableTasks {
18281 templates: vec![],
18282 offset: snapshot.anchor_before(43),
18283 column: 0,
18284 extra_variables: HashMap::default(),
18285 context_range: BufferOffset(43)..BufferOffset(85),
18286 },
18287 );
18288 editor.tasks.insert(
18289 (buffer.read(cx).remote_id(), 8),
18290 RunnableTasks {
18291 templates: vec![],
18292 offset: snapshot.anchor_before(86),
18293 column: 0,
18294 extra_variables: HashMap::default(),
18295 context_range: BufferOffset(86)..BufferOffset(191),
18296 },
18297 );
18298
18299 // Test finding task when cursor is inside function body
18300 editor.change_selections(None, window, cx, |s| {
18301 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
18302 });
18303 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18304 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
18305
18306 // Test finding task when cursor is on function name
18307 editor.change_selections(None, window, cx, |s| {
18308 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
18309 });
18310 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
18311 assert_eq!(row, 8, "Should find task when cursor is on function name");
18312 });
18313}
18314
18315#[gpui::test]
18316async fn test_folding_buffers(cx: &mut TestAppContext) {
18317 init_test(cx, |_| {});
18318
18319 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18320 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
18321 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
18322
18323 let fs = FakeFs::new(cx.executor());
18324 fs.insert_tree(
18325 path!("/a"),
18326 json!({
18327 "first.rs": sample_text_1,
18328 "second.rs": sample_text_2,
18329 "third.rs": sample_text_3,
18330 }),
18331 )
18332 .await;
18333 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18334 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18335 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18336 let worktree = project.update(cx, |project, cx| {
18337 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18338 assert_eq!(worktrees.len(), 1);
18339 worktrees.pop().unwrap()
18340 });
18341 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18342
18343 let buffer_1 = project
18344 .update(cx, |project, cx| {
18345 project.open_buffer((worktree_id, "first.rs"), cx)
18346 })
18347 .await
18348 .unwrap();
18349 let buffer_2 = project
18350 .update(cx, |project, cx| {
18351 project.open_buffer((worktree_id, "second.rs"), cx)
18352 })
18353 .await
18354 .unwrap();
18355 let buffer_3 = project
18356 .update(cx, |project, cx| {
18357 project.open_buffer((worktree_id, "third.rs"), cx)
18358 })
18359 .await
18360 .unwrap();
18361
18362 let multi_buffer = cx.new(|cx| {
18363 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18364 multi_buffer.push_excerpts(
18365 buffer_1.clone(),
18366 [
18367 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18368 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18369 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18370 ],
18371 cx,
18372 );
18373 multi_buffer.push_excerpts(
18374 buffer_2.clone(),
18375 [
18376 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18377 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18378 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18379 ],
18380 cx,
18381 );
18382 multi_buffer.push_excerpts(
18383 buffer_3.clone(),
18384 [
18385 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
18386 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
18387 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
18388 ],
18389 cx,
18390 );
18391 multi_buffer
18392 });
18393 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18394 Editor::new(
18395 EditorMode::full(),
18396 multi_buffer.clone(),
18397 Some(project.clone()),
18398 window,
18399 cx,
18400 )
18401 });
18402
18403 assert_eq!(
18404 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18405 "\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",
18406 );
18407
18408 multi_buffer_editor.update(cx, |editor, cx| {
18409 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18410 });
18411 assert_eq!(
18412 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18413 "\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",
18414 "After folding the first buffer, its text should not be displayed"
18415 );
18416
18417 multi_buffer_editor.update(cx, |editor, cx| {
18418 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18419 });
18420 assert_eq!(
18421 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18422 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
18423 "After folding the second buffer, its text should not be displayed"
18424 );
18425
18426 multi_buffer_editor.update(cx, |editor, cx| {
18427 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18428 });
18429 assert_eq!(
18430 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18431 "\n\n\n\n\n",
18432 "After folding the third buffer, its text should not be displayed"
18433 );
18434
18435 // Emulate selection inside the fold logic, that should work
18436 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18437 editor
18438 .snapshot(window, cx)
18439 .next_line_boundary(Point::new(0, 4));
18440 });
18441
18442 multi_buffer_editor.update(cx, |editor, cx| {
18443 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18444 });
18445 assert_eq!(
18446 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18447 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18448 "After unfolding the second buffer, its text should be displayed"
18449 );
18450
18451 // Typing inside of buffer 1 causes that buffer to be unfolded.
18452 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18453 assert_eq!(
18454 multi_buffer
18455 .read(cx)
18456 .snapshot(cx)
18457 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
18458 .collect::<String>(),
18459 "bbbb"
18460 );
18461 editor.change_selections(None, window, cx, |selections| {
18462 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
18463 });
18464 editor.handle_input("B", window, cx);
18465 });
18466
18467 assert_eq!(
18468 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18469 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
18470 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
18471 );
18472
18473 multi_buffer_editor.update(cx, |editor, cx| {
18474 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18475 });
18476 assert_eq!(
18477 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18478 "\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",
18479 "After unfolding the all buffers, all original text should be displayed"
18480 );
18481}
18482
18483#[gpui::test]
18484async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
18485 init_test(cx, |_| {});
18486
18487 let sample_text_1 = "1111\n2222\n3333".to_string();
18488 let sample_text_2 = "4444\n5555\n6666".to_string();
18489 let sample_text_3 = "7777\n8888\n9999".to_string();
18490
18491 let fs = FakeFs::new(cx.executor());
18492 fs.insert_tree(
18493 path!("/a"),
18494 json!({
18495 "first.rs": sample_text_1,
18496 "second.rs": sample_text_2,
18497 "third.rs": sample_text_3,
18498 }),
18499 )
18500 .await;
18501 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18502 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18503 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18504 let worktree = project.update(cx, |project, cx| {
18505 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18506 assert_eq!(worktrees.len(), 1);
18507 worktrees.pop().unwrap()
18508 });
18509 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18510
18511 let buffer_1 = project
18512 .update(cx, |project, cx| {
18513 project.open_buffer((worktree_id, "first.rs"), cx)
18514 })
18515 .await
18516 .unwrap();
18517 let buffer_2 = project
18518 .update(cx, |project, cx| {
18519 project.open_buffer((worktree_id, "second.rs"), cx)
18520 })
18521 .await
18522 .unwrap();
18523 let buffer_3 = project
18524 .update(cx, |project, cx| {
18525 project.open_buffer((worktree_id, "third.rs"), cx)
18526 })
18527 .await
18528 .unwrap();
18529
18530 let multi_buffer = cx.new(|cx| {
18531 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18532 multi_buffer.push_excerpts(
18533 buffer_1.clone(),
18534 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18535 cx,
18536 );
18537 multi_buffer.push_excerpts(
18538 buffer_2.clone(),
18539 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18540 cx,
18541 );
18542 multi_buffer.push_excerpts(
18543 buffer_3.clone(),
18544 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
18545 cx,
18546 );
18547 multi_buffer
18548 });
18549
18550 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18551 Editor::new(
18552 EditorMode::full(),
18553 multi_buffer,
18554 Some(project.clone()),
18555 window,
18556 cx,
18557 )
18558 });
18559
18560 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
18561 assert_eq!(
18562 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18563 full_text,
18564 );
18565
18566 multi_buffer_editor.update(cx, |editor, cx| {
18567 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
18568 });
18569 assert_eq!(
18570 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18571 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
18572 "After folding the first buffer, its text should not be displayed"
18573 );
18574
18575 multi_buffer_editor.update(cx, |editor, cx| {
18576 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
18577 });
18578
18579 assert_eq!(
18580 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18581 "\n\n\n\n\n\n7777\n8888\n9999",
18582 "After folding the second buffer, its text should not be displayed"
18583 );
18584
18585 multi_buffer_editor.update(cx, |editor, cx| {
18586 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
18587 });
18588 assert_eq!(
18589 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18590 "\n\n\n\n\n",
18591 "After folding the third buffer, its text should not be displayed"
18592 );
18593
18594 multi_buffer_editor.update(cx, |editor, cx| {
18595 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
18596 });
18597 assert_eq!(
18598 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18599 "\n\n\n\n4444\n5555\n6666\n\n",
18600 "After unfolding the second buffer, its text should be displayed"
18601 );
18602
18603 multi_buffer_editor.update(cx, |editor, cx| {
18604 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
18605 });
18606 assert_eq!(
18607 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18608 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
18609 "After unfolding the first buffer, its text should be displayed"
18610 );
18611
18612 multi_buffer_editor.update(cx, |editor, cx| {
18613 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
18614 });
18615 assert_eq!(
18616 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18617 full_text,
18618 "After unfolding all buffers, all original text should be displayed"
18619 );
18620}
18621
18622#[gpui::test]
18623async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
18624 init_test(cx, |_| {});
18625
18626 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
18627
18628 let fs = FakeFs::new(cx.executor());
18629 fs.insert_tree(
18630 path!("/a"),
18631 json!({
18632 "main.rs": sample_text,
18633 }),
18634 )
18635 .await;
18636 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18637 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18638 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18639 let worktree = project.update(cx, |project, cx| {
18640 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
18641 assert_eq!(worktrees.len(), 1);
18642 worktrees.pop().unwrap()
18643 });
18644 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
18645
18646 let buffer_1 = project
18647 .update(cx, |project, cx| {
18648 project.open_buffer((worktree_id, "main.rs"), cx)
18649 })
18650 .await
18651 .unwrap();
18652
18653 let multi_buffer = cx.new(|cx| {
18654 let mut multi_buffer = MultiBuffer::new(ReadWrite);
18655 multi_buffer.push_excerpts(
18656 buffer_1.clone(),
18657 [ExcerptRange::new(
18658 Point::new(0, 0)
18659 ..Point::new(
18660 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
18661 0,
18662 ),
18663 )],
18664 cx,
18665 );
18666 multi_buffer
18667 });
18668 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
18669 Editor::new(
18670 EditorMode::full(),
18671 multi_buffer,
18672 Some(project.clone()),
18673 window,
18674 cx,
18675 )
18676 });
18677
18678 let selection_range = Point::new(1, 0)..Point::new(2, 0);
18679 multi_buffer_editor.update_in(cx, |editor, window, cx| {
18680 enum TestHighlight {}
18681 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
18682 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
18683 editor.highlight_text::<TestHighlight>(
18684 vec![highlight_range.clone()],
18685 HighlightStyle::color(Hsla::green()),
18686 cx,
18687 );
18688 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
18689 });
18690
18691 let full_text = format!("\n\n{sample_text}");
18692 assert_eq!(
18693 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
18694 full_text,
18695 );
18696}
18697
18698#[gpui::test]
18699async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
18700 init_test(cx, |_| {});
18701 cx.update(|cx| {
18702 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18703 "keymaps/default-linux.json",
18704 cx,
18705 )
18706 .unwrap();
18707 cx.bind_keys(default_key_bindings);
18708 });
18709
18710 let (editor, cx) = cx.add_window_view(|window, cx| {
18711 let multi_buffer = MultiBuffer::build_multi(
18712 [
18713 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18714 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18715 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18716 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18717 ],
18718 cx,
18719 );
18720 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18721
18722 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18723 // fold all but the second buffer, so that we test navigating between two
18724 // adjacent folded buffers, as well as folded buffers at the start and
18725 // end the multibuffer
18726 editor.fold_buffer(buffer_ids[0], cx);
18727 editor.fold_buffer(buffer_ids[2], cx);
18728 editor.fold_buffer(buffer_ids[3], cx);
18729
18730 editor
18731 });
18732 cx.simulate_resize(size(px(1000.), px(1000.)));
18733
18734 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18735 cx.assert_excerpts_with_selections(indoc! {"
18736 [EXCERPT]
18737 ˇ[FOLDED]
18738 [EXCERPT]
18739 a1
18740 b1
18741 [EXCERPT]
18742 [FOLDED]
18743 [EXCERPT]
18744 [FOLDED]
18745 "
18746 });
18747 cx.simulate_keystroke("down");
18748 cx.assert_excerpts_with_selections(indoc! {"
18749 [EXCERPT]
18750 [FOLDED]
18751 [EXCERPT]
18752 ˇa1
18753 b1
18754 [EXCERPT]
18755 [FOLDED]
18756 [EXCERPT]
18757 [FOLDED]
18758 "
18759 });
18760 cx.simulate_keystroke("down");
18761 cx.assert_excerpts_with_selections(indoc! {"
18762 [EXCERPT]
18763 [FOLDED]
18764 [EXCERPT]
18765 a1
18766 ˇb1
18767 [EXCERPT]
18768 [FOLDED]
18769 [EXCERPT]
18770 [FOLDED]
18771 "
18772 });
18773 cx.simulate_keystroke("down");
18774 cx.assert_excerpts_with_selections(indoc! {"
18775 [EXCERPT]
18776 [FOLDED]
18777 [EXCERPT]
18778 a1
18779 b1
18780 ˇ[EXCERPT]
18781 [FOLDED]
18782 [EXCERPT]
18783 [FOLDED]
18784 "
18785 });
18786 cx.simulate_keystroke("down");
18787 cx.assert_excerpts_with_selections(indoc! {"
18788 [EXCERPT]
18789 [FOLDED]
18790 [EXCERPT]
18791 a1
18792 b1
18793 [EXCERPT]
18794 ˇ[FOLDED]
18795 [EXCERPT]
18796 [FOLDED]
18797 "
18798 });
18799 for _ in 0..5 {
18800 cx.simulate_keystroke("down");
18801 cx.assert_excerpts_with_selections(indoc! {"
18802 [EXCERPT]
18803 [FOLDED]
18804 [EXCERPT]
18805 a1
18806 b1
18807 [EXCERPT]
18808 [FOLDED]
18809 [EXCERPT]
18810 ˇ[FOLDED]
18811 "
18812 });
18813 }
18814
18815 cx.simulate_keystroke("up");
18816 cx.assert_excerpts_with_selections(indoc! {"
18817 [EXCERPT]
18818 [FOLDED]
18819 [EXCERPT]
18820 a1
18821 b1
18822 [EXCERPT]
18823 ˇ[FOLDED]
18824 [EXCERPT]
18825 [FOLDED]
18826 "
18827 });
18828 cx.simulate_keystroke("up");
18829 cx.assert_excerpts_with_selections(indoc! {"
18830 [EXCERPT]
18831 [FOLDED]
18832 [EXCERPT]
18833 a1
18834 b1
18835 ˇ[EXCERPT]
18836 [FOLDED]
18837 [EXCERPT]
18838 [FOLDED]
18839 "
18840 });
18841 cx.simulate_keystroke("up");
18842 cx.assert_excerpts_with_selections(indoc! {"
18843 [EXCERPT]
18844 [FOLDED]
18845 [EXCERPT]
18846 a1
18847 ˇb1
18848 [EXCERPT]
18849 [FOLDED]
18850 [EXCERPT]
18851 [FOLDED]
18852 "
18853 });
18854 cx.simulate_keystroke("up");
18855 cx.assert_excerpts_with_selections(indoc! {"
18856 [EXCERPT]
18857 [FOLDED]
18858 [EXCERPT]
18859 ˇa1
18860 b1
18861 [EXCERPT]
18862 [FOLDED]
18863 [EXCERPT]
18864 [FOLDED]
18865 "
18866 });
18867 for _ in 0..5 {
18868 cx.simulate_keystroke("up");
18869 cx.assert_excerpts_with_selections(indoc! {"
18870 [EXCERPT]
18871 ˇ[FOLDED]
18872 [EXCERPT]
18873 a1
18874 b1
18875 [EXCERPT]
18876 [FOLDED]
18877 [EXCERPT]
18878 [FOLDED]
18879 "
18880 });
18881 }
18882}
18883
18884#[gpui::test]
18885async fn test_inline_completion_text(cx: &mut TestAppContext) {
18886 init_test(cx, |_| {});
18887
18888 // Simple insertion
18889 assert_highlighted_edits(
18890 "Hello, world!",
18891 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18892 true,
18893 cx,
18894 |highlighted_edits, cx| {
18895 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18896 assert_eq!(highlighted_edits.highlights.len(), 1);
18897 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18898 assert_eq!(
18899 highlighted_edits.highlights[0].1.background_color,
18900 Some(cx.theme().status().created_background)
18901 );
18902 },
18903 )
18904 .await;
18905
18906 // Replacement
18907 assert_highlighted_edits(
18908 "This is a test.",
18909 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18910 false,
18911 cx,
18912 |highlighted_edits, cx| {
18913 assert_eq!(highlighted_edits.text, "That is a test.");
18914 assert_eq!(highlighted_edits.highlights.len(), 1);
18915 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18916 assert_eq!(
18917 highlighted_edits.highlights[0].1.background_color,
18918 Some(cx.theme().status().created_background)
18919 );
18920 },
18921 )
18922 .await;
18923
18924 // Multiple edits
18925 assert_highlighted_edits(
18926 "Hello, world!",
18927 vec![
18928 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18929 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18930 ],
18931 false,
18932 cx,
18933 |highlighted_edits, cx| {
18934 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18935 assert_eq!(highlighted_edits.highlights.len(), 2);
18936 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18937 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18938 assert_eq!(
18939 highlighted_edits.highlights[0].1.background_color,
18940 Some(cx.theme().status().created_background)
18941 );
18942 assert_eq!(
18943 highlighted_edits.highlights[1].1.background_color,
18944 Some(cx.theme().status().created_background)
18945 );
18946 },
18947 )
18948 .await;
18949
18950 // Multiple lines with edits
18951 assert_highlighted_edits(
18952 "First line\nSecond line\nThird line\nFourth line",
18953 vec![
18954 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18955 (
18956 Point::new(2, 0)..Point::new(2, 10),
18957 "New third line".to_string(),
18958 ),
18959 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18960 ],
18961 false,
18962 cx,
18963 |highlighted_edits, cx| {
18964 assert_eq!(
18965 highlighted_edits.text,
18966 "Second modified\nNew third line\nFourth updated line"
18967 );
18968 assert_eq!(highlighted_edits.highlights.len(), 3);
18969 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18970 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18971 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18972 for highlight in &highlighted_edits.highlights {
18973 assert_eq!(
18974 highlight.1.background_color,
18975 Some(cx.theme().status().created_background)
18976 );
18977 }
18978 },
18979 )
18980 .await;
18981}
18982
18983#[gpui::test]
18984async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18985 init_test(cx, |_| {});
18986
18987 // Deletion
18988 assert_highlighted_edits(
18989 "Hello, world!",
18990 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18991 true,
18992 cx,
18993 |highlighted_edits, cx| {
18994 assert_eq!(highlighted_edits.text, "Hello, world!");
18995 assert_eq!(highlighted_edits.highlights.len(), 1);
18996 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18997 assert_eq!(
18998 highlighted_edits.highlights[0].1.background_color,
18999 Some(cx.theme().status().deleted_background)
19000 );
19001 },
19002 )
19003 .await;
19004
19005 // Insertion
19006 assert_highlighted_edits(
19007 "Hello, world!",
19008 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
19009 true,
19010 cx,
19011 |highlighted_edits, cx| {
19012 assert_eq!(highlighted_edits.highlights.len(), 1);
19013 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
19014 assert_eq!(
19015 highlighted_edits.highlights[0].1.background_color,
19016 Some(cx.theme().status().created_background)
19017 );
19018 },
19019 )
19020 .await;
19021}
19022
19023async fn assert_highlighted_edits(
19024 text: &str,
19025 edits: Vec<(Range<Point>, String)>,
19026 include_deletions: bool,
19027 cx: &mut TestAppContext,
19028 assertion_fn: impl Fn(HighlightedText, &App),
19029) {
19030 let window = cx.add_window(|window, cx| {
19031 let buffer = MultiBuffer::build_simple(text, cx);
19032 Editor::new(EditorMode::full(), buffer, None, window, cx)
19033 });
19034 let cx = &mut VisualTestContext::from_window(*window, cx);
19035
19036 let (buffer, snapshot) = window
19037 .update(cx, |editor, _window, cx| {
19038 (
19039 editor.buffer().clone(),
19040 editor.buffer().read(cx).snapshot(cx),
19041 )
19042 })
19043 .unwrap();
19044
19045 let edits = edits
19046 .into_iter()
19047 .map(|(range, edit)| {
19048 (
19049 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
19050 edit,
19051 )
19052 })
19053 .collect::<Vec<_>>();
19054
19055 let text_anchor_edits = edits
19056 .clone()
19057 .into_iter()
19058 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
19059 .collect::<Vec<_>>();
19060
19061 let edit_preview = window
19062 .update(cx, |_, _window, cx| {
19063 buffer
19064 .read(cx)
19065 .as_singleton()
19066 .unwrap()
19067 .read(cx)
19068 .preview_edits(text_anchor_edits.into(), cx)
19069 })
19070 .unwrap()
19071 .await;
19072
19073 cx.update(|_window, cx| {
19074 let highlighted_edits = inline_completion_edit_text(
19075 &snapshot.as_singleton().unwrap().2,
19076 &edits,
19077 &edit_preview,
19078 include_deletions,
19079 cx,
19080 );
19081 assertion_fn(highlighted_edits, cx)
19082 });
19083}
19084
19085#[track_caller]
19086fn assert_breakpoint(
19087 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
19088 path: &Arc<Path>,
19089 expected: Vec<(u32, Breakpoint)>,
19090) {
19091 if expected.len() == 0usize {
19092 assert!(!breakpoints.contains_key(path), "{}", path.display());
19093 } else {
19094 let mut breakpoint = breakpoints
19095 .get(path)
19096 .unwrap()
19097 .into_iter()
19098 .map(|breakpoint| {
19099 (
19100 breakpoint.row,
19101 Breakpoint {
19102 message: breakpoint.message.clone(),
19103 state: breakpoint.state,
19104 condition: breakpoint.condition.clone(),
19105 hit_condition: breakpoint.hit_condition.clone(),
19106 },
19107 )
19108 })
19109 .collect::<Vec<_>>();
19110
19111 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
19112
19113 assert_eq!(expected, breakpoint);
19114 }
19115}
19116
19117fn add_log_breakpoint_at_cursor(
19118 editor: &mut Editor,
19119 log_message: &str,
19120 window: &mut Window,
19121 cx: &mut Context<Editor>,
19122) {
19123 let (anchor, bp) = editor
19124 .breakpoints_at_cursors(window, cx)
19125 .first()
19126 .and_then(|(anchor, bp)| {
19127 if let Some(bp) = bp {
19128 Some((*anchor, bp.clone()))
19129 } else {
19130 None
19131 }
19132 })
19133 .unwrap_or_else(|| {
19134 let cursor_position: Point = editor.selections.newest(cx).head();
19135
19136 let breakpoint_position = editor
19137 .snapshot(window, cx)
19138 .display_snapshot
19139 .buffer_snapshot
19140 .anchor_before(Point::new(cursor_position.row, 0));
19141
19142 (breakpoint_position, Breakpoint::new_log(&log_message))
19143 });
19144
19145 editor.edit_breakpoint_at_anchor(
19146 anchor,
19147 bp,
19148 BreakpointEditAction::EditLogMessage(log_message.into()),
19149 cx,
19150 );
19151}
19152
19153#[gpui::test]
19154async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
19155 init_test(cx, |_| {});
19156
19157 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19158 let fs = FakeFs::new(cx.executor());
19159 fs.insert_tree(
19160 path!("/a"),
19161 json!({
19162 "main.rs": sample_text,
19163 }),
19164 )
19165 .await;
19166 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19167 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19168 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19169
19170 let fs = FakeFs::new(cx.executor());
19171 fs.insert_tree(
19172 path!("/a"),
19173 json!({
19174 "main.rs": sample_text,
19175 }),
19176 )
19177 .await;
19178 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19179 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19180 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19181 let worktree_id = workspace
19182 .update(cx, |workspace, _window, cx| {
19183 workspace.project().update(cx, |project, cx| {
19184 project.worktrees(cx).next().unwrap().read(cx).id()
19185 })
19186 })
19187 .unwrap();
19188
19189 let buffer = project
19190 .update(cx, |project, cx| {
19191 project.open_buffer((worktree_id, "main.rs"), cx)
19192 })
19193 .await
19194 .unwrap();
19195
19196 let (editor, cx) = cx.add_window_view(|window, cx| {
19197 Editor::new(
19198 EditorMode::full(),
19199 MultiBuffer::build_from_buffer(buffer, cx),
19200 Some(project.clone()),
19201 window,
19202 cx,
19203 )
19204 });
19205
19206 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19207 let abs_path = project.read_with(cx, |project, cx| {
19208 project
19209 .absolute_path(&project_path, cx)
19210 .map(|path_buf| Arc::from(path_buf.to_owned()))
19211 .unwrap()
19212 });
19213
19214 // assert we can add breakpoint on the first line
19215 editor.update_in(cx, |editor, window, cx| {
19216 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19217 editor.move_to_end(&MoveToEnd, window, cx);
19218 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19219 });
19220
19221 let breakpoints = editor.update(cx, |editor, cx| {
19222 editor
19223 .breakpoint_store()
19224 .as_ref()
19225 .unwrap()
19226 .read(cx)
19227 .all_source_breakpoints(cx)
19228 .clone()
19229 });
19230
19231 assert_eq!(1, breakpoints.len());
19232 assert_breakpoint(
19233 &breakpoints,
19234 &abs_path,
19235 vec![
19236 (0, Breakpoint::new_standard()),
19237 (3, Breakpoint::new_standard()),
19238 ],
19239 );
19240
19241 editor.update_in(cx, |editor, window, cx| {
19242 editor.move_to_beginning(&MoveToBeginning, window, cx);
19243 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19244 });
19245
19246 let breakpoints = editor.update(cx, |editor, cx| {
19247 editor
19248 .breakpoint_store()
19249 .as_ref()
19250 .unwrap()
19251 .read(cx)
19252 .all_source_breakpoints(cx)
19253 .clone()
19254 });
19255
19256 assert_eq!(1, breakpoints.len());
19257 assert_breakpoint(
19258 &breakpoints,
19259 &abs_path,
19260 vec![(3, Breakpoint::new_standard())],
19261 );
19262
19263 editor.update_in(cx, |editor, window, cx| {
19264 editor.move_to_end(&MoveToEnd, window, cx);
19265 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19266 });
19267
19268 let breakpoints = editor.update(cx, |editor, cx| {
19269 editor
19270 .breakpoint_store()
19271 .as_ref()
19272 .unwrap()
19273 .read(cx)
19274 .all_source_breakpoints(cx)
19275 .clone()
19276 });
19277
19278 assert_eq!(0, breakpoints.len());
19279 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19280}
19281
19282#[gpui::test]
19283async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
19284 init_test(cx, |_| {});
19285
19286 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19287
19288 let fs = FakeFs::new(cx.executor());
19289 fs.insert_tree(
19290 path!("/a"),
19291 json!({
19292 "main.rs": sample_text,
19293 }),
19294 )
19295 .await;
19296 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19297 let (workspace, cx) =
19298 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19299
19300 let worktree_id = workspace.update(cx, |workspace, cx| {
19301 workspace.project().update(cx, |project, cx| {
19302 project.worktrees(cx).next().unwrap().read(cx).id()
19303 })
19304 });
19305
19306 let buffer = project
19307 .update(cx, |project, cx| {
19308 project.open_buffer((worktree_id, "main.rs"), cx)
19309 })
19310 .await
19311 .unwrap();
19312
19313 let (editor, cx) = cx.add_window_view(|window, cx| {
19314 Editor::new(
19315 EditorMode::full(),
19316 MultiBuffer::build_from_buffer(buffer, cx),
19317 Some(project.clone()),
19318 window,
19319 cx,
19320 )
19321 });
19322
19323 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19324 let abs_path = project.read_with(cx, |project, cx| {
19325 project
19326 .absolute_path(&project_path, cx)
19327 .map(|path_buf| Arc::from(path_buf.to_owned()))
19328 .unwrap()
19329 });
19330
19331 editor.update_in(cx, |editor, window, cx| {
19332 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19333 });
19334
19335 let breakpoints = editor.update(cx, |editor, cx| {
19336 editor
19337 .breakpoint_store()
19338 .as_ref()
19339 .unwrap()
19340 .read(cx)
19341 .all_source_breakpoints(cx)
19342 .clone()
19343 });
19344
19345 assert_breakpoint(
19346 &breakpoints,
19347 &abs_path,
19348 vec![(0, Breakpoint::new_log("hello world"))],
19349 );
19350
19351 // Removing a log message from a log breakpoint should remove it
19352 editor.update_in(cx, |editor, window, cx| {
19353 add_log_breakpoint_at_cursor(editor, "", window, cx);
19354 });
19355
19356 let breakpoints = editor.update(cx, |editor, cx| {
19357 editor
19358 .breakpoint_store()
19359 .as_ref()
19360 .unwrap()
19361 .read(cx)
19362 .all_source_breakpoints(cx)
19363 .clone()
19364 });
19365
19366 assert_breakpoint(&breakpoints, &abs_path, vec![]);
19367
19368 editor.update_in(cx, |editor, window, cx| {
19369 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19370 editor.move_to_end(&MoveToEnd, window, cx);
19371 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19372 // Not adding a log message to a standard breakpoint shouldn't remove it
19373 add_log_breakpoint_at_cursor(editor, "", window, cx);
19374 });
19375
19376 let breakpoints = editor.update(cx, |editor, cx| {
19377 editor
19378 .breakpoint_store()
19379 .as_ref()
19380 .unwrap()
19381 .read(cx)
19382 .all_source_breakpoints(cx)
19383 .clone()
19384 });
19385
19386 assert_breakpoint(
19387 &breakpoints,
19388 &abs_path,
19389 vec![
19390 (0, Breakpoint::new_standard()),
19391 (3, Breakpoint::new_standard()),
19392 ],
19393 );
19394
19395 editor.update_in(cx, |editor, window, cx| {
19396 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
19397 });
19398
19399 let breakpoints = editor.update(cx, |editor, cx| {
19400 editor
19401 .breakpoint_store()
19402 .as_ref()
19403 .unwrap()
19404 .read(cx)
19405 .all_source_breakpoints(cx)
19406 .clone()
19407 });
19408
19409 assert_breakpoint(
19410 &breakpoints,
19411 &abs_path,
19412 vec![
19413 (0, Breakpoint::new_standard()),
19414 (3, Breakpoint::new_log("hello world")),
19415 ],
19416 );
19417
19418 editor.update_in(cx, |editor, window, cx| {
19419 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
19420 });
19421
19422 let breakpoints = editor.update(cx, |editor, cx| {
19423 editor
19424 .breakpoint_store()
19425 .as_ref()
19426 .unwrap()
19427 .read(cx)
19428 .all_source_breakpoints(cx)
19429 .clone()
19430 });
19431
19432 assert_breakpoint(
19433 &breakpoints,
19434 &abs_path,
19435 vec![
19436 (0, Breakpoint::new_standard()),
19437 (3, Breakpoint::new_log("hello Earth!!")),
19438 ],
19439 );
19440}
19441
19442/// This also tests that Editor::breakpoint_at_cursor_head is working properly
19443/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
19444/// or when breakpoints were placed out of order. This tests for a regression too
19445#[gpui::test]
19446async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
19447 init_test(cx, |_| {});
19448
19449 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
19450 let fs = FakeFs::new(cx.executor());
19451 fs.insert_tree(
19452 path!("/a"),
19453 json!({
19454 "main.rs": sample_text,
19455 }),
19456 )
19457 .await;
19458 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19459 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19460 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19461
19462 let fs = FakeFs::new(cx.executor());
19463 fs.insert_tree(
19464 path!("/a"),
19465 json!({
19466 "main.rs": sample_text,
19467 }),
19468 )
19469 .await;
19470 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19471 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19472 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19473 let worktree_id = workspace
19474 .update(cx, |workspace, _window, cx| {
19475 workspace.project().update(cx, |project, cx| {
19476 project.worktrees(cx).next().unwrap().read(cx).id()
19477 })
19478 })
19479 .unwrap();
19480
19481 let buffer = project
19482 .update(cx, |project, cx| {
19483 project.open_buffer((worktree_id, "main.rs"), cx)
19484 })
19485 .await
19486 .unwrap();
19487
19488 let (editor, cx) = cx.add_window_view(|window, cx| {
19489 Editor::new(
19490 EditorMode::full(),
19491 MultiBuffer::build_from_buffer(buffer, cx),
19492 Some(project.clone()),
19493 window,
19494 cx,
19495 )
19496 });
19497
19498 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
19499 let abs_path = project.read_with(cx, |project, cx| {
19500 project
19501 .absolute_path(&project_path, cx)
19502 .map(|path_buf| Arc::from(path_buf.to_owned()))
19503 .unwrap()
19504 });
19505
19506 // assert we can add breakpoint on the first line
19507 editor.update_in(cx, |editor, window, cx| {
19508 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19509 editor.move_to_end(&MoveToEnd, window, cx);
19510 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19511 editor.move_up(&MoveUp, window, cx);
19512 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
19513 });
19514
19515 let breakpoints = editor.update(cx, |editor, cx| {
19516 editor
19517 .breakpoint_store()
19518 .as_ref()
19519 .unwrap()
19520 .read(cx)
19521 .all_source_breakpoints(cx)
19522 .clone()
19523 });
19524
19525 assert_eq!(1, breakpoints.len());
19526 assert_breakpoint(
19527 &breakpoints,
19528 &abs_path,
19529 vec![
19530 (0, Breakpoint::new_standard()),
19531 (2, Breakpoint::new_standard()),
19532 (3, Breakpoint::new_standard()),
19533 ],
19534 );
19535
19536 editor.update_in(cx, |editor, window, cx| {
19537 editor.move_to_beginning(&MoveToBeginning, window, cx);
19538 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19539 editor.move_to_end(&MoveToEnd, window, cx);
19540 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19541 // Disabling a breakpoint that doesn't exist should do nothing
19542 editor.move_up(&MoveUp, window, cx);
19543 editor.move_up(&MoveUp, window, cx);
19544 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19545 });
19546
19547 let breakpoints = editor.update(cx, |editor, cx| {
19548 editor
19549 .breakpoint_store()
19550 .as_ref()
19551 .unwrap()
19552 .read(cx)
19553 .all_source_breakpoints(cx)
19554 .clone()
19555 });
19556
19557 let disable_breakpoint = {
19558 let mut bp = Breakpoint::new_standard();
19559 bp.state = BreakpointState::Disabled;
19560 bp
19561 };
19562
19563 assert_eq!(1, breakpoints.len());
19564 assert_breakpoint(
19565 &breakpoints,
19566 &abs_path,
19567 vec![
19568 (0, disable_breakpoint.clone()),
19569 (2, Breakpoint::new_standard()),
19570 (3, disable_breakpoint.clone()),
19571 ],
19572 );
19573
19574 editor.update_in(cx, |editor, window, cx| {
19575 editor.move_to_beginning(&MoveToBeginning, window, cx);
19576 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19577 editor.move_to_end(&MoveToEnd, window, cx);
19578 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
19579 editor.move_up(&MoveUp, window, cx);
19580 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
19581 });
19582
19583 let breakpoints = editor.update(cx, |editor, cx| {
19584 editor
19585 .breakpoint_store()
19586 .as_ref()
19587 .unwrap()
19588 .read(cx)
19589 .all_source_breakpoints(cx)
19590 .clone()
19591 });
19592
19593 assert_eq!(1, breakpoints.len());
19594 assert_breakpoint(
19595 &breakpoints,
19596 &abs_path,
19597 vec![
19598 (0, Breakpoint::new_standard()),
19599 (2, disable_breakpoint),
19600 (3, Breakpoint::new_standard()),
19601 ],
19602 );
19603}
19604
19605#[gpui::test]
19606async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
19607 init_test(cx, |_| {});
19608 let capabilities = lsp::ServerCapabilities {
19609 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
19610 prepare_provider: Some(true),
19611 work_done_progress_options: Default::default(),
19612 })),
19613 ..Default::default()
19614 };
19615 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19616
19617 cx.set_state(indoc! {"
19618 struct Fˇoo {}
19619 "});
19620
19621 cx.update_editor(|editor, _, cx| {
19622 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19623 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19624 editor.highlight_background::<DocumentHighlightRead>(
19625 &[highlight_range],
19626 |c| c.editor_document_highlight_read_background,
19627 cx,
19628 );
19629 });
19630
19631 let mut prepare_rename_handler = cx
19632 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
19633 move |_, _, _| async move {
19634 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
19635 start: lsp::Position {
19636 line: 0,
19637 character: 7,
19638 },
19639 end: lsp::Position {
19640 line: 0,
19641 character: 10,
19642 },
19643 })))
19644 },
19645 );
19646 let prepare_rename_task = cx
19647 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19648 .expect("Prepare rename was not started");
19649 prepare_rename_handler.next().await.unwrap();
19650 prepare_rename_task.await.expect("Prepare rename failed");
19651
19652 let mut rename_handler =
19653 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19654 let edit = lsp::TextEdit {
19655 range: lsp::Range {
19656 start: lsp::Position {
19657 line: 0,
19658 character: 7,
19659 },
19660 end: lsp::Position {
19661 line: 0,
19662 character: 10,
19663 },
19664 },
19665 new_text: "FooRenamed".to_string(),
19666 };
19667 Ok(Some(lsp::WorkspaceEdit::new(
19668 // Specify the same edit twice
19669 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
19670 )))
19671 });
19672 let rename_task = cx
19673 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19674 .expect("Confirm rename was not started");
19675 rename_handler.next().await.unwrap();
19676 rename_task.await.expect("Confirm rename failed");
19677 cx.run_until_parked();
19678
19679 // Despite two edits, only one is actually applied as those are identical
19680 cx.assert_editor_state(indoc! {"
19681 struct FooRenamedˇ {}
19682 "});
19683}
19684
19685#[gpui::test]
19686async fn test_rename_without_prepare(cx: &mut TestAppContext) {
19687 init_test(cx, |_| {});
19688 // These capabilities indicate that the server does not support prepare rename.
19689 let capabilities = lsp::ServerCapabilities {
19690 rename_provider: Some(lsp::OneOf::Left(true)),
19691 ..Default::default()
19692 };
19693 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
19694
19695 cx.set_state(indoc! {"
19696 struct Fˇoo {}
19697 "});
19698
19699 cx.update_editor(|editor, _window, cx| {
19700 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
19701 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19702 editor.highlight_background::<DocumentHighlightRead>(
19703 &[highlight_range],
19704 |c| c.editor_document_highlight_read_background,
19705 cx,
19706 );
19707 });
19708
19709 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19710 .expect("Prepare rename was not started")
19711 .await
19712 .expect("Prepare rename failed");
19713
19714 let mut rename_handler =
19715 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19716 let edit = lsp::TextEdit {
19717 range: lsp::Range {
19718 start: lsp::Position {
19719 line: 0,
19720 character: 7,
19721 },
19722 end: lsp::Position {
19723 line: 0,
19724 character: 10,
19725 },
19726 },
19727 new_text: "FooRenamed".to_string(),
19728 };
19729 Ok(Some(lsp::WorkspaceEdit::new(
19730 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19731 )))
19732 });
19733 let rename_task = cx
19734 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19735 .expect("Confirm rename was not started");
19736 rename_handler.next().await.unwrap();
19737 rename_task.await.expect("Confirm rename failed");
19738 cx.run_until_parked();
19739
19740 // Correct range is renamed, as `surrounding_word` is used to find it.
19741 cx.assert_editor_state(indoc! {"
19742 struct FooRenamedˇ {}
19743 "});
19744}
19745
19746#[gpui::test]
19747async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19748 init_test(cx, |_| {});
19749 let mut cx = EditorTestContext::new(cx).await;
19750
19751 let language = Arc::new(
19752 Language::new(
19753 LanguageConfig::default(),
19754 Some(tree_sitter_html::LANGUAGE.into()),
19755 )
19756 .with_brackets_query(
19757 r#"
19758 ("<" @open "/>" @close)
19759 ("</" @open ">" @close)
19760 ("<" @open ">" @close)
19761 ("\"" @open "\"" @close)
19762 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19763 "#,
19764 )
19765 .unwrap(),
19766 );
19767 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19768
19769 cx.set_state(indoc! {"
19770 <span>ˇ</span>
19771 "});
19772 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19773 cx.assert_editor_state(indoc! {"
19774 <span>
19775 ˇ
19776 </span>
19777 "});
19778
19779 cx.set_state(indoc! {"
19780 <span><span></span>ˇ</span>
19781 "});
19782 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19783 cx.assert_editor_state(indoc! {"
19784 <span><span></span>
19785 ˇ</span>
19786 "});
19787
19788 cx.set_state(indoc! {"
19789 <span>ˇ
19790 </span>
19791 "});
19792 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19793 cx.assert_editor_state(indoc! {"
19794 <span>
19795 ˇ
19796 </span>
19797 "});
19798}
19799
19800#[gpui::test(iterations = 10)]
19801async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19802 init_test(cx, |_| {});
19803
19804 let fs = FakeFs::new(cx.executor());
19805 fs.insert_tree(
19806 path!("/dir"),
19807 json!({
19808 "a.ts": "a",
19809 }),
19810 )
19811 .await;
19812
19813 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19814 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19815 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19816
19817 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19818 language_registry.add(Arc::new(Language::new(
19819 LanguageConfig {
19820 name: "TypeScript".into(),
19821 matcher: LanguageMatcher {
19822 path_suffixes: vec!["ts".to_string()],
19823 ..Default::default()
19824 },
19825 ..Default::default()
19826 },
19827 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19828 )));
19829 let mut fake_language_servers = language_registry.register_fake_lsp(
19830 "TypeScript",
19831 FakeLspAdapter {
19832 capabilities: lsp::ServerCapabilities {
19833 code_lens_provider: Some(lsp::CodeLensOptions {
19834 resolve_provider: Some(true),
19835 }),
19836 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19837 commands: vec!["_the/command".to_string()],
19838 ..lsp::ExecuteCommandOptions::default()
19839 }),
19840 ..lsp::ServerCapabilities::default()
19841 },
19842 ..FakeLspAdapter::default()
19843 },
19844 );
19845
19846 let (buffer, _handle) = project
19847 .update(cx, |p, cx| {
19848 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19849 })
19850 .await
19851 .unwrap();
19852 cx.executor().run_until_parked();
19853
19854 let fake_server = fake_language_servers.next().await.unwrap();
19855
19856 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19857 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19858 drop(buffer_snapshot);
19859 let actions = cx
19860 .update_window(*workspace, |_, window, cx| {
19861 project.code_actions(&buffer, anchor..anchor, window, cx)
19862 })
19863 .unwrap();
19864
19865 fake_server
19866 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19867 Ok(Some(vec![
19868 lsp::CodeLens {
19869 range: lsp::Range::default(),
19870 command: Some(lsp::Command {
19871 title: "Code lens command".to_owned(),
19872 command: "_the/command".to_owned(),
19873 arguments: None,
19874 }),
19875 data: None,
19876 },
19877 lsp::CodeLens {
19878 range: lsp::Range::default(),
19879 command: Some(lsp::Command {
19880 title: "Command not in capabilities".to_owned(),
19881 command: "not in capabilities".to_owned(),
19882 arguments: None,
19883 }),
19884 data: None,
19885 },
19886 lsp::CodeLens {
19887 range: lsp::Range {
19888 start: lsp::Position {
19889 line: 1,
19890 character: 1,
19891 },
19892 end: lsp::Position {
19893 line: 1,
19894 character: 1,
19895 },
19896 },
19897 command: Some(lsp::Command {
19898 title: "Command not in range".to_owned(),
19899 command: "_the/command".to_owned(),
19900 arguments: None,
19901 }),
19902 data: None,
19903 },
19904 ]))
19905 })
19906 .next()
19907 .await;
19908
19909 let actions = actions.await.unwrap();
19910 assert_eq!(
19911 actions.len(),
19912 1,
19913 "Should have only one valid action for the 0..0 range"
19914 );
19915 let action = actions[0].clone();
19916 let apply = project.update(cx, |project, cx| {
19917 project.apply_code_action(buffer.clone(), action, true, cx)
19918 });
19919
19920 // Resolving the code action does not populate its edits. In absence of
19921 // edits, we must execute the given command.
19922 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19923 |mut lens, _| async move {
19924 let lens_command = lens.command.as_mut().expect("should have a command");
19925 assert_eq!(lens_command.title, "Code lens command");
19926 lens_command.arguments = Some(vec![json!("the-argument")]);
19927 Ok(lens)
19928 },
19929 );
19930
19931 // While executing the command, the language server sends the editor
19932 // a `workspaceEdit` request.
19933 fake_server
19934 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19935 let fake = fake_server.clone();
19936 move |params, _| {
19937 assert_eq!(params.command, "_the/command");
19938 let fake = fake.clone();
19939 async move {
19940 fake.server
19941 .request::<lsp::request::ApplyWorkspaceEdit>(
19942 lsp::ApplyWorkspaceEditParams {
19943 label: None,
19944 edit: lsp::WorkspaceEdit {
19945 changes: Some(
19946 [(
19947 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19948 vec![lsp::TextEdit {
19949 range: lsp::Range::new(
19950 lsp::Position::new(0, 0),
19951 lsp::Position::new(0, 0),
19952 ),
19953 new_text: "X".into(),
19954 }],
19955 )]
19956 .into_iter()
19957 .collect(),
19958 ),
19959 ..Default::default()
19960 },
19961 },
19962 )
19963 .await
19964 .into_response()
19965 .unwrap();
19966 Ok(Some(json!(null)))
19967 }
19968 }
19969 })
19970 .next()
19971 .await;
19972
19973 // Applying the code lens command returns a project transaction containing the edits
19974 // sent by the language server in its `workspaceEdit` request.
19975 let transaction = apply.await.unwrap();
19976 assert!(transaction.0.contains_key(&buffer));
19977 buffer.update(cx, |buffer, cx| {
19978 assert_eq!(buffer.text(), "Xa");
19979 buffer.undo(cx);
19980 assert_eq!(buffer.text(), "a");
19981 });
19982}
19983
19984#[gpui::test]
19985async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19986 init_test(cx, |_| {});
19987
19988 let fs = FakeFs::new(cx.executor());
19989 let main_text = r#"fn main() {
19990println!("1");
19991println!("2");
19992println!("3");
19993println!("4");
19994println!("5");
19995}"#;
19996 let lib_text = "mod foo {}";
19997 fs.insert_tree(
19998 path!("/a"),
19999 json!({
20000 "lib.rs": lib_text,
20001 "main.rs": main_text,
20002 }),
20003 )
20004 .await;
20005
20006 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20007 let (workspace, cx) =
20008 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20009 let worktree_id = workspace.update(cx, |workspace, cx| {
20010 workspace.project().update(cx, |project, cx| {
20011 project.worktrees(cx).next().unwrap().read(cx).id()
20012 })
20013 });
20014
20015 let expected_ranges = vec![
20016 Point::new(0, 0)..Point::new(0, 0),
20017 Point::new(1, 0)..Point::new(1, 1),
20018 Point::new(2, 0)..Point::new(2, 2),
20019 Point::new(3, 0)..Point::new(3, 3),
20020 ];
20021
20022 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20023 let editor_1 = workspace
20024 .update_in(cx, |workspace, window, cx| {
20025 workspace.open_path(
20026 (worktree_id, "main.rs"),
20027 Some(pane_1.downgrade()),
20028 true,
20029 window,
20030 cx,
20031 )
20032 })
20033 .unwrap()
20034 .await
20035 .downcast::<Editor>()
20036 .unwrap();
20037 pane_1.update(cx, |pane, cx| {
20038 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20039 open_editor.update(cx, |editor, cx| {
20040 assert_eq!(
20041 editor.display_text(cx),
20042 main_text,
20043 "Original main.rs text on initial open",
20044 );
20045 assert_eq!(
20046 editor
20047 .selections
20048 .all::<Point>(cx)
20049 .into_iter()
20050 .map(|s| s.range())
20051 .collect::<Vec<_>>(),
20052 vec![Point::zero()..Point::zero()],
20053 "Default selections on initial open",
20054 );
20055 })
20056 });
20057 editor_1.update_in(cx, |editor, window, cx| {
20058 editor.change_selections(None, window, cx, |s| {
20059 s.select_ranges(expected_ranges.clone());
20060 });
20061 });
20062
20063 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
20064 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
20065 });
20066 let editor_2 = workspace
20067 .update_in(cx, |workspace, window, cx| {
20068 workspace.open_path(
20069 (worktree_id, "main.rs"),
20070 Some(pane_2.downgrade()),
20071 true,
20072 window,
20073 cx,
20074 )
20075 })
20076 .unwrap()
20077 .await
20078 .downcast::<Editor>()
20079 .unwrap();
20080 pane_2.update(cx, |pane, cx| {
20081 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20082 open_editor.update(cx, |editor, cx| {
20083 assert_eq!(
20084 editor.display_text(cx),
20085 main_text,
20086 "Original main.rs text on initial open in another panel",
20087 );
20088 assert_eq!(
20089 editor
20090 .selections
20091 .all::<Point>(cx)
20092 .into_iter()
20093 .map(|s| s.range())
20094 .collect::<Vec<_>>(),
20095 vec![Point::zero()..Point::zero()],
20096 "Default selections on initial open in another panel",
20097 );
20098 })
20099 });
20100
20101 editor_2.update_in(cx, |editor, window, cx| {
20102 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
20103 });
20104
20105 let _other_editor_1 = workspace
20106 .update_in(cx, |workspace, window, cx| {
20107 workspace.open_path(
20108 (worktree_id, "lib.rs"),
20109 Some(pane_1.downgrade()),
20110 true,
20111 window,
20112 cx,
20113 )
20114 })
20115 .unwrap()
20116 .await
20117 .downcast::<Editor>()
20118 .unwrap();
20119 pane_1
20120 .update_in(cx, |pane, window, cx| {
20121 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20122 })
20123 .await
20124 .unwrap();
20125 drop(editor_1);
20126 pane_1.update(cx, |pane, cx| {
20127 pane.active_item()
20128 .unwrap()
20129 .downcast::<Editor>()
20130 .unwrap()
20131 .update(cx, |editor, cx| {
20132 assert_eq!(
20133 editor.display_text(cx),
20134 lib_text,
20135 "Other file should be open and active",
20136 );
20137 });
20138 assert_eq!(pane.items().count(), 1, "No other editors should be open");
20139 });
20140
20141 let _other_editor_2 = workspace
20142 .update_in(cx, |workspace, window, cx| {
20143 workspace.open_path(
20144 (worktree_id, "lib.rs"),
20145 Some(pane_2.downgrade()),
20146 true,
20147 window,
20148 cx,
20149 )
20150 })
20151 .unwrap()
20152 .await
20153 .downcast::<Editor>()
20154 .unwrap();
20155 pane_2
20156 .update_in(cx, |pane, window, cx| {
20157 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
20158 })
20159 .await
20160 .unwrap();
20161 drop(editor_2);
20162 pane_2.update(cx, |pane, cx| {
20163 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20164 open_editor.update(cx, |editor, cx| {
20165 assert_eq!(
20166 editor.display_text(cx),
20167 lib_text,
20168 "Other file should be open and active in another panel too",
20169 );
20170 });
20171 assert_eq!(
20172 pane.items().count(),
20173 1,
20174 "No other editors should be open in another pane",
20175 );
20176 });
20177
20178 let _editor_1_reopened = workspace
20179 .update_in(cx, |workspace, window, cx| {
20180 workspace.open_path(
20181 (worktree_id, "main.rs"),
20182 Some(pane_1.downgrade()),
20183 true,
20184 window,
20185 cx,
20186 )
20187 })
20188 .unwrap()
20189 .await
20190 .downcast::<Editor>()
20191 .unwrap();
20192 let _editor_2_reopened = workspace
20193 .update_in(cx, |workspace, window, cx| {
20194 workspace.open_path(
20195 (worktree_id, "main.rs"),
20196 Some(pane_2.downgrade()),
20197 true,
20198 window,
20199 cx,
20200 )
20201 })
20202 .unwrap()
20203 .await
20204 .downcast::<Editor>()
20205 .unwrap();
20206 pane_1.update(cx, |pane, cx| {
20207 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20208 open_editor.update(cx, |editor, cx| {
20209 assert_eq!(
20210 editor.display_text(cx),
20211 main_text,
20212 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
20213 );
20214 assert_eq!(
20215 editor
20216 .selections
20217 .all::<Point>(cx)
20218 .into_iter()
20219 .map(|s| s.range())
20220 .collect::<Vec<_>>(),
20221 expected_ranges,
20222 "Previous editor in the 1st panel had selections and should get them restored on reopen",
20223 );
20224 })
20225 });
20226 pane_2.update(cx, |pane, cx| {
20227 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20228 open_editor.update(cx, |editor, cx| {
20229 assert_eq!(
20230 editor.display_text(cx),
20231 r#"fn main() {
20232⋯rintln!("1");
20233⋯intln!("2");
20234⋯ntln!("3");
20235println!("4");
20236println!("5");
20237}"#,
20238 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
20239 );
20240 assert_eq!(
20241 editor
20242 .selections
20243 .all::<Point>(cx)
20244 .into_iter()
20245 .map(|s| s.range())
20246 .collect::<Vec<_>>(),
20247 vec![Point::zero()..Point::zero()],
20248 "Previous editor in the 2nd pane had no selections changed hence should restore none",
20249 );
20250 })
20251 });
20252}
20253
20254#[gpui::test]
20255async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
20256 init_test(cx, |_| {});
20257
20258 let fs = FakeFs::new(cx.executor());
20259 let main_text = r#"fn main() {
20260println!("1");
20261println!("2");
20262println!("3");
20263println!("4");
20264println!("5");
20265}"#;
20266 let lib_text = "mod foo {}";
20267 fs.insert_tree(
20268 path!("/a"),
20269 json!({
20270 "lib.rs": lib_text,
20271 "main.rs": main_text,
20272 }),
20273 )
20274 .await;
20275
20276 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
20277 let (workspace, cx) =
20278 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20279 let worktree_id = workspace.update(cx, |workspace, cx| {
20280 workspace.project().update(cx, |project, cx| {
20281 project.worktrees(cx).next().unwrap().read(cx).id()
20282 })
20283 });
20284
20285 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20286 let editor = workspace
20287 .update_in(cx, |workspace, window, cx| {
20288 workspace.open_path(
20289 (worktree_id, "main.rs"),
20290 Some(pane.downgrade()),
20291 true,
20292 window,
20293 cx,
20294 )
20295 })
20296 .unwrap()
20297 .await
20298 .downcast::<Editor>()
20299 .unwrap();
20300 pane.update(cx, |pane, cx| {
20301 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20302 open_editor.update(cx, |editor, cx| {
20303 assert_eq!(
20304 editor.display_text(cx),
20305 main_text,
20306 "Original main.rs text on initial open",
20307 );
20308 })
20309 });
20310 editor.update_in(cx, |editor, window, cx| {
20311 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
20312 });
20313
20314 cx.update_global(|store: &mut SettingsStore, cx| {
20315 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20316 s.restore_on_file_reopen = Some(false);
20317 });
20318 });
20319 editor.update_in(cx, |editor, window, cx| {
20320 editor.fold_ranges(
20321 vec![
20322 Point::new(1, 0)..Point::new(1, 1),
20323 Point::new(2, 0)..Point::new(2, 2),
20324 Point::new(3, 0)..Point::new(3, 3),
20325 ],
20326 false,
20327 window,
20328 cx,
20329 );
20330 });
20331 pane.update_in(cx, |pane, window, cx| {
20332 pane.close_all_items(&CloseAllItems::default(), window, cx)
20333 })
20334 .await
20335 .unwrap();
20336 pane.update(cx, |pane, _| {
20337 assert!(pane.active_item().is_none());
20338 });
20339 cx.update_global(|store: &mut SettingsStore, cx| {
20340 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
20341 s.restore_on_file_reopen = Some(true);
20342 });
20343 });
20344
20345 let _editor_reopened = workspace
20346 .update_in(cx, |workspace, window, cx| {
20347 workspace.open_path(
20348 (worktree_id, "main.rs"),
20349 Some(pane.downgrade()),
20350 true,
20351 window,
20352 cx,
20353 )
20354 })
20355 .unwrap()
20356 .await
20357 .downcast::<Editor>()
20358 .unwrap();
20359 pane.update(cx, |pane, cx| {
20360 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20361 open_editor.update(cx, |editor, cx| {
20362 assert_eq!(
20363 editor.display_text(cx),
20364 main_text,
20365 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
20366 );
20367 })
20368 });
20369}
20370
20371#[gpui::test]
20372async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
20373 struct EmptyModalView {
20374 focus_handle: gpui::FocusHandle,
20375 }
20376 impl EventEmitter<DismissEvent> for EmptyModalView {}
20377 impl Render for EmptyModalView {
20378 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
20379 div()
20380 }
20381 }
20382 impl Focusable for EmptyModalView {
20383 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
20384 self.focus_handle.clone()
20385 }
20386 }
20387 impl workspace::ModalView for EmptyModalView {}
20388 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
20389 EmptyModalView {
20390 focus_handle: cx.focus_handle(),
20391 }
20392 }
20393
20394 init_test(cx, |_| {});
20395
20396 let fs = FakeFs::new(cx.executor());
20397 let project = Project::test(fs, [], cx).await;
20398 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20399 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
20400 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
20401 let editor = cx.new_window_entity(|window, cx| {
20402 Editor::new(
20403 EditorMode::full(),
20404 buffer,
20405 Some(project.clone()),
20406 window,
20407 cx,
20408 )
20409 });
20410 workspace
20411 .update(cx, |workspace, window, cx| {
20412 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
20413 })
20414 .unwrap();
20415 editor.update_in(cx, |editor, window, cx| {
20416 editor.open_context_menu(&OpenContextMenu, window, cx);
20417 assert!(editor.mouse_context_menu.is_some());
20418 });
20419 workspace
20420 .update(cx, |workspace, window, cx| {
20421 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
20422 })
20423 .unwrap();
20424 cx.read(|cx| {
20425 assert!(editor.read(cx).mouse_context_menu.is_none());
20426 });
20427}
20428
20429#[gpui::test]
20430async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
20431 init_test(cx, |_| {});
20432
20433 let fs = FakeFs::new(cx.executor());
20434 fs.insert_file(path!("/file.html"), Default::default())
20435 .await;
20436
20437 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
20438
20439 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20440 let html_language = Arc::new(Language::new(
20441 LanguageConfig {
20442 name: "HTML".into(),
20443 matcher: LanguageMatcher {
20444 path_suffixes: vec!["html".to_string()],
20445 ..LanguageMatcher::default()
20446 },
20447 brackets: BracketPairConfig {
20448 pairs: vec![BracketPair {
20449 start: "<".into(),
20450 end: ">".into(),
20451 close: true,
20452 ..Default::default()
20453 }],
20454 ..Default::default()
20455 },
20456 ..Default::default()
20457 },
20458 Some(tree_sitter_html::LANGUAGE.into()),
20459 ));
20460 language_registry.add(html_language);
20461 let mut fake_servers = language_registry.register_fake_lsp(
20462 "HTML",
20463 FakeLspAdapter {
20464 capabilities: lsp::ServerCapabilities {
20465 completion_provider: Some(lsp::CompletionOptions {
20466 resolve_provider: Some(true),
20467 ..Default::default()
20468 }),
20469 ..Default::default()
20470 },
20471 ..Default::default()
20472 },
20473 );
20474
20475 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
20476 let cx = &mut VisualTestContext::from_window(*workspace, cx);
20477
20478 let worktree_id = workspace
20479 .update(cx, |workspace, _window, cx| {
20480 workspace.project().update(cx, |project, cx| {
20481 project.worktrees(cx).next().unwrap().read(cx).id()
20482 })
20483 })
20484 .unwrap();
20485 project
20486 .update(cx, |project, cx| {
20487 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
20488 })
20489 .await
20490 .unwrap();
20491 let editor = workspace
20492 .update(cx, |workspace, window, cx| {
20493 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
20494 })
20495 .unwrap()
20496 .await
20497 .unwrap()
20498 .downcast::<Editor>()
20499 .unwrap();
20500
20501 let fake_server = fake_servers.next().await.unwrap();
20502 editor.update_in(cx, |editor, window, cx| {
20503 editor.set_text("<ad></ad>", window, cx);
20504 editor.change_selections(None, window, cx, |selections| {
20505 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
20506 });
20507 let Some((buffer, _)) = editor
20508 .buffer
20509 .read(cx)
20510 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
20511 else {
20512 panic!("Failed to get buffer for selection position");
20513 };
20514 let buffer = buffer.read(cx);
20515 let buffer_id = buffer.remote_id();
20516 let opening_range =
20517 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
20518 let closing_range =
20519 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
20520 let mut linked_ranges = HashMap::default();
20521 linked_ranges.insert(
20522 buffer_id,
20523 vec![(opening_range.clone(), vec![closing_range.clone()])],
20524 );
20525 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
20526 });
20527 let mut completion_handle =
20528 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
20529 Ok(Some(lsp::CompletionResponse::Array(vec![
20530 lsp::CompletionItem {
20531 label: "head".to_string(),
20532 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20533 lsp::InsertReplaceEdit {
20534 new_text: "head".to_string(),
20535 insert: lsp::Range::new(
20536 lsp::Position::new(0, 1),
20537 lsp::Position::new(0, 3),
20538 ),
20539 replace: lsp::Range::new(
20540 lsp::Position::new(0, 1),
20541 lsp::Position::new(0, 3),
20542 ),
20543 },
20544 )),
20545 ..Default::default()
20546 },
20547 ])))
20548 });
20549 editor.update_in(cx, |editor, window, cx| {
20550 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
20551 });
20552 cx.run_until_parked();
20553 completion_handle.next().await.unwrap();
20554 editor.update(cx, |editor, _| {
20555 assert!(
20556 editor.context_menu_visible(),
20557 "Completion menu should be visible"
20558 );
20559 });
20560 editor.update_in(cx, |editor, window, cx| {
20561 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
20562 });
20563 cx.executor().run_until_parked();
20564 editor.update(cx, |editor, cx| {
20565 assert_eq!(editor.text(cx), "<head></head>");
20566 });
20567}
20568
20569#[gpui::test]
20570async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
20571 init_test(cx, |_| {});
20572
20573 let fs = FakeFs::new(cx.executor());
20574 fs.insert_tree(
20575 path!("/root"),
20576 json!({
20577 "a": {
20578 "main.rs": "fn main() {}",
20579 },
20580 "foo": {
20581 "bar": {
20582 "external_file.rs": "pub mod external {}",
20583 }
20584 }
20585 }),
20586 )
20587 .await;
20588
20589 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
20590 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
20591 language_registry.add(rust_lang());
20592 let _fake_servers = language_registry.register_fake_lsp(
20593 "Rust",
20594 FakeLspAdapter {
20595 ..FakeLspAdapter::default()
20596 },
20597 );
20598 let (workspace, cx) =
20599 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
20600 let worktree_id = workspace.update(cx, |workspace, cx| {
20601 workspace.project().update(cx, |project, cx| {
20602 project.worktrees(cx).next().unwrap().read(cx).id()
20603 })
20604 });
20605
20606 let assert_language_servers_count =
20607 |expected: usize, context: &str, cx: &mut VisualTestContext| {
20608 project.update(cx, |project, cx| {
20609 let current = project
20610 .lsp_store()
20611 .read(cx)
20612 .as_local()
20613 .unwrap()
20614 .language_servers
20615 .len();
20616 assert_eq!(expected, current, "{context}");
20617 });
20618 };
20619
20620 assert_language_servers_count(
20621 0,
20622 "No servers should be running before any file is open",
20623 cx,
20624 );
20625 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
20626 let main_editor = workspace
20627 .update_in(cx, |workspace, window, cx| {
20628 workspace.open_path(
20629 (worktree_id, "main.rs"),
20630 Some(pane.downgrade()),
20631 true,
20632 window,
20633 cx,
20634 )
20635 })
20636 .unwrap()
20637 .await
20638 .downcast::<Editor>()
20639 .unwrap();
20640 pane.update(cx, |pane, cx| {
20641 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20642 open_editor.update(cx, |editor, cx| {
20643 assert_eq!(
20644 editor.display_text(cx),
20645 "fn main() {}",
20646 "Original main.rs text on initial open",
20647 );
20648 });
20649 assert_eq!(open_editor, main_editor);
20650 });
20651 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
20652
20653 let external_editor = workspace
20654 .update_in(cx, |workspace, window, cx| {
20655 workspace.open_abs_path(
20656 PathBuf::from("/root/foo/bar/external_file.rs"),
20657 OpenOptions::default(),
20658 window,
20659 cx,
20660 )
20661 })
20662 .await
20663 .expect("opening external file")
20664 .downcast::<Editor>()
20665 .expect("downcasted external file's open element to editor");
20666 pane.update(cx, |pane, cx| {
20667 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20668 open_editor.update(cx, |editor, cx| {
20669 assert_eq!(
20670 editor.display_text(cx),
20671 "pub mod external {}",
20672 "External file is open now",
20673 );
20674 });
20675 assert_eq!(open_editor, external_editor);
20676 });
20677 assert_language_servers_count(
20678 1,
20679 "Second, external, *.rs file should join the existing server",
20680 cx,
20681 );
20682
20683 pane.update_in(cx, |pane, window, cx| {
20684 pane.close_active_item(&CloseActiveItem::default(), window, cx)
20685 })
20686 .await
20687 .unwrap();
20688 pane.update_in(cx, |pane, window, cx| {
20689 pane.navigate_backward(window, cx);
20690 });
20691 cx.run_until_parked();
20692 pane.update(cx, |pane, cx| {
20693 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
20694 open_editor.update(cx, |editor, cx| {
20695 assert_eq!(
20696 editor.display_text(cx),
20697 "pub mod external {}",
20698 "External file is open now",
20699 );
20700 });
20701 });
20702 assert_language_servers_count(
20703 1,
20704 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20705 cx,
20706 );
20707
20708 cx.update(|_, cx| {
20709 workspace::reload(&workspace::Reload::default(), cx);
20710 });
20711 assert_language_servers_count(
20712 1,
20713 "After reloading the worktree with local and external files opened, only one project should be started",
20714 cx,
20715 );
20716}
20717
20718#[gpui::test]
20719async fn test_tab_in_leading_whitespace_auto_indents_for_python(cx: &mut TestAppContext) {
20720 init_test(cx, |_| {});
20721
20722 let mut cx = EditorTestContext::new(cx).await;
20723 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20724 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20725
20726 // test cursor move to start of each line on tab
20727 // for `if`, `elif`, `else`, `while`, `with` and `for`
20728 cx.set_state(indoc! {"
20729 def main():
20730 ˇ for item in items:
20731 ˇ while item.active:
20732 ˇ if item.value > 10:
20733 ˇ continue
20734 ˇ elif item.value < 0:
20735 ˇ break
20736 ˇ else:
20737 ˇ with item.context() as ctx:
20738 ˇ yield count
20739 ˇ else:
20740 ˇ log('while else')
20741 ˇ else:
20742 ˇ log('for else')
20743 "});
20744 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20745 cx.assert_editor_state(indoc! {"
20746 def main():
20747 ˇfor item in items:
20748 ˇwhile item.active:
20749 ˇif item.value > 10:
20750 ˇcontinue
20751 ˇelif item.value < 0:
20752 ˇbreak
20753 ˇelse:
20754 ˇwith item.context() as ctx:
20755 ˇyield count
20756 ˇelse:
20757 ˇlog('while else')
20758 ˇelse:
20759 ˇlog('for else')
20760 "});
20761 // test relative indent is preserved when tab
20762 // for `if`, `elif`, `else`, `while`, `with` and `for`
20763 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20764 cx.assert_editor_state(indoc! {"
20765 def main():
20766 ˇfor item in items:
20767 ˇwhile item.active:
20768 ˇif item.value > 10:
20769 ˇcontinue
20770 ˇelif item.value < 0:
20771 ˇbreak
20772 ˇelse:
20773 ˇwith item.context() as ctx:
20774 ˇyield count
20775 ˇelse:
20776 ˇlog('while else')
20777 ˇelse:
20778 ˇlog('for else')
20779 "});
20780
20781 // test cursor move to start of each line on tab
20782 // for `try`, `except`, `else`, `finally`, `match` and `def`
20783 cx.set_state(indoc! {"
20784 def main():
20785 ˇ try:
20786 ˇ fetch()
20787 ˇ except ValueError:
20788 ˇ handle_error()
20789 ˇ else:
20790 ˇ match value:
20791 ˇ case _:
20792 ˇ finally:
20793 ˇ def status():
20794 ˇ return 0
20795 "});
20796 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20797 cx.assert_editor_state(indoc! {"
20798 def main():
20799 ˇtry:
20800 ˇfetch()
20801 ˇexcept ValueError:
20802 ˇhandle_error()
20803 ˇelse:
20804 ˇmatch value:
20805 ˇcase _:
20806 ˇfinally:
20807 ˇdef status():
20808 ˇreturn 0
20809 "});
20810 // test relative indent is preserved when tab
20811 // for `try`, `except`, `else`, `finally`, `match` and `def`
20812 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
20813 cx.assert_editor_state(indoc! {"
20814 def main():
20815 ˇtry:
20816 ˇfetch()
20817 ˇexcept ValueError:
20818 ˇhandle_error()
20819 ˇelse:
20820 ˇmatch value:
20821 ˇcase _:
20822 ˇfinally:
20823 ˇdef status():
20824 ˇreturn 0
20825 "});
20826}
20827
20828#[gpui::test]
20829async fn test_outdent_after_input_for_python(cx: &mut TestAppContext) {
20830 init_test(cx, |_| {});
20831
20832 let mut cx = EditorTestContext::new(cx).await;
20833 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
20834 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
20835
20836 // test `else` auto outdents when typed inside `if` block
20837 cx.set_state(indoc! {"
20838 def main():
20839 if i == 2:
20840 return
20841 ˇ
20842 "});
20843 cx.update_editor(|editor, window, cx| {
20844 editor.handle_input("else:", window, cx);
20845 });
20846 cx.assert_editor_state(indoc! {"
20847 def main():
20848 if i == 2:
20849 return
20850 else:ˇ
20851 "});
20852
20853 // test `except` auto outdents when typed inside `try` block
20854 cx.set_state(indoc! {"
20855 def main():
20856 try:
20857 i = 2
20858 ˇ
20859 "});
20860 cx.update_editor(|editor, window, cx| {
20861 editor.handle_input("except:", window, cx);
20862 });
20863 cx.assert_editor_state(indoc! {"
20864 def main():
20865 try:
20866 i = 2
20867 except:ˇ
20868 "});
20869
20870 // test `else` auto outdents when typed inside `except` block
20871 cx.set_state(indoc! {"
20872 def main():
20873 try:
20874 i = 2
20875 except:
20876 j = 2
20877 ˇ
20878 "});
20879 cx.update_editor(|editor, window, cx| {
20880 editor.handle_input("else:", window, cx);
20881 });
20882 cx.assert_editor_state(indoc! {"
20883 def main():
20884 try:
20885 i = 2
20886 except:
20887 j = 2
20888 else:ˇ
20889 "});
20890
20891 // test `finally` auto outdents when typed inside `else` block
20892 cx.set_state(indoc! {"
20893 def main():
20894 try:
20895 i = 2
20896 except:
20897 j = 2
20898 else:
20899 k = 2
20900 ˇ
20901 "});
20902 cx.update_editor(|editor, window, cx| {
20903 editor.handle_input("finally:", window, cx);
20904 });
20905 cx.assert_editor_state(indoc! {"
20906 def main():
20907 try:
20908 i = 2
20909 except:
20910 j = 2
20911 else:
20912 k = 2
20913 finally:ˇ
20914 "});
20915
20916 // TODO: test `except` auto outdents when typed inside `try` block right after for block
20917 // cx.set_state(indoc! {"
20918 // def main():
20919 // try:
20920 // for i in range(n):
20921 // pass
20922 // ˇ
20923 // "});
20924 // cx.update_editor(|editor, window, cx| {
20925 // editor.handle_input("except:", window, cx);
20926 // });
20927 // cx.assert_editor_state(indoc! {"
20928 // def main():
20929 // try:
20930 // for i in range(n):
20931 // pass
20932 // except:ˇ
20933 // "});
20934
20935 // TODO: test `else` auto outdents when typed inside `except` block right after for block
20936 // cx.set_state(indoc! {"
20937 // def main():
20938 // try:
20939 // i = 2
20940 // except:
20941 // for i in range(n):
20942 // pass
20943 // ˇ
20944 // "});
20945 // cx.update_editor(|editor, window, cx| {
20946 // editor.handle_input("else:", window, cx);
20947 // });
20948 // cx.assert_editor_state(indoc! {"
20949 // def main():
20950 // try:
20951 // i = 2
20952 // except:
20953 // for i in range(n):
20954 // pass
20955 // else:ˇ
20956 // "});
20957
20958 // TODO: test `finally` auto outdents when typed inside `else` block right after for block
20959 // cx.set_state(indoc! {"
20960 // def main():
20961 // try:
20962 // i = 2
20963 // except:
20964 // j = 2
20965 // else:
20966 // for i in range(n):
20967 // pass
20968 // ˇ
20969 // "});
20970 // cx.update_editor(|editor, window, cx| {
20971 // editor.handle_input("finally:", window, cx);
20972 // });
20973 // cx.assert_editor_state(indoc! {"
20974 // def main():
20975 // try:
20976 // i = 2
20977 // except:
20978 // j = 2
20979 // else:
20980 // for i in range(n):
20981 // pass
20982 // finally:ˇ
20983 // "});
20984
20985 // test `else` stays at correct indent when typed after `for` block
20986 cx.set_state(indoc! {"
20987 def main():
20988 for i in range(10):
20989 if i == 3:
20990 break
20991 ˇ
20992 "});
20993 cx.update_editor(|editor, window, cx| {
20994 editor.handle_input("else:", window, cx);
20995 });
20996 cx.assert_editor_state(indoc! {"
20997 def main():
20998 for i in range(10):
20999 if i == 3:
21000 break
21001 else:ˇ
21002 "});
21003
21004 // test does not outdent on typing after line with square brackets
21005 cx.set_state(indoc! {"
21006 def f() -> list[str]:
21007 ˇ
21008 "});
21009 cx.update_editor(|editor, window, cx| {
21010 editor.handle_input("a", window, cx);
21011 });
21012 cx.assert_editor_state(indoc! {"
21013 def f() -> list[str]:
21014 aˇ
21015 "});
21016}
21017
21018#[gpui::test]
21019async fn test_indent_on_newline_for_python(cx: &mut TestAppContext) {
21020 init_test(cx, |_| {});
21021 update_test_language_settings(cx, |settings| {
21022 settings.defaults.extend_comment_on_newline = Some(false);
21023 });
21024 let mut cx = EditorTestContext::new(cx).await;
21025 let language = languages::language("python", tree_sitter_python::LANGUAGE.into());
21026 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
21027
21028 // test correct indent after newline on comment
21029 cx.set_state(indoc! {"
21030 # COMMENT:ˇ
21031 "});
21032 cx.update_editor(|editor, window, cx| {
21033 editor.newline(&Newline, window, cx);
21034 });
21035 cx.assert_editor_state(indoc! {"
21036 # COMMENT:
21037 ˇ
21038 "});
21039
21040 // test correct indent after newline in brackets
21041 cx.set_state(indoc! {"
21042 {ˇ}
21043 "});
21044 cx.update_editor(|editor, window, cx| {
21045 editor.newline(&Newline, window, cx);
21046 });
21047 cx.run_until_parked();
21048 cx.assert_editor_state(indoc! {"
21049 {
21050 ˇ
21051 }
21052 "});
21053
21054 cx.set_state(indoc! {"
21055 (ˇ)
21056 "});
21057 cx.update_editor(|editor, window, cx| {
21058 editor.newline(&Newline, window, cx);
21059 });
21060 cx.run_until_parked();
21061 cx.assert_editor_state(indoc! {"
21062 (
21063 ˇ
21064 )
21065 "});
21066
21067 // do not indent after empty lists or dictionaries
21068 cx.set_state(indoc! {"
21069 a = []ˇ
21070 "});
21071 cx.update_editor(|editor, window, cx| {
21072 editor.newline(&Newline, window, cx);
21073 });
21074 cx.run_until_parked();
21075 cx.assert_editor_state(indoc! {"
21076 a = []
21077 ˇ
21078 "});
21079}
21080
21081fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
21082 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
21083 point..point
21084}
21085
21086fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
21087 let (text, ranges) = marked_text_ranges(marked_text, true);
21088 assert_eq!(editor.text(cx), text);
21089 assert_eq!(
21090 editor.selections.ranges(cx),
21091 ranges,
21092 "Assert selections are {}",
21093 marked_text
21094 );
21095}
21096
21097pub fn handle_signature_help_request(
21098 cx: &mut EditorLspTestContext,
21099 mocked_response: lsp::SignatureHelp,
21100) -> impl Future<Output = ()> + use<> {
21101 let mut request =
21102 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
21103 let mocked_response = mocked_response.clone();
21104 async move { Ok(Some(mocked_response)) }
21105 });
21106
21107 async move {
21108 request.next().await;
21109 }
21110}
21111
21112/// Handle completion request passing a marked string specifying where the completion
21113/// should be triggered from using '|' character, what range should be replaced, and what completions
21114/// should be returned using '<' and '>' to delimit the range.
21115///
21116/// Also see `handle_completion_request_with_insert_and_replace`.
21117#[track_caller]
21118pub fn handle_completion_request(
21119 cx: &mut EditorLspTestContext,
21120 marked_string: &str,
21121 completions: Vec<&'static str>,
21122 counter: Arc<AtomicUsize>,
21123) -> impl Future<Output = ()> {
21124 let complete_from_marker: TextRangeMarker = '|'.into();
21125 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21126 let (_, mut marked_ranges) = marked_text_ranges_by(
21127 marked_string,
21128 vec![complete_from_marker.clone(), replace_range_marker.clone()],
21129 );
21130
21131 let complete_from_position =
21132 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21133 let replace_range =
21134 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21135
21136 let mut request =
21137 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21138 let completions = completions.clone();
21139 counter.fetch_add(1, atomic::Ordering::Release);
21140 async move {
21141 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21142 assert_eq!(
21143 params.text_document_position.position,
21144 complete_from_position
21145 );
21146 Ok(Some(lsp::CompletionResponse::Array(
21147 completions
21148 .iter()
21149 .map(|completion_text| lsp::CompletionItem {
21150 label: completion_text.to_string(),
21151 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
21152 range: replace_range,
21153 new_text: completion_text.to_string(),
21154 })),
21155 ..Default::default()
21156 })
21157 .collect(),
21158 )))
21159 }
21160 });
21161
21162 async move {
21163 request.next().await;
21164 }
21165}
21166
21167/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
21168/// given instead, which also contains an `insert` range.
21169///
21170/// This function uses markers to define ranges:
21171/// - `|` marks the cursor position
21172/// - `<>` marks the replace range
21173/// - `[]` marks the insert range (optional, defaults to `replace_range.start..cursor_pos`which is what Rust-Analyzer provides)
21174pub fn handle_completion_request_with_insert_and_replace(
21175 cx: &mut EditorLspTestContext,
21176 marked_string: &str,
21177 completions: Vec<(&'static str, &'static str)>, // (label, new_text)
21178 counter: Arc<AtomicUsize>,
21179) -> impl Future<Output = ()> {
21180 let complete_from_marker: TextRangeMarker = '|'.into();
21181 let replace_range_marker: TextRangeMarker = ('<', '>').into();
21182 let insert_range_marker: TextRangeMarker = ('{', '}').into();
21183
21184 let (_, mut marked_ranges) = marked_text_ranges_by(
21185 marked_string,
21186 vec![
21187 complete_from_marker.clone(),
21188 replace_range_marker.clone(),
21189 insert_range_marker.clone(),
21190 ],
21191 );
21192
21193 let complete_from_position =
21194 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
21195 let replace_range =
21196 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
21197
21198 let insert_range = match marked_ranges.remove(&insert_range_marker) {
21199 Some(ranges) if !ranges.is_empty() => cx.to_lsp_range(ranges[0].clone()),
21200 _ => lsp::Range {
21201 start: replace_range.start,
21202 end: complete_from_position,
21203 },
21204 };
21205
21206 let mut request =
21207 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
21208 let completions = completions.clone();
21209 counter.fetch_add(1, atomic::Ordering::Release);
21210 async move {
21211 assert_eq!(params.text_document_position.text_document.uri, url.clone());
21212 assert_eq!(
21213 params.text_document_position.position, complete_from_position,
21214 "marker `|` position doesn't match",
21215 );
21216 Ok(Some(lsp::CompletionResponse::Array(
21217 completions
21218 .iter()
21219 .map(|(label, new_text)| lsp::CompletionItem {
21220 label: label.to_string(),
21221 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
21222 lsp::InsertReplaceEdit {
21223 insert: insert_range,
21224 replace: replace_range,
21225 new_text: new_text.to_string(),
21226 },
21227 )),
21228 ..Default::default()
21229 })
21230 .collect(),
21231 )))
21232 }
21233 });
21234
21235 async move {
21236 request.next().await;
21237 }
21238}
21239
21240fn handle_resolve_completion_request(
21241 cx: &mut EditorLspTestContext,
21242 edits: Option<Vec<(&'static str, &'static str)>>,
21243) -> impl Future<Output = ()> {
21244 let edits = edits.map(|edits| {
21245 edits
21246 .iter()
21247 .map(|(marked_string, new_text)| {
21248 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
21249 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
21250 lsp::TextEdit::new(replace_range, new_text.to_string())
21251 })
21252 .collect::<Vec<_>>()
21253 });
21254
21255 let mut request =
21256 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
21257 let edits = edits.clone();
21258 async move {
21259 Ok(lsp::CompletionItem {
21260 additional_text_edits: edits,
21261 ..Default::default()
21262 })
21263 }
21264 });
21265
21266 async move {
21267 request.next().await;
21268 }
21269}
21270
21271pub(crate) fn update_test_language_settings(
21272 cx: &mut TestAppContext,
21273 f: impl Fn(&mut AllLanguageSettingsContent),
21274) {
21275 cx.update(|cx| {
21276 SettingsStore::update_global(cx, |store, cx| {
21277 store.update_user_settings::<AllLanguageSettings>(cx, f);
21278 });
21279 });
21280}
21281
21282pub(crate) fn update_test_project_settings(
21283 cx: &mut TestAppContext,
21284 f: impl Fn(&mut ProjectSettings),
21285) {
21286 cx.update(|cx| {
21287 SettingsStore::update_global(cx, |store, cx| {
21288 store.update_user_settings::<ProjectSettings>(cx, f);
21289 });
21290 });
21291}
21292
21293pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
21294 cx.update(|cx| {
21295 assets::Assets.load_test_fonts(cx);
21296 let store = SettingsStore::test(cx);
21297 cx.set_global(store);
21298 theme::init(theme::LoadThemes::JustBase, cx);
21299 release_channel::init(SemanticVersion::default(), cx);
21300 client::init_settings(cx);
21301 language::init(cx);
21302 Project::init_settings(cx);
21303 workspace::init_settings(cx);
21304 crate::init(cx);
21305 });
21306
21307 update_test_language_settings(cx, f);
21308}
21309
21310#[track_caller]
21311fn assert_hunk_revert(
21312 not_reverted_text_with_selections: &str,
21313 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
21314 expected_reverted_text_with_selections: &str,
21315 base_text: &str,
21316 cx: &mut EditorLspTestContext,
21317) {
21318 cx.set_state(not_reverted_text_with_selections);
21319 cx.set_head_text(base_text);
21320 cx.executor().run_until_parked();
21321
21322 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
21323 let snapshot = editor.snapshot(window, cx);
21324 let reverted_hunk_statuses = snapshot
21325 .buffer_snapshot
21326 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
21327 .map(|hunk| hunk.status().kind)
21328 .collect::<Vec<_>>();
21329
21330 editor.git_restore(&Default::default(), window, cx);
21331 reverted_hunk_statuses
21332 });
21333 cx.executor().run_until_parked();
21334 cx.assert_editor_state(expected_reverted_text_with_selections);
21335 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
21336}