1use super::*;
2use crate::{
3 JoinLines,
4 linked_editing_ranges::LinkedEditingRanges,
5 scroll::scroll_amount::ScrollAmount,
6 test::{
7 assert_text_with_selections, build_editor,
8 editor_lsp_test_context::{EditorLspTestContext, git_commit_lang},
9 editor_test_context::EditorTestContext,
10 select_ranges,
11 },
12};
13use buffer_diff::{BufferDiff, DiffHunkSecondaryStatus, DiffHunkStatus, DiffHunkStatusKind};
14use futures::StreamExt;
15use gpui::{
16 BackgroundExecutor, DismissEvent, SemanticVersion, TestAppContext, UpdateGlobal,
17 VisualTestContext, WindowBounds, WindowOptions, div,
18};
19use indoc::indoc;
20use language::{
21 BracketPairConfig,
22 Capability::ReadWrite,
23 FakeLspAdapter, LanguageConfig, LanguageConfigOverride, LanguageMatcher, LanguageName,
24 Override, Point,
25 language_settings::{
26 AllLanguageSettings, AllLanguageSettingsContent, CompletionSettings,
27 LanguageSettingsContent, LspInsertMode, PrettierSettings,
28 },
29};
30use language_settings::{Formatter, FormatterList, IndentGuideSettings};
31use lsp::CompletionParams;
32use multi_buffer::{IndentGuide, PathKey};
33use parking_lot::Mutex;
34use pretty_assertions::{assert_eq, assert_ne};
35use project::{
36 FakeFs,
37 debugger::breakpoint_store::{BreakpointState, SourceBreakpoint},
38 project_settings::{LspSettings, ProjectSettings},
39};
40use serde_json::{self, json};
41use std::{cell::RefCell, future::Future, rc::Rc, sync::atomic::AtomicBool, time::Instant};
42use std::{
43 iter,
44 sync::atomic::{self, AtomicUsize},
45};
46use test::{build_editor_with_project, editor_lsp_test_context::rust_lang};
47use text::ToPoint as _;
48use unindent::Unindent;
49use util::{
50 assert_set_eq, path,
51 test::{TextRangeMarker, marked_text_ranges, marked_text_ranges_by, sample_text},
52 uri,
53};
54use workspace::{
55 CloseActiveItem, CloseAllItems, CloseInactiveItems, NavigationEntry, OpenOptions, ViewId,
56 item::{FollowEvent, FollowableItem, Item, ItemHandle},
57};
58
59#[gpui::test]
60fn test_edit_events(cx: &mut TestAppContext) {
61 init_test(cx, |_| {});
62
63 let buffer = cx.new(|cx| {
64 let mut buffer = language::Buffer::local("123456", cx);
65 buffer.set_group_interval(Duration::from_secs(1));
66 buffer
67 });
68
69 let events = Rc::new(RefCell::new(Vec::new()));
70 let editor1 = cx.add_window({
71 let events = events.clone();
72 |window, cx| {
73 let entity = cx.entity().clone();
74 cx.subscribe_in(
75 &entity,
76 window,
77 move |_, _, event: &EditorEvent, _, _| match event {
78 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor1", "edited")),
79 EditorEvent::BufferEdited => {
80 events.borrow_mut().push(("editor1", "buffer edited"))
81 }
82 _ => {}
83 },
84 )
85 .detach();
86 Editor::for_buffer(buffer.clone(), None, window, cx)
87 }
88 });
89
90 let editor2 = cx.add_window({
91 let events = events.clone();
92 |window, cx| {
93 cx.subscribe_in(
94 &cx.entity().clone(),
95 window,
96 move |_, _, event: &EditorEvent, _, _| match event {
97 EditorEvent::Edited { .. } => events.borrow_mut().push(("editor2", "edited")),
98 EditorEvent::BufferEdited => {
99 events.borrow_mut().push(("editor2", "buffer edited"))
100 }
101 _ => {}
102 },
103 )
104 .detach();
105 Editor::for_buffer(buffer.clone(), None, window, cx)
106 }
107 });
108
109 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
110
111 // Mutating editor 1 will emit an `Edited` event only for that editor.
112 _ = editor1.update(cx, |editor, window, cx| editor.insert("X", window, cx));
113 assert_eq!(
114 mem::take(&mut *events.borrow_mut()),
115 [
116 ("editor1", "edited"),
117 ("editor1", "buffer edited"),
118 ("editor2", "buffer edited"),
119 ]
120 );
121
122 // Mutating editor 2 will emit an `Edited` event only for that editor.
123 _ = editor2.update(cx, |editor, window, cx| editor.delete(&Delete, window, cx));
124 assert_eq!(
125 mem::take(&mut *events.borrow_mut()),
126 [
127 ("editor2", "edited"),
128 ("editor1", "buffer edited"),
129 ("editor2", "buffer edited"),
130 ]
131 );
132
133 // Undoing on editor 1 will emit an `Edited` event only for that editor.
134 _ = editor1.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
135 assert_eq!(
136 mem::take(&mut *events.borrow_mut()),
137 [
138 ("editor1", "edited"),
139 ("editor1", "buffer edited"),
140 ("editor2", "buffer edited"),
141 ]
142 );
143
144 // Redoing on editor 1 will emit an `Edited` event only for that editor.
145 _ = editor1.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
146 assert_eq!(
147 mem::take(&mut *events.borrow_mut()),
148 [
149 ("editor1", "edited"),
150 ("editor1", "buffer edited"),
151 ("editor2", "buffer edited"),
152 ]
153 );
154
155 // Undoing on editor 2 will emit an `Edited` event only for that editor.
156 _ = editor2.update(cx, |editor, window, cx| editor.undo(&Undo, window, cx));
157 assert_eq!(
158 mem::take(&mut *events.borrow_mut()),
159 [
160 ("editor2", "edited"),
161 ("editor1", "buffer edited"),
162 ("editor2", "buffer edited"),
163 ]
164 );
165
166 // Redoing on editor 2 will emit an `Edited` event only for that editor.
167 _ = editor2.update(cx, |editor, window, cx| editor.redo(&Redo, window, cx));
168 assert_eq!(
169 mem::take(&mut *events.borrow_mut()),
170 [
171 ("editor2", "edited"),
172 ("editor1", "buffer edited"),
173 ("editor2", "buffer edited"),
174 ]
175 );
176
177 // No event is emitted when the mutation is a no-op.
178 _ = editor2.update(cx, |editor, window, cx| {
179 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
180
181 editor.backspace(&Backspace, window, cx);
182 });
183 assert_eq!(mem::take(&mut *events.borrow_mut()), []);
184}
185
186#[gpui::test]
187fn test_undo_redo_with_selection_restoration(cx: &mut TestAppContext) {
188 init_test(cx, |_| {});
189
190 let mut now = Instant::now();
191 let group_interval = Duration::from_millis(1);
192 let buffer = cx.new(|cx| {
193 let mut buf = language::Buffer::local("123456", cx);
194 buf.set_group_interval(group_interval);
195 buf
196 });
197 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
198 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
199
200 _ = editor.update(cx, |editor, window, cx| {
201 editor.start_transaction_at(now, window, cx);
202 editor.change_selections(None, window, cx, |s| s.select_ranges([2..4]));
203
204 editor.insert("cd", window, cx);
205 editor.end_transaction_at(now, cx);
206 assert_eq!(editor.text(cx), "12cd56");
207 assert_eq!(editor.selections.ranges(cx), vec![4..4]);
208
209 editor.start_transaction_at(now, window, cx);
210 editor.change_selections(None, window, cx, |s| s.select_ranges([4..5]));
211 editor.insert("e", window, cx);
212 editor.end_transaction_at(now, cx);
213 assert_eq!(editor.text(cx), "12cde6");
214 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
215
216 now += group_interval + Duration::from_millis(1);
217 editor.change_selections(None, window, cx, |s| s.select_ranges([2..2]));
218
219 // Simulate an edit in another editor
220 buffer.update(cx, |buffer, cx| {
221 buffer.start_transaction_at(now, cx);
222 buffer.edit([(0..1, "a")], None, cx);
223 buffer.edit([(1..1, "b")], None, cx);
224 buffer.end_transaction_at(now, cx);
225 });
226
227 assert_eq!(editor.text(cx), "ab2cde6");
228 assert_eq!(editor.selections.ranges(cx), vec![3..3]);
229
230 // Last transaction happened past the group interval in a different editor.
231 // Undo it individually and don't restore selections.
232 editor.undo(&Undo, window, cx);
233 assert_eq!(editor.text(cx), "12cde6");
234 assert_eq!(editor.selections.ranges(cx), vec![2..2]);
235
236 // First two transactions happened within the group interval in this editor.
237 // Undo them together and restore selections.
238 editor.undo(&Undo, window, cx);
239 editor.undo(&Undo, window, cx); // Undo stack is empty here, so this is a no-op.
240 assert_eq!(editor.text(cx), "123456");
241 assert_eq!(editor.selections.ranges(cx), vec![0..0]);
242
243 // Redo the first two transactions together.
244 editor.redo(&Redo, window, cx);
245 assert_eq!(editor.text(cx), "12cde6");
246 assert_eq!(editor.selections.ranges(cx), vec![5..5]);
247
248 // Redo the last transaction on its own.
249 editor.redo(&Redo, window, cx);
250 assert_eq!(editor.text(cx), "ab2cde6");
251 assert_eq!(editor.selections.ranges(cx), vec![6..6]);
252
253 // Test empty transactions.
254 editor.start_transaction_at(now, window, cx);
255 editor.end_transaction_at(now, cx);
256 editor.undo(&Undo, window, cx);
257 assert_eq!(editor.text(cx), "12cde6");
258 });
259}
260
261#[gpui::test]
262fn test_ime_composition(cx: &mut TestAppContext) {
263 init_test(cx, |_| {});
264
265 let buffer = cx.new(|cx| {
266 let mut buffer = language::Buffer::local("abcde", cx);
267 // Ensure automatic grouping doesn't occur.
268 buffer.set_group_interval(Duration::ZERO);
269 buffer
270 });
271
272 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
273 cx.add_window(|window, cx| {
274 let mut editor = build_editor(buffer.clone(), window, cx);
275
276 // Start a new IME composition.
277 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
278 editor.replace_and_mark_text_in_range(Some(0..1), "á", None, window, cx);
279 editor.replace_and_mark_text_in_range(Some(0..1), "ä", None, window, cx);
280 assert_eq!(editor.text(cx), "äbcde");
281 assert_eq!(
282 editor.marked_text_ranges(cx),
283 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
284 );
285
286 // Finalize IME composition.
287 editor.replace_text_in_range(None, "ā", window, cx);
288 assert_eq!(editor.text(cx), "ābcde");
289 assert_eq!(editor.marked_text_ranges(cx), None);
290
291 // IME composition edits are grouped and are undone/redone at once.
292 editor.undo(&Default::default(), window, cx);
293 assert_eq!(editor.text(cx), "abcde");
294 assert_eq!(editor.marked_text_ranges(cx), None);
295 editor.redo(&Default::default(), window, cx);
296 assert_eq!(editor.text(cx), "ābcde");
297 assert_eq!(editor.marked_text_ranges(cx), None);
298
299 // Start a new IME composition.
300 editor.replace_and_mark_text_in_range(Some(0..1), "à", None, window, cx);
301 assert_eq!(
302 editor.marked_text_ranges(cx),
303 Some(vec![OffsetUtf16(0)..OffsetUtf16(1)])
304 );
305
306 // Undoing during an IME composition cancels it.
307 editor.undo(&Default::default(), window, cx);
308 assert_eq!(editor.text(cx), "ābcde");
309 assert_eq!(editor.marked_text_ranges(cx), None);
310
311 // Start a new IME composition with an invalid marked range, ensuring it gets clipped.
312 editor.replace_and_mark_text_in_range(Some(4..999), "è", None, window, cx);
313 assert_eq!(editor.text(cx), "ābcdè");
314 assert_eq!(
315 editor.marked_text_ranges(cx),
316 Some(vec![OffsetUtf16(4)..OffsetUtf16(5)])
317 );
318
319 // Finalize IME composition with an invalid replacement range, ensuring it gets clipped.
320 editor.replace_text_in_range(Some(4..999), "ę", window, cx);
321 assert_eq!(editor.text(cx), "ābcdę");
322 assert_eq!(editor.marked_text_ranges(cx), None);
323
324 // Start a new IME composition with multiple cursors.
325 editor.change_selections(None, window, cx, |s| {
326 s.select_ranges([
327 OffsetUtf16(1)..OffsetUtf16(1),
328 OffsetUtf16(3)..OffsetUtf16(3),
329 OffsetUtf16(5)..OffsetUtf16(5),
330 ])
331 });
332 editor.replace_and_mark_text_in_range(Some(4..5), "XYZ", None, window, cx);
333 assert_eq!(editor.text(cx), "XYZbXYZdXYZ");
334 assert_eq!(
335 editor.marked_text_ranges(cx),
336 Some(vec![
337 OffsetUtf16(0)..OffsetUtf16(3),
338 OffsetUtf16(4)..OffsetUtf16(7),
339 OffsetUtf16(8)..OffsetUtf16(11)
340 ])
341 );
342
343 // Ensure the newly-marked range gets treated as relative to the previously-marked ranges.
344 editor.replace_and_mark_text_in_range(Some(1..2), "1", None, window, cx);
345 assert_eq!(editor.text(cx), "X1ZbX1ZdX1Z");
346 assert_eq!(
347 editor.marked_text_ranges(cx),
348 Some(vec![
349 OffsetUtf16(1)..OffsetUtf16(2),
350 OffsetUtf16(5)..OffsetUtf16(6),
351 OffsetUtf16(9)..OffsetUtf16(10)
352 ])
353 );
354
355 // Finalize IME composition with multiple cursors.
356 editor.replace_text_in_range(Some(9..10), "2", window, cx);
357 assert_eq!(editor.text(cx), "X2ZbX2ZdX2Z");
358 assert_eq!(editor.marked_text_ranges(cx), None);
359
360 editor
361 });
362}
363
364#[gpui::test]
365fn test_selection_with_mouse(cx: &mut TestAppContext) {
366 init_test(cx, |_| {});
367
368 let editor = cx.add_window(|window, cx| {
369 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
370 build_editor(buffer, window, cx)
371 });
372
373 _ = editor.update(cx, |editor, window, cx| {
374 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
375 });
376 assert_eq!(
377 editor
378 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
379 .unwrap(),
380 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
381 );
382
383 _ = editor.update(cx, |editor, window, cx| {
384 editor.update_selection(
385 DisplayPoint::new(DisplayRow(3), 3),
386 0,
387 gpui::Point::<f32>::default(),
388 window,
389 cx,
390 );
391 });
392
393 assert_eq!(
394 editor
395 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
396 .unwrap(),
397 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
398 );
399
400 _ = editor.update(cx, |editor, window, cx| {
401 editor.update_selection(
402 DisplayPoint::new(DisplayRow(1), 1),
403 0,
404 gpui::Point::<f32>::default(),
405 window,
406 cx,
407 );
408 });
409
410 assert_eq!(
411 editor
412 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
413 .unwrap(),
414 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
415 );
416
417 _ = editor.update(cx, |editor, window, cx| {
418 editor.end_selection(window, cx);
419 editor.update_selection(
420 DisplayPoint::new(DisplayRow(3), 3),
421 0,
422 gpui::Point::<f32>::default(),
423 window,
424 cx,
425 );
426 });
427
428 assert_eq!(
429 editor
430 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
431 .unwrap(),
432 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1)]
433 );
434
435 _ = editor.update(cx, |editor, window, cx| {
436 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 3), true, 1, window, cx);
437 editor.update_selection(
438 DisplayPoint::new(DisplayRow(0), 0),
439 0,
440 gpui::Point::<f32>::default(),
441 window,
442 cx,
443 );
444 });
445
446 assert_eq!(
447 editor
448 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
449 .unwrap(),
450 [
451 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(1), 1),
452 DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)
453 ]
454 );
455
456 _ = editor.update(cx, |editor, window, cx| {
457 editor.end_selection(window, cx);
458 });
459
460 assert_eq!(
461 editor
462 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
463 .unwrap(),
464 [DisplayPoint::new(DisplayRow(3), 3)..DisplayPoint::new(DisplayRow(0), 0)]
465 );
466}
467
468#[gpui::test]
469fn test_multiple_cursor_removal(cx: &mut TestAppContext) {
470 init_test(cx, |_| {});
471
472 let editor = cx.add_window(|window, cx| {
473 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
474 build_editor(buffer, window, cx)
475 });
476
477 _ = editor.update(cx, |editor, window, cx| {
478 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), false, 1, window, cx);
479 });
480
481 _ = editor.update(cx, |editor, window, cx| {
482 editor.end_selection(window, cx);
483 });
484
485 _ = editor.update(cx, |editor, window, cx| {
486 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 2), true, 1, window, cx);
487 });
488
489 _ = editor.update(cx, |editor, window, cx| {
490 editor.end_selection(window, cx);
491 });
492
493 assert_eq!(
494 editor
495 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
496 .unwrap(),
497 [
498 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
499 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)
500 ]
501 );
502
503 _ = editor.update(cx, |editor, window, cx| {
504 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 1), true, 1, window, cx);
505 });
506
507 _ = editor.update(cx, |editor, window, cx| {
508 editor.end_selection(window, cx);
509 });
510
511 assert_eq!(
512 editor
513 .update(cx, |editor, _, cx| editor.selections.display_ranges(cx))
514 .unwrap(),
515 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
516 );
517}
518
519#[gpui::test]
520fn test_canceling_pending_selection(cx: &mut TestAppContext) {
521 init_test(cx, |_| {});
522
523 let editor = cx.add_window(|window, cx| {
524 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
525 build_editor(buffer, window, cx)
526 });
527
528 _ = editor.update(cx, |editor, window, cx| {
529 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
530 assert_eq!(
531 editor.selections.display_ranges(cx),
532 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
533 );
534 });
535
536 _ = editor.update(cx, |editor, window, cx| {
537 editor.update_selection(
538 DisplayPoint::new(DisplayRow(3), 3),
539 0,
540 gpui::Point::<f32>::default(),
541 window,
542 cx,
543 );
544 assert_eq!(
545 editor.selections.display_ranges(cx),
546 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
547 );
548 });
549
550 _ = editor.update(cx, |editor, window, cx| {
551 editor.cancel(&Cancel, window, cx);
552 editor.update_selection(
553 DisplayPoint::new(DisplayRow(1), 1),
554 0,
555 gpui::Point::<f32>::default(),
556 window,
557 cx,
558 );
559 assert_eq!(
560 editor.selections.display_ranges(cx),
561 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3)]
562 );
563 });
564}
565
566#[gpui::test]
567fn test_movement_actions_with_pending_selection(cx: &mut TestAppContext) {
568 init_test(cx, |_| {});
569
570 let editor = cx.add_window(|window, cx| {
571 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
572 build_editor(buffer, window, cx)
573 });
574
575 _ = editor.update(cx, |editor, window, cx| {
576 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
577 assert_eq!(
578 editor.selections.display_ranges(cx),
579 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
580 );
581
582 editor.move_down(&Default::default(), window, cx);
583 assert_eq!(
584 editor.selections.display_ranges(cx),
585 [DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 2)]
586 );
587
588 editor.begin_selection(DisplayPoint::new(DisplayRow(2), 2), false, 1, window, cx);
589 assert_eq!(
590 editor.selections.display_ranges(cx),
591 [DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(2), 2)]
592 );
593
594 editor.move_up(&Default::default(), window, cx);
595 assert_eq!(
596 editor.selections.display_ranges(cx),
597 [DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2)]
598 );
599 });
600}
601
602#[gpui::test]
603fn test_clone(cx: &mut TestAppContext) {
604 init_test(cx, |_| {});
605
606 let (text, selection_ranges) = marked_text_ranges(
607 indoc! {"
608 one
609 two
610 threeˇ
611 four
612 fiveˇ
613 "},
614 true,
615 );
616
617 let editor = cx.add_window(|window, cx| {
618 let buffer = MultiBuffer::build_simple(&text, cx);
619 build_editor(buffer, window, cx)
620 });
621
622 _ = editor.update(cx, |editor, window, cx| {
623 editor.change_selections(None, window, cx, |s| {
624 s.select_ranges(selection_ranges.clone())
625 });
626 editor.fold_creases(
627 vec![
628 Crease::simple(Point::new(1, 0)..Point::new(2, 0), FoldPlaceholder::test()),
629 Crease::simple(Point::new(3, 0)..Point::new(4, 0), FoldPlaceholder::test()),
630 ],
631 true,
632 window,
633 cx,
634 );
635 });
636
637 let cloned_editor = editor
638 .update(cx, |editor, _, cx| {
639 cx.open_window(Default::default(), |window, cx| {
640 cx.new(|cx| editor.clone(window, cx))
641 })
642 })
643 .unwrap()
644 .unwrap();
645
646 let snapshot = editor
647 .update(cx, |e, window, cx| e.snapshot(window, cx))
648 .unwrap();
649 let cloned_snapshot = cloned_editor
650 .update(cx, |e, window, cx| e.snapshot(window, cx))
651 .unwrap();
652
653 assert_eq!(
654 cloned_editor
655 .update(cx, |e, _, cx| e.display_text(cx))
656 .unwrap(),
657 editor.update(cx, |e, _, cx| e.display_text(cx)).unwrap()
658 );
659 assert_eq!(
660 cloned_snapshot
661 .folds_in_range(0..text.len())
662 .collect::<Vec<_>>(),
663 snapshot.folds_in_range(0..text.len()).collect::<Vec<_>>(),
664 );
665 assert_set_eq!(
666 cloned_editor
667 .update(cx, |editor, _, cx| editor.selections.ranges::<Point>(cx))
668 .unwrap(),
669 editor
670 .update(cx, |editor, _, cx| editor.selections.ranges(cx))
671 .unwrap()
672 );
673 assert_set_eq!(
674 cloned_editor
675 .update(cx, |e, _window, cx| e.selections.display_ranges(cx))
676 .unwrap(),
677 editor
678 .update(cx, |e, _, cx| e.selections.display_ranges(cx))
679 .unwrap()
680 );
681}
682
683#[gpui::test]
684async fn test_navigation_history(cx: &mut TestAppContext) {
685 init_test(cx, |_| {});
686
687 use workspace::item::Item;
688
689 let fs = FakeFs::new(cx.executor());
690 let project = Project::test(fs, [], cx).await;
691 let workspace = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
692 let pane = workspace
693 .update(cx, |workspace, _, _| workspace.active_pane().clone())
694 .unwrap();
695
696 _ = workspace.update(cx, |_v, window, cx| {
697 cx.new(|cx| {
698 let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
699 let mut editor = build_editor(buffer.clone(), window, cx);
700 let handle = cx.entity();
701 editor.set_nav_history(Some(pane.read(cx).nav_history_for_item(&handle)));
702
703 fn pop_history(editor: &mut Editor, cx: &mut App) -> Option<NavigationEntry> {
704 editor.nav_history.as_mut().unwrap().pop_backward(cx)
705 }
706
707 // Move the cursor a small distance.
708 // Nothing is added to the navigation history.
709 editor.change_selections(None, window, cx, |s| {
710 s.select_display_ranges([
711 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)
712 ])
713 });
714 editor.change_selections(None, window, cx, |s| {
715 s.select_display_ranges([
716 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)
717 ])
718 });
719 assert!(pop_history(&mut editor, cx).is_none());
720
721 // Move the cursor a large distance.
722 // The history can jump back to the previous position.
723 editor.change_selections(None, window, cx, |s| {
724 s.select_display_ranges([
725 DisplayPoint::new(DisplayRow(13), 0)..DisplayPoint::new(DisplayRow(13), 3)
726 ])
727 });
728 let nav_entry = pop_history(&mut editor, cx).unwrap();
729 editor.navigate(nav_entry.data.unwrap(), window, cx);
730 assert_eq!(nav_entry.item.id(), cx.entity_id());
731 assert_eq!(
732 editor.selections.display_ranges(cx),
733 &[DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0)]
734 );
735 assert!(pop_history(&mut editor, cx).is_none());
736
737 // Move the cursor a small distance via the mouse.
738 // Nothing is added to the navigation history.
739 editor.begin_selection(DisplayPoint::new(DisplayRow(5), 0), false, 1, window, cx);
740 editor.end_selection(window, cx);
741 assert_eq!(
742 editor.selections.display_ranges(cx),
743 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
744 );
745 assert!(pop_history(&mut editor, cx).is_none());
746
747 // Move the cursor a large distance via the mouse.
748 // The history can jump back to the previous position.
749 editor.begin_selection(DisplayPoint::new(DisplayRow(15), 0), false, 1, window, cx);
750 editor.end_selection(window, cx);
751 assert_eq!(
752 editor.selections.display_ranges(cx),
753 &[DisplayPoint::new(DisplayRow(15), 0)..DisplayPoint::new(DisplayRow(15), 0)]
754 );
755 let nav_entry = pop_history(&mut editor, cx).unwrap();
756 editor.navigate(nav_entry.data.unwrap(), window, cx);
757 assert_eq!(nav_entry.item.id(), cx.entity_id());
758 assert_eq!(
759 editor.selections.display_ranges(cx),
760 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 0)]
761 );
762 assert!(pop_history(&mut editor, cx).is_none());
763
764 // Set scroll position to check later
765 editor.set_scroll_position(gpui::Point::<f32>::new(5.5, 5.5), window, cx);
766 let original_scroll_position = editor.scroll_manager.anchor();
767
768 // Jump to the end of the document and adjust scroll
769 editor.move_to_end(&MoveToEnd, window, cx);
770 editor.set_scroll_position(gpui::Point::<f32>::new(-2.5, -0.5), window, cx);
771 assert_ne!(editor.scroll_manager.anchor(), original_scroll_position);
772
773 let nav_entry = pop_history(&mut editor, cx).unwrap();
774 editor.navigate(nav_entry.data.unwrap(), window, cx);
775 assert_eq!(editor.scroll_manager.anchor(), original_scroll_position);
776
777 // Ensure we don't panic when navigation data contains invalid anchors *and* points.
778 let mut invalid_anchor = editor.scroll_manager.anchor().anchor;
779 invalid_anchor.text_anchor.buffer_id = BufferId::new(999).ok();
780 let invalid_point = Point::new(9999, 0);
781 editor.navigate(
782 Box::new(NavigationData {
783 cursor_anchor: invalid_anchor,
784 cursor_position: invalid_point,
785 scroll_anchor: ScrollAnchor {
786 anchor: invalid_anchor,
787 offset: Default::default(),
788 },
789 scroll_top_row: invalid_point.row,
790 }),
791 window,
792 cx,
793 );
794 assert_eq!(
795 editor.selections.display_ranges(cx),
796 &[editor.max_point(cx)..editor.max_point(cx)]
797 );
798 assert_eq!(
799 editor.scroll_position(cx),
800 gpui::Point::new(0., editor.max_point(cx).row().as_f32())
801 );
802
803 editor
804 })
805 });
806}
807
808#[gpui::test]
809fn test_cancel(cx: &mut TestAppContext) {
810 init_test(cx, |_| {});
811
812 let editor = cx.add_window(|window, cx| {
813 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\ndddddd\n", cx);
814 build_editor(buffer, window, cx)
815 });
816
817 _ = editor.update(cx, |editor, window, cx| {
818 editor.begin_selection(DisplayPoint::new(DisplayRow(3), 4), false, 1, window, cx);
819 editor.update_selection(
820 DisplayPoint::new(DisplayRow(1), 1),
821 0,
822 gpui::Point::<f32>::default(),
823 window,
824 cx,
825 );
826 editor.end_selection(window, cx);
827
828 editor.begin_selection(DisplayPoint::new(DisplayRow(0), 1), true, 1, window, cx);
829 editor.update_selection(
830 DisplayPoint::new(DisplayRow(0), 3),
831 0,
832 gpui::Point::<f32>::default(),
833 window,
834 cx,
835 );
836 editor.end_selection(window, cx);
837 assert_eq!(
838 editor.selections.display_ranges(cx),
839 [
840 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 3),
841 DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1),
842 ]
843 );
844 });
845
846 _ = editor.update(cx, |editor, window, cx| {
847 editor.cancel(&Cancel, window, cx);
848 assert_eq!(
849 editor.selections.display_ranges(cx),
850 [DisplayPoint::new(DisplayRow(3), 4)..DisplayPoint::new(DisplayRow(1), 1)]
851 );
852 });
853
854 _ = editor.update(cx, |editor, window, cx| {
855 editor.cancel(&Cancel, window, cx);
856 assert_eq!(
857 editor.selections.display_ranges(cx),
858 [DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1)]
859 );
860 });
861}
862
863#[gpui::test]
864fn test_fold_action(cx: &mut TestAppContext) {
865 init_test(cx, |_| {});
866
867 let editor = cx.add_window(|window, cx| {
868 let buffer = MultiBuffer::build_simple(
869 &"
870 impl Foo {
871 // Hello!
872
873 fn a() {
874 1
875 }
876
877 fn b() {
878 2
879 }
880
881 fn c() {
882 3
883 }
884 }
885 "
886 .unindent(),
887 cx,
888 );
889 build_editor(buffer.clone(), window, cx)
890 });
891
892 _ = editor.update(cx, |editor, window, cx| {
893 editor.change_selections(None, window, cx, |s| {
894 s.select_display_ranges([
895 DisplayPoint::new(DisplayRow(7), 0)..DisplayPoint::new(DisplayRow(12), 0)
896 ]);
897 });
898 editor.fold(&Fold, window, cx);
899 assert_eq!(
900 editor.display_text(cx),
901 "
902 impl Foo {
903 // Hello!
904
905 fn a() {
906 1
907 }
908
909 fn b() {⋯
910 }
911
912 fn c() {⋯
913 }
914 }
915 "
916 .unindent(),
917 );
918
919 editor.fold(&Fold, window, cx);
920 assert_eq!(
921 editor.display_text(cx),
922 "
923 impl Foo {⋯
924 }
925 "
926 .unindent(),
927 );
928
929 editor.unfold_lines(&UnfoldLines, window, cx);
930 assert_eq!(
931 editor.display_text(cx),
932 "
933 impl Foo {
934 // Hello!
935
936 fn a() {
937 1
938 }
939
940 fn b() {⋯
941 }
942
943 fn c() {⋯
944 }
945 }
946 "
947 .unindent(),
948 );
949
950 editor.unfold_lines(&UnfoldLines, window, cx);
951 assert_eq!(
952 editor.display_text(cx),
953 editor.buffer.read(cx).read(cx).text()
954 );
955 });
956}
957
958#[gpui::test]
959fn test_fold_action_whitespace_sensitive_language(cx: &mut TestAppContext) {
960 init_test(cx, |_| {});
961
962 let editor = cx.add_window(|window, cx| {
963 let buffer = MultiBuffer::build_simple(
964 &"
965 class Foo:
966 # Hello!
967
968 def a():
969 print(1)
970
971 def b():
972 print(2)
973
974 def c():
975 print(3)
976 "
977 .unindent(),
978 cx,
979 );
980 build_editor(buffer.clone(), window, cx)
981 });
982
983 _ = editor.update(cx, |editor, window, cx| {
984 editor.change_selections(None, window, cx, |s| {
985 s.select_display_ranges([
986 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(10), 0)
987 ]);
988 });
989 editor.fold(&Fold, window, cx);
990 assert_eq!(
991 editor.display_text(cx),
992 "
993 class Foo:
994 # Hello!
995
996 def a():
997 print(1)
998
999 def b():⋯
1000
1001 def c():⋯
1002 "
1003 .unindent(),
1004 );
1005
1006 editor.fold(&Fold, window, cx);
1007 assert_eq!(
1008 editor.display_text(cx),
1009 "
1010 class Foo:⋯
1011 "
1012 .unindent(),
1013 );
1014
1015 editor.unfold_lines(&UnfoldLines, window, cx);
1016 assert_eq!(
1017 editor.display_text(cx),
1018 "
1019 class Foo:
1020 # Hello!
1021
1022 def a():
1023 print(1)
1024
1025 def b():⋯
1026
1027 def c():⋯
1028 "
1029 .unindent(),
1030 );
1031
1032 editor.unfold_lines(&UnfoldLines, window, cx);
1033 assert_eq!(
1034 editor.display_text(cx),
1035 editor.buffer.read(cx).read(cx).text()
1036 );
1037 });
1038}
1039
1040#[gpui::test]
1041fn test_fold_action_multiple_line_breaks(cx: &mut TestAppContext) {
1042 init_test(cx, |_| {});
1043
1044 let editor = cx.add_window(|window, cx| {
1045 let buffer = MultiBuffer::build_simple(
1046 &"
1047 class Foo:
1048 # Hello!
1049
1050 def a():
1051 print(1)
1052
1053 def b():
1054 print(2)
1055
1056
1057 def c():
1058 print(3)
1059
1060
1061 "
1062 .unindent(),
1063 cx,
1064 );
1065 build_editor(buffer.clone(), window, cx)
1066 });
1067
1068 _ = editor.update(cx, |editor, window, cx| {
1069 editor.change_selections(None, window, cx, |s| {
1070 s.select_display_ranges([
1071 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(11), 0)
1072 ]);
1073 });
1074 editor.fold(&Fold, window, cx);
1075 assert_eq!(
1076 editor.display_text(cx),
1077 "
1078 class Foo:
1079 # Hello!
1080
1081 def a():
1082 print(1)
1083
1084 def b():⋯
1085
1086
1087 def c():⋯
1088
1089
1090 "
1091 .unindent(),
1092 );
1093
1094 editor.fold(&Fold, window, cx);
1095 assert_eq!(
1096 editor.display_text(cx),
1097 "
1098 class Foo:⋯
1099
1100
1101 "
1102 .unindent(),
1103 );
1104
1105 editor.unfold_lines(&UnfoldLines, window, cx);
1106 assert_eq!(
1107 editor.display_text(cx),
1108 "
1109 class Foo:
1110 # Hello!
1111
1112 def a():
1113 print(1)
1114
1115 def b():⋯
1116
1117
1118 def c():⋯
1119
1120
1121 "
1122 .unindent(),
1123 );
1124
1125 editor.unfold_lines(&UnfoldLines, window, cx);
1126 assert_eq!(
1127 editor.display_text(cx),
1128 editor.buffer.read(cx).read(cx).text()
1129 );
1130 });
1131}
1132
1133#[gpui::test]
1134fn test_fold_at_level(cx: &mut TestAppContext) {
1135 init_test(cx, |_| {});
1136
1137 let editor = cx.add_window(|window, cx| {
1138 let buffer = MultiBuffer::build_simple(
1139 &"
1140 class Foo:
1141 # Hello!
1142
1143 def a():
1144 print(1)
1145
1146 def b():
1147 print(2)
1148
1149
1150 class Bar:
1151 # World!
1152
1153 def a():
1154 print(1)
1155
1156 def b():
1157 print(2)
1158
1159
1160 "
1161 .unindent(),
1162 cx,
1163 );
1164 build_editor(buffer.clone(), window, cx)
1165 });
1166
1167 _ = editor.update(cx, |editor, window, cx| {
1168 editor.fold_at_level(&FoldAtLevel(2), window, cx);
1169 assert_eq!(
1170 editor.display_text(cx),
1171 "
1172 class Foo:
1173 # Hello!
1174
1175 def a():⋯
1176
1177 def b():⋯
1178
1179
1180 class Bar:
1181 # World!
1182
1183 def a():⋯
1184
1185 def b():⋯
1186
1187
1188 "
1189 .unindent(),
1190 );
1191
1192 editor.fold_at_level(&FoldAtLevel(1), window, cx);
1193 assert_eq!(
1194 editor.display_text(cx),
1195 "
1196 class Foo:⋯
1197
1198
1199 class Bar:⋯
1200
1201
1202 "
1203 .unindent(),
1204 );
1205
1206 editor.unfold_all(&UnfoldAll, window, cx);
1207 editor.fold_at_level(&FoldAtLevel(0), window, cx);
1208 assert_eq!(
1209 editor.display_text(cx),
1210 "
1211 class Foo:
1212 # Hello!
1213
1214 def a():
1215 print(1)
1216
1217 def b():
1218 print(2)
1219
1220
1221 class Bar:
1222 # World!
1223
1224 def a():
1225 print(1)
1226
1227 def b():
1228 print(2)
1229
1230
1231 "
1232 .unindent(),
1233 );
1234
1235 assert_eq!(
1236 editor.display_text(cx),
1237 editor.buffer.read(cx).read(cx).text()
1238 );
1239 });
1240}
1241
1242#[gpui::test]
1243fn test_move_cursor(cx: &mut TestAppContext) {
1244 init_test(cx, |_| {});
1245
1246 let buffer = cx.update(|cx| MultiBuffer::build_simple(&sample_text(6, 6, 'a'), cx));
1247 let editor = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
1248
1249 buffer.update(cx, |buffer, cx| {
1250 buffer.edit(
1251 vec![
1252 (Point::new(1, 0)..Point::new(1, 0), "\t"),
1253 (Point::new(1, 1)..Point::new(1, 1), "\t"),
1254 ],
1255 None,
1256 cx,
1257 );
1258 });
1259 _ = editor.update(cx, |editor, window, cx| {
1260 assert_eq!(
1261 editor.selections.display_ranges(cx),
1262 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1263 );
1264
1265 editor.move_down(&MoveDown, window, cx);
1266 assert_eq!(
1267 editor.selections.display_ranges(cx),
1268 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1269 );
1270
1271 editor.move_right(&MoveRight, window, cx);
1272 assert_eq!(
1273 editor.selections.display_ranges(cx),
1274 &[DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4)]
1275 );
1276
1277 editor.move_left(&MoveLeft, window, cx);
1278 assert_eq!(
1279 editor.selections.display_ranges(cx),
1280 &[DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0)]
1281 );
1282
1283 editor.move_up(&MoveUp, window, cx);
1284 assert_eq!(
1285 editor.selections.display_ranges(cx),
1286 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1287 );
1288
1289 editor.move_to_end(&MoveToEnd, window, cx);
1290 assert_eq!(
1291 editor.selections.display_ranges(cx),
1292 &[DisplayPoint::new(DisplayRow(5), 6)..DisplayPoint::new(DisplayRow(5), 6)]
1293 );
1294
1295 editor.move_to_beginning(&MoveToBeginning, window, cx);
1296 assert_eq!(
1297 editor.selections.display_ranges(cx),
1298 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)]
1299 );
1300
1301 editor.change_selections(None, window, cx, |s| {
1302 s.select_display_ranges([
1303 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 2)
1304 ]);
1305 });
1306 editor.select_to_beginning(&SelectToBeginning, window, cx);
1307 assert_eq!(
1308 editor.selections.display_ranges(cx),
1309 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 0)]
1310 );
1311
1312 editor.select_to_end(&SelectToEnd, window, cx);
1313 assert_eq!(
1314 editor.selections.display_ranges(cx),
1315 &[DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(5), 6)]
1316 );
1317 });
1318}
1319
1320#[gpui::test]
1321fn test_move_cursor_multibyte(cx: &mut TestAppContext) {
1322 init_test(cx, |_| {});
1323
1324 let editor = cx.add_window(|window, cx| {
1325 let buffer = MultiBuffer::build_simple("🟥🟧🟨🟩🟦🟪\nabcde\nαβγδε", cx);
1326 build_editor(buffer.clone(), window, cx)
1327 });
1328
1329 assert_eq!('🟥'.len_utf8(), 4);
1330 assert_eq!('α'.len_utf8(), 2);
1331
1332 _ = editor.update(cx, |editor, window, cx| {
1333 editor.fold_creases(
1334 vec![
1335 Crease::simple(Point::new(0, 8)..Point::new(0, 16), FoldPlaceholder::test()),
1336 Crease::simple(Point::new(1, 2)..Point::new(1, 4), FoldPlaceholder::test()),
1337 Crease::simple(Point::new(2, 4)..Point::new(2, 8), FoldPlaceholder::test()),
1338 ],
1339 true,
1340 window,
1341 cx,
1342 );
1343 assert_eq!(editor.display_text(cx), "🟥🟧⋯🟦🟪\nab⋯e\nαβ⋯ε");
1344
1345 editor.move_right(&MoveRight, window, cx);
1346 assert_eq!(
1347 editor.selections.display_ranges(cx),
1348 &[empty_range(0, "🟥".len())]
1349 );
1350 editor.move_right(&MoveRight, window, cx);
1351 assert_eq!(
1352 editor.selections.display_ranges(cx),
1353 &[empty_range(0, "🟥🟧".len())]
1354 );
1355 editor.move_right(&MoveRight, window, cx);
1356 assert_eq!(
1357 editor.selections.display_ranges(cx),
1358 &[empty_range(0, "🟥🟧⋯".len())]
1359 );
1360
1361 editor.move_down(&MoveDown, window, cx);
1362 assert_eq!(
1363 editor.selections.display_ranges(cx),
1364 &[empty_range(1, "ab⋯e".len())]
1365 );
1366 editor.move_left(&MoveLeft, window, cx);
1367 assert_eq!(
1368 editor.selections.display_ranges(cx),
1369 &[empty_range(1, "ab⋯".len())]
1370 );
1371 editor.move_left(&MoveLeft, window, cx);
1372 assert_eq!(
1373 editor.selections.display_ranges(cx),
1374 &[empty_range(1, "ab".len())]
1375 );
1376 editor.move_left(&MoveLeft, window, cx);
1377 assert_eq!(
1378 editor.selections.display_ranges(cx),
1379 &[empty_range(1, "a".len())]
1380 );
1381
1382 editor.move_down(&MoveDown, window, cx);
1383 assert_eq!(
1384 editor.selections.display_ranges(cx),
1385 &[empty_range(2, "α".len())]
1386 );
1387 editor.move_right(&MoveRight, window, cx);
1388 assert_eq!(
1389 editor.selections.display_ranges(cx),
1390 &[empty_range(2, "αβ".len())]
1391 );
1392 editor.move_right(&MoveRight, window, cx);
1393 assert_eq!(
1394 editor.selections.display_ranges(cx),
1395 &[empty_range(2, "αβ⋯".len())]
1396 );
1397 editor.move_right(&MoveRight, window, cx);
1398 assert_eq!(
1399 editor.selections.display_ranges(cx),
1400 &[empty_range(2, "αβ⋯ε".len())]
1401 );
1402
1403 editor.move_up(&MoveUp, window, cx);
1404 assert_eq!(
1405 editor.selections.display_ranges(cx),
1406 &[empty_range(1, "ab⋯e".len())]
1407 );
1408 editor.move_down(&MoveDown, window, cx);
1409 assert_eq!(
1410 editor.selections.display_ranges(cx),
1411 &[empty_range(2, "αβ⋯ε".len())]
1412 );
1413 editor.move_up(&MoveUp, window, cx);
1414 assert_eq!(
1415 editor.selections.display_ranges(cx),
1416 &[empty_range(1, "ab⋯e".len())]
1417 );
1418
1419 editor.move_up(&MoveUp, window, cx);
1420 assert_eq!(
1421 editor.selections.display_ranges(cx),
1422 &[empty_range(0, "🟥🟧".len())]
1423 );
1424 editor.move_left(&MoveLeft, window, cx);
1425 assert_eq!(
1426 editor.selections.display_ranges(cx),
1427 &[empty_range(0, "🟥".len())]
1428 );
1429 editor.move_left(&MoveLeft, window, cx);
1430 assert_eq!(
1431 editor.selections.display_ranges(cx),
1432 &[empty_range(0, "".len())]
1433 );
1434 });
1435}
1436
1437#[gpui::test]
1438fn test_move_cursor_different_line_lengths(cx: &mut TestAppContext) {
1439 init_test(cx, |_| {});
1440
1441 let editor = cx.add_window(|window, cx| {
1442 let buffer = MultiBuffer::build_simple("ⓐⓑⓒⓓⓔ\nabcd\nαβγ\nabcd\nⓐⓑⓒⓓⓔ\n", cx);
1443 build_editor(buffer.clone(), window, cx)
1444 });
1445 _ = editor.update(cx, |editor, window, cx| {
1446 editor.change_selections(None, window, cx, |s| {
1447 s.select_display_ranges([empty_range(0, "ⓐⓑⓒⓓⓔ".len())]);
1448 });
1449
1450 // moving above start of document should move selection to start of document,
1451 // but the next move down should still be at the original goal_x
1452 editor.move_up(&MoveUp, window, cx);
1453 assert_eq!(
1454 editor.selections.display_ranges(cx),
1455 &[empty_range(0, "".len())]
1456 );
1457
1458 editor.move_down(&MoveDown, window, cx);
1459 assert_eq!(
1460 editor.selections.display_ranges(cx),
1461 &[empty_range(1, "abcd".len())]
1462 );
1463
1464 editor.move_down(&MoveDown, window, cx);
1465 assert_eq!(
1466 editor.selections.display_ranges(cx),
1467 &[empty_range(2, "αβγ".len())]
1468 );
1469
1470 editor.move_down(&MoveDown, window, cx);
1471 assert_eq!(
1472 editor.selections.display_ranges(cx),
1473 &[empty_range(3, "abcd".len())]
1474 );
1475
1476 editor.move_down(&MoveDown, window, cx);
1477 assert_eq!(
1478 editor.selections.display_ranges(cx),
1479 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1480 );
1481
1482 // moving past end of document should not change goal_x
1483 editor.move_down(&MoveDown, window, cx);
1484 assert_eq!(
1485 editor.selections.display_ranges(cx),
1486 &[empty_range(5, "".len())]
1487 );
1488
1489 editor.move_down(&MoveDown, window, cx);
1490 assert_eq!(
1491 editor.selections.display_ranges(cx),
1492 &[empty_range(5, "".len())]
1493 );
1494
1495 editor.move_up(&MoveUp, window, cx);
1496 assert_eq!(
1497 editor.selections.display_ranges(cx),
1498 &[empty_range(4, "ⓐⓑⓒⓓⓔ".len())]
1499 );
1500
1501 editor.move_up(&MoveUp, window, cx);
1502 assert_eq!(
1503 editor.selections.display_ranges(cx),
1504 &[empty_range(3, "abcd".len())]
1505 );
1506
1507 editor.move_up(&MoveUp, window, cx);
1508 assert_eq!(
1509 editor.selections.display_ranges(cx),
1510 &[empty_range(2, "αβγ".len())]
1511 );
1512 });
1513}
1514
1515#[gpui::test]
1516fn test_beginning_end_of_line(cx: &mut TestAppContext) {
1517 init_test(cx, |_| {});
1518 let move_to_beg = MoveToBeginningOfLine {
1519 stop_at_soft_wraps: true,
1520 stop_at_indent: true,
1521 };
1522
1523 let delete_to_beg = DeleteToBeginningOfLine {
1524 stop_at_indent: false,
1525 };
1526
1527 let move_to_end = MoveToEndOfLine {
1528 stop_at_soft_wraps: true,
1529 };
1530
1531 let editor = cx.add_window(|window, cx| {
1532 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1533 build_editor(buffer, window, cx)
1534 });
1535 _ = editor.update(cx, |editor, window, cx| {
1536 editor.change_selections(None, window, cx, |s| {
1537 s.select_display_ranges([
1538 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1539 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1540 ]);
1541 });
1542 });
1543
1544 _ = editor.update(cx, |editor, window, cx| {
1545 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1546 assert_eq!(
1547 editor.selections.display_ranges(cx),
1548 &[
1549 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1550 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1551 ]
1552 );
1553 });
1554
1555 _ = editor.update(cx, |editor, window, cx| {
1556 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1557 assert_eq!(
1558 editor.selections.display_ranges(cx),
1559 &[
1560 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1561 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1562 ]
1563 );
1564 });
1565
1566 _ = editor.update(cx, |editor, window, cx| {
1567 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1568 assert_eq!(
1569 editor.selections.display_ranges(cx),
1570 &[
1571 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1572 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1573 ]
1574 );
1575 });
1576
1577 _ = editor.update(cx, |editor, window, cx| {
1578 editor.move_to_end_of_line(&move_to_end, window, cx);
1579 assert_eq!(
1580 editor.selections.display_ranges(cx),
1581 &[
1582 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1583 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1584 ]
1585 );
1586 });
1587
1588 // Moving to the end of line again is a no-op.
1589 _ = editor.update(cx, |editor, window, cx| {
1590 editor.move_to_end_of_line(&move_to_end, window, cx);
1591 assert_eq!(
1592 editor.selections.display_ranges(cx),
1593 &[
1594 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
1595 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
1596 ]
1597 );
1598 });
1599
1600 _ = editor.update(cx, |editor, window, cx| {
1601 editor.move_left(&MoveLeft, window, cx);
1602 editor.select_to_beginning_of_line(
1603 &SelectToBeginningOfLine {
1604 stop_at_soft_wraps: true,
1605 stop_at_indent: true,
1606 },
1607 window,
1608 cx,
1609 );
1610 assert_eq!(
1611 editor.selections.display_ranges(cx),
1612 &[
1613 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1614 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1615 ]
1616 );
1617 });
1618
1619 _ = editor.update(cx, |editor, window, cx| {
1620 editor.select_to_beginning_of_line(
1621 &SelectToBeginningOfLine {
1622 stop_at_soft_wraps: true,
1623 stop_at_indent: true,
1624 },
1625 window,
1626 cx,
1627 );
1628 assert_eq!(
1629 editor.selections.display_ranges(cx),
1630 &[
1631 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1632 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1633 ]
1634 );
1635 });
1636
1637 _ = editor.update(cx, |editor, window, cx| {
1638 editor.select_to_beginning_of_line(
1639 &SelectToBeginningOfLine {
1640 stop_at_soft_wraps: true,
1641 stop_at_indent: true,
1642 },
1643 window,
1644 cx,
1645 );
1646 assert_eq!(
1647 editor.selections.display_ranges(cx),
1648 &[
1649 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1650 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1651 ]
1652 );
1653 });
1654
1655 _ = editor.update(cx, |editor, window, cx| {
1656 editor.select_to_end_of_line(
1657 &SelectToEndOfLine {
1658 stop_at_soft_wraps: true,
1659 },
1660 window,
1661 cx,
1662 );
1663 assert_eq!(
1664 editor.selections.display_ranges(cx),
1665 &[
1666 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
1667 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 5),
1668 ]
1669 );
1670 });
1671
1672 _ = editor.update(cx, |editor, window, cx| {
1673 editor.delete_to_end_of_line(&DeleteToEndOfLine, window, cx);
1674 assert_eq!(editor.display_text(cx), "ab\n de");
1675 assert_eq!(
1676 editor.selections.display_ranges(cx),
1677 &[
1678 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
1679 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1680 ]
1681 );
1682 });
1683
1684 _ = editor.update(cx, |editor, window, cx| {
1685 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1686 assert_eq!(editor.display_text(cx), "\n");
1687 assert_eq!(
1688 editor.selections.display_ranges(cx),
1689 &[
1690 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1691 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1692 ]
1693 );
1694 });
1695}
1696
1697#[gpui::test]
1698fn test_beginning_end_of_line_ignore_soft_wrap(cx: &mut TestAppContext) {
1699 init_test(cx, |_| {});
1700 let move_to_beg = MoveToBeginningOfLine {
1701 stop_at_soft_wraps: false,
1702 stop_at_indent: false,
1703 };
1704
1705 let move_to_end = MoveToEndOfLine {
1706 stop_at_soft_wraps: false,
1707 };
1708
1709 let editor = cx.add_window(|window, cx| {
1710 let buffer = MultiBuffer::build_simple("thequickbrownfox\njumpedoverthelazydogs", cx);
1711 build_editor(buffer, window, cx)
1712 });
1713
1714 _ = editor.update(cx, |editor, window, cx| {
1715 editor.set_wrap_width(Some(140.0.into()), cx);
1716
1717 // We expect the following lines after wrapping
1718 // ```
1719 // thequickbrownfox
1720 // jumpedoverthelazydo
1721 // gs
1722 // ```
1723 // The final `gs` was soft-wrapped onto a new line.
1724 assert_eq!(
1725 "thequickbrownfox\njumpedoverthelaz\nydogs",
1726 editor.display_text(cx),
1727 );
1728
1729 // First, let's assert behavior on the first line, that was not soft-wrapped.
1730 // Start the cursor at the `k` on the first line
1731 editor.change_selections(None, window, cx, |s| {
1732 s.select_display_ranges([
1733 DisplayPoint::new(DisplayRow(0), 7)..DisplayPoint::new(DisplayRow(0), 7)
1734 ]);
1735 });
1736
1737 // Moving to the beginning of the line should put us at the beginning of the line.
1738 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1739 assert_eq!(
1740 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),],
1741 editor.selections.display_ranges(cx)
1742 );
1743
1744 // Moving to the end of the line should put us at the end of the line.
1745 editor.move_to_end_of_line(&move_to_end, window, cx);
1746 assert_eq!(
1747 vec![DisplayPoint::new(DisplayRow(0), 16)..DisplayPoint::new(DisplayRow(0), 16),],
1748 editor.selections.display_ranges(cx)
1749 );
1750
1751 // Now, let's assert behavior on the second line, that ended up being soft-wrapped.
1752 // Start the cursor at the last line (`y` that was wrapped to a new line)
1753 editor.change_selections(None, window, cx, |s| {
1754 s.select_display_ranges([
1755 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0)
1756 ]);
1757 });
1758
1759 // Moving to the beginning of the line should put us at the start of the second line of
1760 // display text, i.e., the `j`.
1761 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1762 assert_eq!(
1763 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1764 editor.selections.display_ranges(cx)
1765 );
1766
1767 // Moving to the beginning of the line again should be a no-op.
1768 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1769 assert_eq!(
1770 vec![DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),],
1771 editor.selections.display_ranges(cx)
1772 );
1773
1774 // Moving to the end of the line should put us right after the `s` that was soft-wrapped to the
1775 // next display line.
1776 editor.move_to_end_of_line(&move_to_end, window, cx);
1777 assert_eq!(
1778 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1779 editor.selections.display_ranges(cx)
1780 );
1781
1782 // Moving to the end of the line again should be a no-op.
1783 editor.move_to_end_of_line(&move_to_end, window, cx);
1784 assert_eq!(
1785 vec![DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),],
1786 editor.selections.display_ranges(cx)
1787 );
1788 });
1789}
1790
1791#[gpui::test]
1792fn test_beginning_of_line_stop_at_indent(cx: &mut TestAppContext) {
1793 init_test(cx, |_| {});
1794
1795 let move_to_beg = MoveToBeginningOfLine {
1796 stop_at_soft_wraps: true,
1797 stop_at_indent: true,
1798 };
1799
1800 let select_to_beg = SelectToBeginningOfLine {
1801 stop_at_soft_wraps: true,
1802 stop_at_indent: true,
1803 };
1804
1805 let delete_to_beg = DeleteToBeginningOfLine {
1806 stop_at_indent: true,
1807 };
1808
1809 let move_to_end = MoveToEndOfLine {
1810 stop_at_soft_wraps: false,
1811 };
1812
1813 let editor = cx.add_window(|window, cx| {
1814 let buffer = MultiBuffer::build_simple("abc\n def", cx);
1815 build_editor(buffer, window, cx)
1816 });
1817
1818 _ = editor.update(cx, |editor, window, cx| {
1819 editor.change_selections(None, window, cx, |s| {
1820 s.select_display_ranges([
1821 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
1822 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 4),
1823 ]);
1824 });
1825
1826 // Moving to the beginning of the line should put the first cursor at the beginning of the line,
1827 // and the second cursor at the first non-whitespace character in the line.
1828 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1829 assert_eq!(
1830 editor.selections.display_ranges(cx),
1831 &[
1832 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1833 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1834 ]
1835 );
1836
1837 // Moving to the beginning of the line again should be a no-op for the first cursor,
1838 // and should move the second cursor to the beginning of the line.
1839 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1840 assert_eq!(
1841 editor.selections.display_ranges(cx),
1842 &[
1843 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1844 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
1845 ]
1846 );
1847
1848 // Moving to the beginning of the line again should still be a no-op for the first cursor,
1849 // and should move the second cursor back to the first non-whitespace character in the line.
1850 editor.move_to_beginning_of_line(&move_to_beg, window, cx);
1851 assert_eq!(
1852 editor.selections.display_ranges(cx),
1853 &[
1854 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
1855 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
1856 ]
1857 );
1858
1859 // Selecting to the beginning of the line should select to the beginning of the line for the first cursor,
1860 // and to the first non-whitespace character in the line for the second cursor.
1861 editor.move_to_end_of_line(&move_to_end, window, cx);
1862 editor.move_left(&MoveLeft, window, cx);
1863 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1864 assert_eq!(
1865 editor.selections.display_ranges(cx),
1866 &[
1867 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1868 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 2),
1869 ]
1870 );
1871
1872 // Selecting to the beginning of the line again should be a no-op for the first cursor,
1873 // and should select to the beginning of the line for the second cursor.
1874 editor.select_to_beginning_of_line(&select_to_beg, window, cx);
1875 assert_eq!(
1876 editor.selections.display_ranges(cx),
1877 &[
1878 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 0),
1879 DisplayPoint::new(DisplayRow(1), 4)..DisplayPoint::new(DisplayRow(1), 0),
1880 ]
1881 );
1882
1883 // Deleting to the beginning of the line should delete to the beginning of the line for the first cursor,
1884 // and should delete to the first non-whitespace character in the line for the second cursor.
1885 editor.move_to_end_of_line(&move_to_end, window, cx);
1886 editor.move_left(&MoveLeft, window, cx);
1887 editor.delete_to_beginning_of_line(&delete_to_beg, window, cx);
1888 assert_eq!(editor.text(cx), "c\n f");
1889 });
1890}
1891
1892#[gpui::test]
1893fn test_prev_next_word_boundary(cx: &mut TestAppContext) {
1894 init_test(cx, |_| {});
1895
1896 let editor = cx.add_window(|window, cx| {
1897 let buffer = MultiBuffer::build_simple("use std::str::{foo, bar}\n\n {baz.qux()}", cx);
1898 build_editor(buffer, window, cx)
1899 });
1900 _ = editor.update(cx, |editor, window, cx| {
1901 editor.change_selections(None, window, cx, |s| {
1902 s.select_display_ranges([
1903 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11),
1904 DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4),
1905 ])
1906 });
1907
1908 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1909 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1910
1911 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1912 assert_selection_ranges("use stdˇ::str::{foo, bar}\n\n ˇ{baz.qux()}", editor, cx);
1913
1914 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1915 assert_selection_ranges("use ˇstd::str::{foo, bar}\n\nˇ {baz.qux()}", editor, cx);
1916
1917 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1918 assert_selection_ranges("ˇuse std::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1919
1920 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
1921 assert_selection_ranges("ˇuse std::str::{foo, barˇ}\n\n {baz.qux()}", editor, cx);
1922
1923 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1924 assert_selection_ranges("useˇ std::str::{foo, bar}ˇ\n\n {baz.qux()}", editor, cx);
1925
1926 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1927 assert_selection_ranges("use stdˇ::str::{foo, bar}\nˇ\n {baz.qux()}", editor, cx);
1928
1929 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1930 assert_selection_ranges("use std::ˇstr::{foo, bar}\n\n {ˇbaz.qux()}", editor, cx);
1931
1932 editor.move_right(&MoveRight, window, cx);
1933 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1934 assert_selection_ranges(
1935 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1936 editor,
1937 cx,
1938 );
1939
1940 editor.select_to_previous_word_start(&SelectToPreviousWordStart, window, cx);
1941 assert_selection_ranges(
1942 "use std«ˇ::s»tr::{foo, bar}\n\n «ˇ{b»az.qux()}",
1943 editor,
1944 cx,
1945 );
1946
1947 editor.select_to_next_word_end(&SelectToNextWordEnd, window, cx);
1948 assert_selection_ranges(
1949 "use std::«ˇs»tr::{foo, bar}\n\n {«ˇb»az.qux()}",
1950 editor,
1951 cx,
1952 );
1953 });
1954}
1955
1956#[gpui::test]
1957fn test_prev_next_word_bounds_with_soft_wrap(cx: &mut TestAppContext) {
1958 init_test(cx, |_| {});
1959
1960 let editor = cx.add_window(|window, cx| {
1961 let buffer = MultiBuffer::build_simple("use one::{\n two::three::four::five\n};", cx);
1962 build_editor(buffer, window, cx)
1963 });
1964
1965 _ = editor.update(cx, |editor, window, cx| {
1966 editor.set_wrap_width(Some(140.0.into()), cx);
1967 assert_eq!(
1968 editor.display_text(cx),
1969 "use one::{\n two::three::\n four::five\n};"
1970 );
1971
1972 editor.change_selections(None, window, cx, |s| {
1973 s.select_display_ranges([
1974 DisplayPoint::new(DisplayRow(1), 7)..DisplayPoint::new(DisplayRow(1), 7)
1975 ]);
1976 });
1977
1978 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1979 assert_eq!(
1980 editor.selections.display_ranges(cx),
1981 &[DisplayPoint::new(DisplayRow(1), 9)..DisplayPoint::new(DisplayRow(1), 9)]
1982 );
1983
1984 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1985 assert_eq!(
1986 editor.selections.display_ranges(cx),
1987 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
1988 );
1989
1990 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1991 assert_eq!(
1992 editor.selections.display_ranges(cx),
1993 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
1994 );
1995
1996 editor.move_to_next_word_end(&MoveToNextWordEnd, window, cx);
1997 assert_eq!(
1998 editor.selections.display_ranges(cx),
1999 &[DisplayPoint::new(DisplayRow(2), 8)..DisplayPoint::new(DisplayRow(2), 8)]
2000 );
2001
2002 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2003 assert_eq!(
2004 editor.selections.display_ranges(cx),
2005 &[DisplayPoint::new(DisplayRow(2), 4)..DisplayPoint::new(DisplayRow(2), 4)]
2006 );
2007
2008 editor.move_to_previous_word_start(&MoveToPreviousWordStart, window, cx);
2009 assert_eq!(
2010 editor.selections.display_ranges(cx),
2011 &[DisplayPoint::new(DisplayRow(1), 14)..DisplayPoint::new(DisplayRow(1), 14)]
2012 );
2013 });
2014}
2015
2016#[gpui::test]
2017async fn test_move_start_of_paragraph_end_of_paragraph(cx: &mut TestAppContext) {
2018 init_test(cx, |_| {});
2019 let mut cx = EditorTestContext::new(cx).await;
2020
2021 let line_height = cx.editor(|editor, window, _| {
2022 editor
2023 .style()
2024 .unwrap()
2025 .text
2026 .line_height_in_pixels(window.rem_size())
2027 });
2028 cx.simulate_window_resize(cx.window, size(px(100.), 4. * line_height));
2029
2030 cx.set_state(
2031 &r#"ˇone
2032 two
2033
2034 three
2035 fourˇ
2036 five
2037
2038 six"#
2039 .unindent(),
2040 );
2041
2042 cx.update_editor(|editor, window, cx| {
2043 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2044 });
2045 cx.assert_editor_state(
2046 &r#"one
2047 two
2048 ˇ
2049 three
2050 four
2051 five
2052 ˇ
2053 six"#
2054 .unindent(),
2055 );
2056
2057 cx.update_editor(|editor, window, cx| {
2058 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2059 });
2060 cx.assert_editor_state(
2061 &r#"one
2062 two
2063
2064 three
2065 four
2066 five
2067 ˇ
2068 sixˇ"#
2069 .unindent(),
2070 );
2071
2072 cx.update_editor(|editor, window, cx| {
2073 editor.move_to_end_of_paragraph(&MoveToEndOfParagraph, window, cx)
2074 });
2075 cx.assert_editor_state(
2076 &r#"one
2077 two
2078
2079 three
2080 four
2081 five
2082
2083 sixˇ"#
2084 .unindent(),
2085 );
2086
2087 cx.update_editor(|editor, window, cx| {
2088 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2089 });
2090 cx.assert_editor_state(
2091 &r#"one
2092 two
2093
2094 three
2095 four
2096 five
2097 ˇ
2098 six"#
2099 .unindent(),
2100 );
2101
2102 cx.update_editor(|editor, window, cx| {
2103 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2104 });
2105 cx.assert_editor_state(
2106 &r#"one
2107 two
2108 ˇ
2109 three
2110 four
2111 five
2112
2113 six"#
2114 .unindent(),
2115 );
2116
2117 cx.update_editor(|editor, window, cx| {
2118 editor.move_to_start_of_paragraph(&MoveToStartOfParagraph, window, cx)
2119 });
2120 cx.assert_editor_state(
2121 &r#"ˇone
2122 two
2123
2124 three
2125 four
2126 five
2127
2128 six"#
2129 .unindent(),
2130 );
2131}
2132
2133#[gpui::test]
2134async fn test_scroll_page_up_page_down(cx: &mut TestAppContext) {
2135 init_test(cx, |_| {});
2136 let mut cx = EditorTestContext::new(cx).await;
2137 let line_height = cx.editor(|editor, window, _| {
2138 editor
2139 .style()
2140 .unwrap()
2141 .text
2142 .line_height_in_pixels(window.rem_size())
2143 });
2144 let window = cx.window;
2145 cx.simulate_window_resize(window, size(px(1000.), 4. * line_height + px(0.5)));
2146
2147 cx.set_state(
2148 r#"ˇone
2149 two
2150 three
2151 four
2152 five
2153 six
2154 seven
2155 eight
2156 nine
2157 ten
2158 "#,
2159 );
2160
2161 cx.update_editor(|editor, window, cx| {
2162 assert_eq!(
2163 editor.snapshot(window, cx).scroll_position(),
2164 gpui::Point::new(0., 0.)
2165 );
2166 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2167 assert_eq!(
2168 editor.snapshot(window, cx).scroll_position(),
2169 gpui::Point::new(0., 3.)
2170 );
2171 editor.scroll_screen(&ScrollAmount::Page(1.), window, cx);
2172 assert_eq!(
2173 editor.snapshot(window, cx).scroll_position(),
2174 gpui::Point::new(0., 6.)
2175 );
2176 editor.scroll_screen(&ScrollAmount::Page(-1.), window, cx);
2177 assert_eq!(
2178 editor.snapshot(window, cx).scroll_position(),
2179 gpui::Point::new(0., 3.)
2180 );
2181
2182 editor.scroll_screen(&ScrollAmount::Page(-0.5), window, cx);
2183 assert_eq!(
2184 editor.snapshot(window, cx).scroll_position(),
2185 gpui::Point::new(0., 1.)
2186 );
2187 editor.scroll_screen(&ScrollAmount::Page(0.5), window, cx);
2188 assert_eq!(
2189 editor.snapshot(window, cx).scroll_position(),
2190 gpui::Point::new(0., 3.)
2191 );
2192 });
2193}
2194
2195#[gpui::test]
2196async fn test_autoscroll(cx: &mut TestAppContext) {
2197 init_test(cx, |_| {});
2198 let mut cx = EditorTestContext::new(cx).await;
2199
2200 let line_height = cx.update_editor(|editor, window, cx| {
2201 editor.set_vertical_scroll_margin(2, cx);
2202 editor
2203 .style()
2204 .unwrap()
2205 .text
2206 .line_height_in_pixels(window.rem_size())
2207 });
2208 let window = cx.window;
2209 cx.simulate_window_resize(window, size(px(1000.), 6. * line_height));
2210
2211 cx.set_state(
2212 r#"ˇone
2213 two
2214 three
2215 four
2216 five
2217 six
2218 seven
2219 eight
2220 nine
2221 ten
2222 "#,
2223 );
2224 cx.update_editor(|editor, window, cx| {
2225 assert_eq!(
2226 editor.snapshot(window, cx).scroll_position(),
2227 gpui::Point::new(0., 0.0)
2228 );
2229 });
2230
2231 // Add a cursor below the visible area. Since both cursors cannot fit
2232 // on screen, the editor autoscrolls to reveal the newest cursor, and
2233 // allows the vertical scroll margin below that cursor.
2234 cx.update_editor(|editor, window, cx| {
2235 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2236 selections.select_ranges([
2237 Point::new(0, 0)..Point::new(0, 0),
2238 Point::new(6, 0)..Point::new(6, 0),
2239 ]);
2240 })
2241 });
2242 cx.update_editor(|editor, window, cx| {
2243 assert_eq!(
2244 editor.snapshot(window, cx).scroll_position(),
2245 gpui::Point::new(0., 3.0)
2246 );
2247 });
2248
2249 // Move down. The editor cursor scrolls down to track the newest cursor.
2250 cx.update_editor(|editor, window, cx| {
2251 editor.move_down(&Default::default(), window, cx);
2252 });
2253 cx.update_editor(|editor, window, cx| {
2254 assert_eq!(
2255 editor.snapshot(window, cx).scroll_position(),
2256 gpui::Point::new(0., 4.0)
2257 );
2258 });
2259
2260 // Add a cursor above the visible area. Since both cursors fit on screen,
2261 // the editor scrolls to show both.
2262 cx.update_editor(|editor, window, cx| {
2263 editor.change_selections(Some(Autoscroll::fit()), window, cx, |selections| {
2264 selections.select_ranges([
2265 Point::new(1, 0)..Point::new(1, 0),
2266 Point::new(6, 0)..Point::new(6, 0),
2267 ]);
2268 })
2269 });
2270 cx.update_editor(|editor, window, cx| {
2271 assert_eq!(
2272 editor.snapshot(window, cx).scroll_position(),
2273 gpui::Point::new(0., 1.0)
2274 );
2275 });
2276}
2277
2278#[gpui::test]
2279async fn test_move_page_up_page_down(cx: &mut TestAppContext) {
2280 init_test(cx, |_| {});
2281 let mut cx = EditorTestContext::new(cx).await;
2282
2283 let line_height = cx.editor(|editor, window, _cx| {
2284 editor
2285 .style()
2286 .unwrap()
2287 .text
2288 .line_height_in_pixels(window.rem_size())
2289 });
2290 let window = cx.window;
2291 cx.simulate_window_resize(window, size(px(100.), 4. * line_height));
2292 cx.set_state(
2293 &r#"
2294 ˇone
2295 two
2296 threeˇ
2297 four
2298 five
2299 six
2300 seven
2301 eight
2302 nine
2303 ten
2304 "#
2305 .unindent(),
2306 );
2307
2308 cx.update_editor(|editor, window, cx| {
2309 editor.move_page_down(&MovePageDown::default(), window, cx)
2310 });
2311 cx.assert_editor_state(
2312 &r#"
2313 one
2314 two
2315 three
2316 ˇfour
2317 five
2318 sixˇ
2319 seven
2320 eight
2321 nine
2322 ten
2323 "#
2324 .unindent(),
2325 );
2326
2327 cx.update_editor(|editor, window, cx| {
2328 editor.move_page_down(&MovePageDown::default(), window, cx)
2329 });
2330 cx.assert_editor_state(
2331 &r#"
2332 one
2333 two
2334 three
2335 four
2336 five
2337 six
2338 ˇseven
2339 eight
2340 nineˇ
2341 ten
2342 "#
2343 .unindent(),
2344 );
2345
2346 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2347 cx.assert_editor_state(
2348 &r#"
2349 one
2350 two
2351 three
2352 ˇfour
2353 five
2354 sixˇ
2355 seven
2356 eight
2357 nine
2358 ten
2359 "#
2360 .unindent(),
2361 );
2362
2363 cx.update_editor(|editor, window, cx| editor.move_page_up(&MovePageUp::default(), window, cx));
2364 cx.assert_editor_state(
2365 &r#"
2366 ˇone
2367 two
2368 threeˇ
2369 four
2370 five
2371 six
2372 seven
2373 eight
2374 nine
2375 ten
2376 "#
2377 .unindent(),
2378 );
2379
2380 // Test select collapsing
2381 cx.update_editor(|editor, window, cx| {
2382 editor.move_page_down(&MovePageDown::default(), window, cx);
2383 editor.move_page_down(&MovePageDown::default(), window, cx);
2384 editor.move_page_down(&MovePageDown::default(), window, cx);
2385 });
2386 cx.assert_editor_state(
2387 &r#"
2388 one
2389 two
2390 three
2391 four
2392 five
2393 six
2394 seven
2395 eight
2396 nine
2397 ˇten
2398 ˇ"#
2399 .unindent(),
2400 );
2401}
2402
2403#[gpui::test]
2404async fn test_delete_to_beginning_of_line(cx: &mut TestAppContext) {
2405 init_test(cx, |_| {});
2406 let mut cx = EditorTestContext::new(cx).await;
2407 cx.set_state("one «two threeˇ» four");
2408 cx.update_editor(|editor, window, cx| {
2409 editor.delete_to_beginning_of_line(
2410 &DeleteToBeginningOfLine {
2411 stop_at_indent: false,
2412 },
2413 window,
2414 cx,
2415 );
2416 assert_eq!(editor.text(cx), " four");
2417 });
2418}
2419
2420#[gpui::test]
2421fn test_delete_to_word_boundary(cx: &mut TestAppContext) {
2422 init_test(cx, |_| {});
2423
2424 let editor = cx.add_window(|window, cx| {
2425 let buffer = MultiBuffer::build_simple("one two three four", cx);
2426 build_editor(buffer.clone(), window, cx)
2427 });
2428
2429 _ = editor.update(cx, |editor, window, cx| {
2430 editor.change_selections(None, window, cx, |s| {
2431 s.select_display_ranges([
2432 // an empty selection - the preceding word fragment is deleted
2433 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2434 // characters selected - they are deleted
2435 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 12),
2436 ])
2437 });
2438 editor.delete_to_previous_word_start(
2439 &DeleteToPreviousWordStart {
2440 ignore_newlines: false,
2441 },
2442 window,
2443 cx,
2444 );
2445 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e two te four");
2446 });
2447
2448 _ = editor.update(cx, |editor, window, cx| {
2449 editor.change_selections(None, window, cx, |s| {
2450 s.select_display_ranges([
2451 // an empty selection - the following word fragment is deleted
2452 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 3),
2453 // characters selected - they are deleted
2454 DisplayPoint::new(DisplayRow(0), 9)..DisplayPoint::new(DisplayRow(0), 10),
2455 ])
2456 });
2457 editor.delete_to_next_word_end(
2458 &DeleteToNextWordEnd {
2459 ignore_newlines: false,
2460 },
2461 window,
2462 cx,
2463 );
2464 assert_eq!(editor.buffer.read(cx).read(cx).text(), "e t te our");
2465 });
2466}
2467
2468#[gpui::test]
2469fn test_delete_to_previous_word_start_or_newline(cx: &mut TestAppContext) {
2470 init_test(cx, |_| {});
2471
2472 let editor = cx.add_window(|window, cx| {
2473 let buffer = MultiBuffer::build_simple("one\n2\nthree\n4", cx);
2474 build_editor(buffer.clone(), window, cx)
2475 });
2476 let del_to_prev_word_start = DeleteToPreviousWordStart {
2477 ignore_newlines: false,
2478 };
2479 let del_to_prev_word_start_ignore_newlines = DeleteToPreviousWordStart {
2480 ignore_newlines: true,
2481 };
2482
2483 _ = editor.update(cx, |editor, window, cx| {
2484 editor.change_selections(None, window, cx, |s| {
2485 s.select_display_ranges([
2486 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1)
2487 ])
2488 });
2489 editor.delete_to_previous_word_start(&del_to_prev_word_start, window, cx);
2490 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n2\nthree\n");
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");
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\n");
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");
2497 editor.delete_to_previous_word_start(&del_to_prev_word_start_ignore_newlines, window, cx);
2498 assert_eq!(editor.buffer.read(cx).read(cx).text(), "one\n");
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(), "");
2501 });
2502}
2503
2504#[gpui::test]
2505fn test_delete_to_next_word_end_or_newline(cx: &mut TestAppContext) {
2506 init_test(cx, |_| {});
2507
2508 let editor = cx.add_window(|window, cx| {
2509 let buffer = MultiBuffer::build_simple("\none\n two\nthree\n four", cx);
2510 build_editor(buffer.clone(), window, cx)
2511 });
2512 let del_to_next_word_end = DeleteToNextWordEnd {
2513 ignore_newlines: false,
2514 };
2515 let del_to_next_word_end_ignore_newlines = DeleteToNextWordEnd {
2516 ignore_newlines: true,
2517 };
2518
2519 _ = editor.update(cx, |editor, window, cx| {
2520 editor.change_selections(None, window, cx, |s| {
2521 s.select_display_ranges([
2522 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0)
2523 ])
2524 });
2525 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2526 assert_eq!(
2527 editor.buffer.read(cx).read(cx).text(),
2528 "one\n two\nthree\n four"
2529 );
2530 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2531 assert_eq!(
2532 editor.buffer.read(cx).read(cx).text(),
2533 "\n two\nthree\n four"
2534 );
2535 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2536 assert_eq!(
2537 editor.buffer.read(cx).read(cx).text(),
2538 "two\nthree\n four"
2539 );
2540 editor.delete_to_next_word_end(&del_to_next_word_end, window, cx);
2541 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\nthree\n four");
2542 editor.delete_to_next_word_end(&del_to_next_word_end_ignore_newlines, window, cx);
2543 assert_eq!(editor.buffer.read(cx).read(cx).text(), "\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(), "");
2546 });
2547}
2548
2549#[gpui::test]
2550fn test_newline(cx: &mut TestAppContext) {
2551 init_test(cx, |_| {});
2552
2553 let editor = cx.add_window(|window, cx| {
2554 let buffer = MultiBuffer::build_simple("aaaa\n bbbb\n", cx);
2555 build_editor(buffer.clone(), window, cx)
2556 });
2557
2558 _ = editor.update(cx, |editor, window, cx| {
2559 editor.change_selections(None, window, cx, |s| {
2560 s.select_display_ranges([
2561 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
2562 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
2563 DisplayPoint::new(DisplayRow(1), 6)..DisplayPoint::new(DisplayRow(1), 6),
2564 ])
2565 });
2566
2567 editor.newline(&Newline, window, cx);
2568 assert_eq!(editor.text(cx), "aa\naa\n \n bb\n bb\n");
2569 });
2570}
2571
2572#[gpui::test]
2573fn test_newline_with_old_selections(cx: &mut TestAppContext) {
2574 init_test(cx, |_| {});
2575
2576 let editor = cx.add_window(|window, cx| {
2577 let buffer = MultiBuffer::build_simple(
2578 "
2579 a
2580 b(
2581 X
2582 )
2583 c(
2584 X
2585 )
2586 "
2587 .unindent()
2588 .as_str(),
2589 cx,
2590 );
2591 let mut editor = build_editor(buffer.clone(), window, cx);
2592 editor.change_selections(None, window, cx, |s| {
2593 s.select_ranges([
2594 Point::new(2, 4)..Point::new(2, 5),
2595 Point::new(5, 4)..Point::new(5, 5),
2596 ])
2597 });
2598 editor
2599 });
2600
2601 _ = editor.update(cx, |editor, window, cx| {
2602 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2603 editor.buffer.update(cx, |buffer, cx| {
2604 buffer.edit(
2605 [
2606 (Point::new(1, 2)..Point::new(3, 0), ""),
2607 (Point::new(4, 2)..Point::new(6, 0), ""),
2608 ],
2609 None,
2610 cx,
2611 );
2612 assert_eq!(
2613 buffer.read(cx).text(),
2614 "
2615 a
2616 b()
2617 c()
2618 "
2619 .unindent()
2620 );
2621 });
2622 assert_eq!(
2623 editor.selections.ranges(cx),
2624 &[
2625 Point::new(1, 2)..Point::new(1, 2),
2626 Point::new(2, 2)..Point::new(2, 2),
2627 ],
2628 );
2629
2630 editor.newline(&Newline, window, cx);
2631 assert_eq!(
2632 editor.text(cx),
2633 "
2634 a
2635 b(
2636 )
2637 c(
2638 )
2639 "
2640 .unindent()
2641 );
2642
2643 // The selections are moved after the inserted newlines
2644 assert_eq!(
2645 editor.selections.ranges(cx),
2646 &[
2647 Point::new(2, 0)..Point::new(2, 0),
2648 Point::new(4, 0)..Point::new(4, 0),
2649 ],
2650 );
2651 });
2652}
2653
2654#[gpui::test]
2655async fn test_newline_above(cx: &mut TestAppContext) {
2656 init_test(cx, |settings| {
2657 settings.defaults.tab_size = NonZeroU32::new(4)
2658 });
2659
2660 let language = Arc::new(
2661 Language::new(
2662 LanguageConfig::default(),
2663 Some(tree_sitter_rust::LANGUAGE.into()),
2664 )
2665 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2666 .unwrap(),
2667 );
2668
2669 let mut cx = EditorTestContext::new(cx).await;
2670 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2671 cx.set_state(indoc! {"
2672 const a: ˇA = (
2673 (ˇ
2674 «const_functionˇ»(ˇ),
2675 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2676 )ˇ
2677 ˇ);ˇ
2678 "});
2679
2680 cx.update_editor(|e, window, cx| e.newline_above(&NewlineAbove, window, cx));
2681 cx.assert_editor_state(indoc! {"
2682 ˇ
2683 const a: A = (
2684 ˇ
2685 (
2686 ˇ
2687 ˇ
2688 const_function(),
2689 ˇ
2690 ˇ
2691 ˇ
2692 ˇ
2693 something_else,
2694 ˇ
2695 )
2696 ˇ
2697 ˇ
2698 );
2699 "});
2700}
2701
2702#[gpui::test]
2703async fn test_newline_below(cx: &mut TestAppContext) {
2704 init_test(cx, |settings| {
2705 settings.defaults.tab_size = NonZeroU32::new(4)
2706 });
2707
2708 let language = Arc::new(
2709 Language::new(
2710 LanguageConfig::default(),
2711 Some(tree_sitter_rust::LANGUAGE.into()),
2712 )
2713 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2714 .unwrap(),
2715 );
2716
2717 let mut cx = EditorTestContext::new(cx).await;
2718 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2719 cx.set_state(indoc! {"
2720 const a: ˇA = (
2721 (ˇ
2722 «const_functionˇ»(ˇ),
2723 so«mˇ»et«hˇ»ing_ˇelse,ˇ
2724 )ˇ
2725 ˇ);ˇ
2726 "});
2727
2728 cx.update_editor(|e, window, cx| e.newline_below(&NewlineBelow, window, cx));
2729 cx.assert_editor_state(indoc! {"
2730 const a: A = (
2731 ˇ
2732 (
2733 ˇ
2734 const_function(),
2735 ˇ
2736 ˇ
2737 something_else,
2738 ˇ
2739 ˇ
2740 ˇ
2741 ˇ
2742 )
2743 ˇ
2744 );
2745 ˇ
2746 ˇ
2747 "});
2748}
2749
2750#[gpui::test]
2751async fn test_newline_comments(cx: &mut TestAppContext) {
2752 init_test(cx, |settings| {
2753 settings.defaults.tab_size = NonZeroU32::new(4)
2754 });
2755
2756 let language = Arc::new(Language::new(
2757 LanguageConfig {
2758 line_comments: vec!["//".into()],
2759 ..LanguageConfig::default()
2760 },
2761 None,
2762 ));
2763 {
2764 let mut cx = EditorTestContext::new(cx).await;
2765 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2766 cx.set_state(indoc! {"
2767 // Fooˇ
2768 "});
2769
2770 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2771 cx.assert_editor_state(indoc! {"
2772 // Foo
2773 //ˇ
2774 "});
2775 // Ensure that if cursor is before the comment start, we do not actually insert a comment prefix.
2776 cx.set_state(indoc! {"
2777 ˇ// Foo
2778 "});
2779 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
2780 cx.assert_editor_state(indoc! {"
2781
2782 ˇ// Foo
2783 "});
2784 }
2785 // Ensure that comment continuations can be disabled.
2786 update_test_language_settings(cx, |settings| {
2787 settings.defaults.extend_comment_on_newline = Some(false);
2788 });
2789 let mut cx = EditorTestContext::new(cx).await;
2790 cx.set_state(indoc! {"
2791 // Fooˇ
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
2800#[gpui::test]
2801fn test_insert_with_old_selections(cx: &mut TestAppContext) {
2802 init_test(cx, |_| {});
2803
2804 let editor = cx.add_window(|window, cx| {
2805 let buffer = MultiBuffer::build_simple("a( X ), b( Y ), c( Z )", cx);
2806 let mut editor = build_editor(buffer.clone(), window, cx);
2807 editor.change_selections(None, window, cx, |s| {
2808 s.select_ranges([3..4, 11..12, 19..20])
2809 });
2810 editor
2811 });
2812
2813 _ = editor.update(cx, |editor, window, cx| {
2814 // Edit the buffer directly, deleting ranges surrounding the editor's selections
2815 editor.buffer.update(cx, |buffer, cx| {
2816 buffer.edit([(2..5, ""), (10..13, ""), (18..21, "")], None, cx);
2817 assert_eq!(buffer.read(cx).text(), "a(), b(), c()".unindent());
2818 });
2819 assert_eq!(editor.selections.ranges(cx), &[2..2, 7..7, 12..12],);
2820
2821 editor.insert("Z", window, cx);
2822 assert_eq!(editor.text(cx), "a(Z), b(Z), c(Z)");
2823
2824 // The selections are moved after the inserted characters
2825 assert_eq!(editor.selections.ranges(cx), &[3..3, 9..9, 15..15],);
2826 });
2827}
2828
2829#[gpui::test]
2830async fn test_tab(cx: &mut TestAppContext) {
2831 init_test(cx, |settings| {
2832 settings.defaults.tab_size = NonZeroU32::new(3)
2833 });
2834
2835 let mut cx = EditorTestContext::new(cx).await;
2836 cx.set_state(indoc! {"
2837 ˇabˇc
2838 ˇ🏀ˇ🏀ˇefg
2839 dˇ
2840 "});
2841 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2842 cx.assert_editor_state(indoc! {"
2843 ˇab ˇc
2844 ˇ🏀 ˇ🏀 ˇefg
2845 d ˇ
2846 "});
2847
2848 cx.set_state(indoc! {"
2849 a
2850 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2851 "});
2852 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2853 cx.assert_editor_state(indoc! {"
2854 a
2855 «🏀ˇ»🏀«🏀ˇ»🏀«🏀ˇ»
2856 "});
2857}
2858
2859#[gpui::test]
2860async fn test_tab_in_leading_whitespace_auto_indents_lines(cx: &mut TestAppContext) {
2861 init_test(cx, |_| {});
2862
2863 let mut cx = EditorTestContext::new(cx).await;
2864 let language = Arc::new(
2865 Language::new(
2866 LanguageConfig::default(),
2867 Some(tree_sitter_rust::LANGUAGE.into()),
2868 )
2869 .with_indents_query(r#"(_ "(" ")" @end) @indent"#)
2870 .unwrap(),
2871 );
2872 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
2873
2874 // test when all cursors are not at suggested indent
2875 // then simply move to their suggested indent location
2876 cx.set_state(indoc! {"
2877 const a: B = (
2878 c(
2879 ˇ
2880 ˇ )
2881 );
2882 "});
2883 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2884 cx.assert_editor_state(indoc! {"
2885 const a: B = (
2886 c(
2887 ˇ
2888 ˇ)
2889 );
2890 "});
2891
2892 // test cursor already at suggested indent not moving when
2893 // other cursors are yet to reach their suggested indents
2894 cx.set_state(indoc! {"
2895 ˇ
2896 const a: B = (
2897 c(
2898 d(
2899 ˇ
2900 )
2901 ˇ
2902 ˇ )
2903 );
2904 "});
2905 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2906 cx.assert_editor_state(indoc! {"
2907 ˇ
2908 const a: B = (
2909 c(
2910 d(
2911 ˇ
2912 )
2913 ˇ
2914 ˇ)
2915 );
2916 "});
2917 // test when all cursors are at suggested indent then tab is inserted
2918 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2919 cx.assert_editor_state(indoc! {"
2920 ˇ
2921 const a: B = (
2922 c(
2923 d(
2924 ˇ
2925 )
2926 ˇ
2927 ˇ)
2928 );
2929 "});
2930
2931 // test when current indent is less than suggested indent,
2932 // we adjust line to match suggested indent and move cursor to it
2933 //
2934 // when no other cursor is at word boundary, all of them should move
2935 cx.set_state(indoc! {"
2936 const a: B = (
2937 c(
2938 d(
2939 ˇ
2940 ˇ )
2941 ˇ )
2942 );
2943 "});
2944 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2945 cx.assert_editor_state(indoc! {"
2946 const a: B = (
2947 c(
2948 d(
2949 ˇ
2950 ˇ)
2951 ˇ)
2952 );
2953 "});
2954
2955 // test when current indent is less than suggested indent,
2956 // we adjust line to match suggested indent and move cursor to it
2957 //
2958 // when some other cursor is at word boundary, it should not move
2959 cx.set_state(indoc! {"
2960 const a: B = (
2961 c(
2962 d(
2963 ˇ
2964 ˇ )
2965 ˇ)
2966 );
2967 "});
2968 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2969 cx.assert_editor_state(indoc! {"
2970 const a: B = (
2971 c(
2972 d(
2973 ˇ
2974 ˇ)
2975 ˇ)
2976 );
2977 "});
2978
2979 // test when current indent is more than suggested indent,
2980 // we just move cursor to current indent instead of suggested indent
2981 //
2982 // when no other cursor is at word boundary, all of them should move
2983 cx.set_state(indoc! {"
2984 const a: B = (
2985 c(
2986 d(
2987 ˇ
2988 ˇ )
2989 ˇ )
2990 );
2991 "});
2992 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
2993 cx.assert_editor_state(indoc! {"
2994 const a: B = (
2995 c(
2996 d(
2997 ˇ
2998 ˇ)
2999 ˇ)
3000 );
3001 "});
3002 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3003 cx.assert_editor_state(indoc! {"
3004 const a: B = (
3005 c(
3006 d(
3007 ˇ
3008 ˇ)
3009 ˇ)
3010 );
3011 "});
3012
3013 // test when current indent is more than suggested indent,
3014 // we just move cursor to current indent instead of suggested indent
3015 //
3016 // when some other cursor is at word boundary, it doesn't move
3017 cx.set_state(indoc! {"
3018 const a: B = (
3019 c(
3020 d(
3021 ˇ
3022 ˇ )
3023 ˇ)
3024 );
3025 "});
3026 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3027 cx.assert_editor_state(indoc! {"
3028 const a: B = (
3029 c(
3030 d(
3031 ˇ
3032 ˇ)
3033 ˇ)
3034 );
3035 "});
3036
3037 // handle auto-indent when there are multiple cursors on the same line
3038 cx.set_state(indoc! {"
3039 const a: B = (
3040 c(
3041 ˇ ˇ
3042 ˇ )
3043 );
3044 "});
3045 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3046 cx.assert_editor_state(indoc! {"
3047 const a: B = (
3048 c(
3049 ˇ
3050 ˇ)
3051 );
3052 "});
3053}
3054
3055#[gpui::test]
3056async fn test_tab_with_mixed_whitespace_txt(cx: &mut TestAppContext) {
3057 init_test(cx, |settings| {
3058 settings.defaults.tab_size = NonZeroU32::new(3)
3059 });
3060
3061 let mut cx = EditorTestContext::new(cx).await;
3062 cx.set_state(indoc! {"
3063 ˇ
3064 \t ˇ
3065 \t ˇ
3066 \t ˇ
3067 \t \t\t \t \t\t \t\t \t \t ˇ
3068 "});
3069
3070 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3071 cx.assert_editor_state(indoc! {"
3072 ˇ
3073 \t ˇ
3074 \t ˇ
3075 \t ˇ
3076 \t \t\t \t \t\t \t\t \t \t ˇ
3077 "});
3078}
3079
3080#[gpui::test]
3081async fn test_tab_with_mixed_whitespace_rust(cx: &mut TestAppContext) {
3082 init_test(cx, |settings| {
3083 settings.defaults.tab_size = NonZeroU32::new(4)
3084 });
3085
3086 let language = Arc::new(
3087 Language::new(
3088 LanguageConfig::default(),
3089 Some(tree_sitter_rust::LANGUAGE.into()),
3090 )
3091 .with_indents_query(r#"(_ "{" "}" @end) @indent"#)
3092 .unwrap(),
3093 );
3094
3095 let mut cx = EditorTestContext::new(cx).await;
3096 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
3097 cx.set_state(indoc! {"
3098 fn a() {
3099 if b {
3100 \t ˇc
3101 }
3102 }
3103 "});
3104
3105 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3106 cx.assert_editor_state(indoc! {"
3107 fn a() {
3108 if b {
3109 ˇc
3110 }
3111 }
3112 "});
3113}
3114
3115#[gpui::test]
3116async fn test_indent_outdent(cx: &mut TestAppContext) {
3117 init_test(cx, |settings| {
3118 settings.defaults.tab_size = NonZeroU32::new(4);
3119 });
3120
3121 let mut cx = EditorTestContext::new(cx).await;
3122
3123 cx.set_state(indoc! {"
3124 «oneˇ» «twoˇ»
3125 three
3126 four
3127 "});
3128 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3129 cx.assert_editor_state(indoc! {"
3130 «oneˇ» «twoˇ»
3131 three
3132 four
3133 "});
3134
3135 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3136 cx.assert_editor_state(indoc! {"
3137 «oneˇ» «twoˇ»
3138 three
3139 four
3140 "});
3141
3142 // select across line ending
3143 cx.set_state(indoc! {"
3144 one two
3145 t«hree
3146 ˇ» four
3147 "});
3148 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3149 cx.assert_editor_state(indoc! {"
3150 one two
3151 t«hree
3152 ˇ» four
3153 "});
3154
3155 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3156 cx.assert_editor_state(indoc! {"
3157 one two
3158 t«hree
3159 ˇ» four
3160 "});
3161
3162 // Ensure that indenting/outdenting works when the cursor is at column 0.
3163 cx.set_state(indoc! {"
3164 one two
3165 ˇthree
3166 four
3167 "});
3168 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3169 cx.assert_editor_state(indoc! {"
3170 one two
3171 ˇthree
3172 four
3173 "});
3174
3175 cx.set_state(indoc! {"
3176 one two
3177 ˇ three
3178 four
3179 "});
3180 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3181 cx.assert_editor_state(indoc! {"
3182 one two
3183 ˇthree
3184 four
3185 "});
3186}
3187
3188#[gpui::test]
3189async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
3190 init_test(cx, |settings| {
3191 settings.defaults.hard_tabs = Some(true);
3192 });
3193
3194 let mut cx = EditorTestContext::new(cx).await;
3195
3196 // select two ranges on one line
3197 cx.set_state(indoc! {"
3198 «oneˇ» «twoˇ»
3199 three
3200 four
3201 "});
3202 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3203 cx.assert_editor_state(indoc! {"
3204 \t«oneˇ» «twoˇ»
3205 three
3206 four
3207 "});
3208 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3209 cx.assert_editor_state(indoc! {"
3210 \t\t«oneˇ» «twoˇ»
3211 three
3212 four
3213 "});
3214 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3215 cx.assert_editor_state(indoc! {"
3216 \t«oneˇ» «twoˇ»
3217 three
3218 four
3219 "});
3220 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3221 cx.assert_editor_state(indoc! {"
3222 «oneˇ» «twoˇ»
3223 three
3224 four
3225 "});
3226
3227 // select across a line ending
3228 cx.set_state(indoc! {"
3229 one two
3230 t«hree
3231 ˇ»four
3232 "});
3233 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3234 cx.assert_editor_state(indoc! {"
3235 one two
3236 \tt«hree
3237 ˇ»four
3238 "});
3239 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3240 cx.assert_editor_state(indoc! {"
3241 one two
3242 \t\tt«hree
3243 ˇ»four
3244 "});
3245 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3246 cx.assert_editor_state(indoc! {"
3247 one two
3248 \tt«hree
3249 ˇ»four
3250 "});
3251 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3252 cx.assert_editor_state(indoc! {"
3253 one two
3254 t«hree
3255 ˇ»four
3256 "});
3257
3258 // Ensure that indenting/outdenting works when the cursor is at column 0.
3259 cx.set_state(indoc! {"
3260 one two
3261 ˇthree
3262 four
3263 "});
3264 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3265 cx.assert_editor_state(indoc! {"
3266 one two
3267 ˇthree
3268 four
3269 "});
3270 cx.update_editor(|e, window, cx| e.tab(&Tab, window, cx));
3271 cx.assert_editor_state(indoc! {"
3272 one two
3273 \tˇthree
3274 four
3275 "});
3276 cx.update_editor(|e, window, cx| e.backtab(&Backtab, window, cx));
3277 cx.assert_editor_state(indoc! {"
3278 one two
3279 ˇthree
3280 four
3281 "});
3282}
3283
3284#[gpui::test]
3285fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
3286 init_test(cx, |settings| {
3287 settings.languages.extend([
3288 (
3289 "TOML".into(),
3290 LanguageSettingsContent {
3291 tab_size: NonZeroU32::new(2),
3292 ..Default::default()
3293 },
3294 ),
3295 (
3296 "Rust".into(),
3297 LanguageSettingsContent {
3298 tab_size: NonZeroU32::new(4),
3299 ..Default::default()
3300 },
3301 ),
3302 ]);
3303 });
3304
3305 let toml_language = Arc::new(Language::new(
3306 LanguageConfig {
3307 name: "TOML".into(),
3308 ..Default::default()
3309 },
3310 None,
3311 ));
3312 let rust_language = Arc::new(Language::new(
3313 LanguageConfig {
3314 name: "Rust".into(),
3315 ..Default::default()
3316 },
3317 None,
3318 ));
3319
3320 let toml_buffer =
3321 cx.new(|cx| Buffer::local("a = 1\nb = 2\n", cx).with_language(toml_language, cx));
3322 let rust_buffer =
3323 cx.new(|cx| Buffer::local("const c: usize = 3;\n", cx).with_language(rust_language, cx));
3324 let multibuffer = cx.new(|cx| {
3325 let mut multibuffer = MultiBuffer::new(ReadWrite);
3326 multibuffer.push_excerpts(
3327 toml_buffer.clone(),
3328 [ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0))],
3329 cx,
3330 );
3331 multibuffer.push_excerpts(
3332 rust_buffer.clone(),
3333 [ExcerptRange::new(Point::new(0, 0)..Point::new(1, 0))],
3334 cx,
3335 );
3336 multibuffer
3337 });
3338
3339 cx.add_window(|window, cx| {
3340 let mut editor = build_editor(multibuffer, window, cx);
3341
3342 assert_eq!(
3343 editor.text(cx),
3344 indoc! {"
3345 a = 1
3346 b = 2
3347
3348 const c: usize = 3;
3349 "}
3350 );
3351
3352 select_ranges(
3353 &mut editor,
3354 indoc! {"
3355 «aˇ» = 1
3356 b = 2
3357
3358 «const c:ˇ» usize = 3;
3359 "},
3360 window,
3361 cx,
3362 );
3363
3364 editor.tab(&Tab, window, cx);
3365 assert_text_with_selections(
3366 &mut editor,
3367 indoc! {"
3368 «aˇ» = 1
3369 b = 2
3370
3371 «const c:ˇ» usize = 3;
3372 "},
3373 cx,
3374 );
3375 editor.backtab(&Backtab, window, cx);
3376 assert_text_with_selections(
3377 &mut editor,
3378 indoc! {"
3379 «aˇ» = 1
3380 b = 2
3381
3382 «const c:ˇ» usize = 3;
3383 "},
3384 cx,
3385 );
3386
3387 editor
3388 });
3389}
3390
3391#[gpui::test]
3392async fn test_backspace(cx: &mut TestAppContext) {
3393 init_test(cx, |_| {});
3394
3395 let mut cx = EditorTestContext::new(cx).await;
3396
3397 // Basic backspace
3398 cx.set_state(indoc! {"
3399 onˇe two three
3400 fou«rˇ» five six
3401 seven «ˇeight nine
3402 »ten
3403 "});
3404 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3405 cx.assert_editor_state(indoc! {"
3406 oˇe two three
3407 fouˇ five six
3408 seven ˇten
3409 "});
3410
3411 // Test backspace inside and around indents
3412 cx.set_state(indoc! {"
3413 zero
3414 ˇone
3415 ˇtwo
3416 ˇ ˇ ˇ three
3417 ˇ ˇ four
3418 "});
3419 cx.update_editor(|e, window, cx| e.backspace(&Backspace, window, cx));
3420 cx.assert_editor_state(indoc! {"
3421 zero
3422 ˇone
3423 ˇtwo
3424 ˇ threeˇ four
3425 "});
3426}
3427
3428#[gpui::test]
3429async fn test_delete(cx: &mut TestAppContext) {
3430 init_test(cx, |_| {});
3431
3432 let mut cx = EditorTestContext::new(cx).await;
3433 cx.set_state(indoc! {"
3434 onˇe two three
3435 fou«rˇ» five six
3436 seven «ˇeight nine
3437 »ten
3438 "});
3439 cx.update_editor(|e, window, cx| e.delete(&Delete, window, cx));
3440 cx.assert_editor_state(indoc! {"
3441 onˇ two three
3442 fouˇ five six
3443 seven ˇten
3444 "});
3445}
3446
3447#[gpui::test]
3448fn test_delete_line(cx: &mut TestAppContext) {
3449 init_test(cx, |_| {});
3450
3451 let editor = cx.add_window(|window, cx| {
3452 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3453 build_editor(buffer, window, cx)
3454 });
3455 _ = editor.update(cx, |editor, window, cx| {
3456 editor.change_selections(None, window, cx, |s| {
3457 s.select_display_ranges([
3458 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
3459 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
3460 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
3461 ])
3462 });
3463 editor.delete_line(&DeleteLine, window, cx);
3464 assert_eq!(editor.display_text(cx), "ghi");
3465 assert_eq!(
3466 editor.selections.display_ranges(cx),
3467 vec![
3468 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 0),
3469 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)
3470 ]
3471 );
3472 });
3473
3474 let editor = cx.add_window(|window, cx| {
3475 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
3476 build_editor(buffer, window, cx)
3477 });
3478 _ = editor.update(cx, |editor, window, cx| {
3479 editor.change_selections(None, window, cx, |s| {
3480 s.select_display_ranges([
3481 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(0), 1)
3482 ])
3483 });
3484 editor.delete_line(&DeleteLine, window, cx);
3485 assert_eq!(editor.display_text(cx), "ghi\n");
3486 assert_eq!(
3487 editor.selections.display_ranges(cx),
3488 vec![DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1)]
3489 );
3490 });
3491}
3492
3493#[gpui::test]
3494fn test_join_lines_with_single_selection(cx: &mut TestAppContext) {
3495 init_test(cx, |_| {});
3496
3497 cx.add_window(|window, cx| {
3498 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3499 let mut editor = build_editor(buffer.clone(), window, cx);
3500 let buffer = buffer.read(cx).as_singleton().unwrap();
3501
3502 assert_eq!(
3503 editor.selections.ranges::<Point>(cx),
3504 &[Point::new(0, 0)..Point::new(0, 0)]
3505 );
3506
3507 // When on single line, replace newline at end by space
3508 editor.join_lines(&JoinLines, window, cx);
3509 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3510 assert_eq!(
3511 editor.selections.ranges::<Point>(cx),
3512 &[Point::new(0, 3)..Point::new(0, 3)]
3513 );
3514
3515 // When multiple lines are selected, remove newlines that are spanned by the selection
3516 editor.change_selections(None, window, cx, |s| {
3517 s.select_ranges([Point::new(0, 5)..Point::new(2, 2)])
3518 });
3519 editor.join_lines(&JoinLines, window, cx);
3520 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc ddd\n\n");
3521 assert_eq!(
3522 editor.selections.ranges::<Point>(cx),
3523 &[Point::new(0, 11)..Point::new(0, 11)]
3524 );
3525
3526 // Undo should be transactional
3527 editor.undo(&Undo, window, cx);
3528 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n\n");
3529 assert_eq!(
3530 editor.selections.ranges::<Point>(cx),
3531 &[Point::new(0, 5)..Point::new(2, 2)]
3532 );
3533
3534 // When joining an empty line don't insert a space
3535 editor.change_selections(None, window, cx, |s| {
3536 s.select_ranges([Point::new(2, 1)..Point::new(2, 2)])
3537 });
3538 editor.join_lines(&JoinLines, window, cx);
3539 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd\n");
3540 assert_eq!(
3541 editor.selections.ranges::<Point>(cx),
3542 [Point::new(2, 3)..Point::new(2, 3)]
3543 );
3544
3545 // We can remove trailing newlines
3546 editor.join_lines(&JoinLines, window, cx);
3547 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3548 assert_eq!(
3549 editor.selections.ranges::<Point>(cx),
3550 [Point::new(2, 3)..Point::new(2, 3)]
3551 );
3552
3553 // We don't blow up on the last line
3554 editor.join_lines(&JoinLines, window, cx);
3555 assert_eq!(buffer.read(cx).text(), "aaa bbb\nccc\nddd");
3556 assert_eq!(
3557 editor.selections.ranges::<Point>(cx),
3558 [Point::new(2, 3)..Point::new(2, 3)]
3559 );
3560
3561 // reset to test indentation
3562 editor.buffer.update(cx, |buffer, cx| {
3563 buffer.edit(
3564 [
3565 (Point::new(1, 0)..Point::new(1, 2), " "),
3566 (Point::new(2, 0)..Point::new(2, 3), " \n\td"),
3567 ],
3568 None,
3569 cx,
3570 )
3571 });
3572
3573 // We remove any leading spaces
3574 assert_eq!(buffer.read(cx).text(), "aaa bbb\n c\n \n\td");
3575 editor.change_selections(None, window, cx, |s| {
3576 s.select_ranges([Point::new(0, 1)..Point::new(0, 1)])
3577 });
3578 editor.join_lines(&JoinLines, window, cx);
3579 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n \n\td");
3580
3581 // We don't insert a space for a line containing only spaces
3582 editor.join_lines(&JoinLines, window, cx);
3583 assert_eq!(buffer.read(cx).text(), "aaa bbb c\n\td");
3584
3585 // We ignore any leading tabs
3586 editor.join_lines(&JoinLines, window, cx);
3587 assert_eq!(buffer.read(cx).text(), "aaa bbb c d");
3588
3589 editor
3590 });
3591}
3592
3593#[gpui::test]
3594fn test_join_lines_with_multi_selection(cx: &mut TestAppContext) {
3595 init_test(cx, |_| {});
3596
3597 cx.add_window(|window, cx| {
3598 let buffer = MultiBuffer::build_simple("aaa\nbbb\nccc\nddd\n\n", cx);
3599 let mut editor = build_editor(buffer.clone(), window, cx);
3600 let buffer = buffer.read(cx).as_singleton().unwrap();
3601
3602 editor.change_selections(None, window, cx, |s| {
3603 s.select_ranges([
3604 Point::new(0, 2)..Point::new(1, 1),
3605 Point::new(1, 2)..Point::new(1, 2),
3606 Point::new(3, 1)..Point::new(3, 2),
3607 ])
3608 });
3609
3610 editor.join_lines(&JoinLines, window, cx);
3611 assert_eq!(buffer.read(cx).text(), "aaa bbb ccc\nddd\n");
3612
3613 assert_eq!(
3614 editor.selections.ranges::<Point>(cx),
3615 [
3616 Point::new(0, 7)..Point::new(0, 7),
3617 Point::new(1, 3)..Point::new(1, 3)
3618 ]
3619 );
3620 editor
3621 });
3622}
3623
3624#[gpui::test]
3625async fn test_join_lines_with_git_diff_base(executor: BackgroundExecutor, cx: &mut TestAppContext) {
3626 init_test(cx, |_| {});
3627
3628 let mut cx = EditorTestContext::new(cx).await;
3629
3630 let diff_base = r#"
3631 Line 0
3632 Line 1
3633 Line 2
3634 Line 3
3635 "#
3636 .unindent();
3637
3638 cx.set_state(
3639 &r#"
3640 ˇLine 0
3641 Line 1
3642 Line 2
3643 Line 3
3644 "#
3645 .unindent(),
3646 );
3647
3648 cx.set_head_text(&diff_base);
3649 executor.run_until_parked();
3650
3651 // Join lines
3652 cx.update_editor(|editor, window, cx| {
3653 editor.join_lines(&JoinLines, window, cx);
3654 });
3655 executor.run_until_parked();
3656
3657 cx.assert_editor_state(
3658 &r#"
3659 Line 0ˇ Line 1
3660 Line 2
3661 Line 3
3662 "#
3663 .unindent(),
3664 );
3665 // Join again
3666 cx.update_editor(|editor, window, cx| {
3667 editor.join_lines(&JoinLines, window, cx);
3668 });
3669 executor.run_until_parked();
3670
3671 cx.assert_editor_state(
3672 &r#"
3673 Line 0 Line 1ˇ Line 2
3674 Line 3
3675 "#
3676 .unindent(),
3677 );
3678}
3679
3680#[gpui::test]
3681async fn test_custom_newlines_cause_no_false_positive_diffs(
3682 executor: BackgroundExecutor,
3683 cx: &mut TestAppContext,
3684) {
3685 init_test(cx, |_| {});
3686 let mut cx = EditorTestContext::new(cx).await;
3687 cx.set_state("Line 0\r\nLine 1\rˇ\nLine 2\r\nLine 3");
3688 cx.set_head_text("Line 0\r\nLine 1\r\nLine 2\r\nLine 3");
3689 executor.run_until_parked();
3690
3691 cx.update_editor(|editor, window, cx| {
3692 let snapshot = editor.snapshot(window, cx);
3693 assert_eq!(
3694 snapshot
3695 .buffer_snapshot
3696 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
3697 .collect::<Vec<_>>(),
3698 Vec::new(),
3699 "Should not have any diffs for files with custom newlines"
3700 );
3701 });
3702}
3703
3704#[gpui::test]
3705async fn test_manipulate_lines_with_single_selection(cx: &mut TestAppContext) {
3706 init_test(cx, |_| {});
3707
3708 let mut cx = EditorTestContext::new(cx).await;
3709
3710 // Test sort_lines_case_insensitive()
3711 cx.set_state(indoc! {"
3712 «z
3713 y
3714 x
3715 Z
3716 Y
3717 Xˇ»
3718 "});
3719 cx.update_editor(|e, window, cx| {
3720 e.sort_lines_case_insensitive(&SortLinesCaseInsensitive, window, cx)
3721 });
3722 cx.assert_editor_state(indoc! {"
3723 «x
3724 X
3725 y
3726 Y
3727 z
3728 Zˇ»
3729 "});
3730
3731 // Test reverse_lines()
3732 cx.set_state(indoc! {"
3733 «5
3734 4
3735 3
3736 2
3737 1ˇ»
3738 "});
3739 cx.update_editor(|e, window, cx| e.reverse_lines(&ReverseLines, window, cx));
3740 cx.assert_editor_state(indoc! {"
3741 «1
3742 2
3743 3
3744 4
3745 5ˇ»
3746 "});
3747
3748 // Skip testing shuffle_line()
3749
3750 // From here on out, test more complex cases of manipulate_lines() with a single driver method: sort_lines_case_sensitive()
3751 // Since all methods calling manipulate_lines() are doing the exact same general thing (reordering lines)
3752
3753 // Don't manipulate when cursor is on single line, but expand the selection
3754 cx.set_state(indoc! {"
3755 ddˇdd
3756 ccc
3757 bb
3758 a
3759 "});
3760 cx.update_editor(|e, window, cx| {
3761 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3762 });
3763 cx.assert_editor_state(indoc! {"
3764 «ddddˇ»
3765 ccc
3766 bb
3767 a
3768 "});
3769
3770 // Basic manipulate case
3771 // Start selection moves to column 0
3772 // End of selection shrinks to fit shorter line
3773 cx.set_state(indoc! {"
3774 dd«d
3775 ccc
3776 bb
3777 aaaaaˇ»
3778 "});
3779 cx.update_editor(|e, window, cx| {
3780 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3781 });
3782 cx.assert_editor_state(indoc! {"
3783 «aaaaa
3784 bb
3785 ccc
3786 dddˇ»
3787 "});
3788
3789 // Manipulate case with newlines
3790 cx.set_state(indoc! {"
3791 dd«d
3792 ccc
3793
3794 bb
3795 aaaaa
3796
3797 ˇ»
3798 "});
3799 cx.update_editor(|e, window, cx| {
3800 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3801 });
3802 cx.assert_editor_state(indoc! {"
3803 «
3804
3805 aaaaa
3806 bb
3807 ccc
3808 dddˇ»
3809
3810 "});
3811
3812 // Adding new line
3813 cx.set_state(indoc! {"
3814 aa«a
3815 bbˇ»b
3816 "});
3817 cx.update_editor(|e, window, cx| {
3818 e.manipulate_lines(window, cx, |lines| lines.push("added_line"))
3819 });
3820 cx.assert_editor_state(indoc! {"
3821 «aaa
3822 bbb
3823 added_lineˇ»
3824 "});
3825
3826 // Removing line
3827 cx.set_state(indoc! {"
3828 aa«a
3829 bbbˇ»
3830 "});
3831 cx.update_editor(|e, window, cx| {
3832 e.manipulate_lines(window, cx, |lines| {
3833 lines.pop();
3834 })
3835 });
3836 cx.assert_editor_state(indoc! {"
3837 «aaaˇ»
3838 "});
3839
3840 // Removing all lines
3841 cx.set_state(indoc! {"
3842 aa«a
3843 bbbˇ»
3844 "});
3845 cx.update_editor(|e, window, cx| {
3846 e.manipulate_lines(window, cx, |lines| {
3847 lines.drain(..);
3848 })
3849 });
3850 cx.assert_editor_state(indoc! {"
3851 ˇ
3852 "});
3853}
3854
3855#[gpui::test]
3856async fn test_unique_lines_multi_selection(cx: &mut TestAppContext) {
3857 init_test(cx, |_| {});
3858
3859 let mut cx = EditorTestContext::new(cx).await;
3860
3861 // Consider continuous selection as single selection
3862 cx.set_state(indoc! {"
3863 Aaa«aa
3864 cˇ»c«c
3865 bb
3866 aaaˇ»aa
3867 "});
3868 cx.update_editor(|e, window, cx| {
3869 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3870 });
3871 cx.assert_editor_state(indoc! {"
3872 «Aaaaa
3873 ccc
3874 bb
3875 aaaaaˇ»
3876 "});
3877
3878 cx.set_state(indoc! {"
3879 Aaa«aa
3880 cˇ»c«c
3881 bb
3882 aaaˇ»aa
3883 "});
3884 cx.update_editor(|e, window, cx| {
3885 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3886 });
3887 cx.assert_editor_state(indoc! {"
3888 «Aaaaa
3889 ccc
3890 bbˇ»
3891 "});
3892
3893 // Consider non continuous selection as distinct dedup operations
3894 cx.set_state(indoc! {"
3895 «aaaaa
3896 bb
3897 aaaaa
3898 aaaaaˇ»
3899
3900 aaa«aaˇ»
3901 "});
3902 cx.update_editor(|e, window, cx| {
3903 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3904 });
3905 cx.assert_editor_state(indoc! {"
3906 «aaaaa
3907 bbˇ»
3908
3909 «aaaaaˇ»
3910 "});
3911}
3912
3913#[gpui::test]
3914async fn test_unique_lines_single_selection(cx: &mut TestAppContext) {
3915 init_test(cx, |_| {});
3916
3917 let mut cx = EditorTestContext::new(cx).await;
3918
3919 cx.set_state(indoc! {"
3920 «Aaa
3921 aAa
3922 Aaaˇ»
3923 "});
3924 cx.update_editor(|e, window, cx| {
3925 e.unique_lines_case_sensitive(&UniqueLinesCaseSensitive, window, cx)
3926 });
3927 cx.assert_editor_state(indoc! {"
3928 «Aaa
3929 aAaˇ»
3930 "});
3931
3932 cx.set_state(indoc! {"
3933 «Aaa
3934 aAa
3935 aaAˇ»
3936 "});
3937 cx.update_editor(|e, window, cx| {
3938 e.unique_lines_case_insensitive(&UniqueLinesCaseInsensitive, window, cx)
3939 });
3940 cx.assert_editor_state(indoc! {"
3941 «Aaaˇ»
3942 "});
3943}
3944
3945#[gpui::test]
3946async fn test_manipulate_lines_with_multi_selection(cx: &mut TestAppContext) {
3947 init_test(cx, |_| {});
3948
3949 let mut cx = EditorTestContext::new(cx).await;
3950
3951 // Manipulate with multiple selections on a single line
3952 cx.set_state(indoc! {"
3953 dd«dd
3954 cˇ»c«c
3955 bb
3956 aaaˇ»aa
3957 "});
3958 cx.update_editor(|e, window, cx| {
3959 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3960 });
3961 cx.assert_editor_state(indoc! {"
3962 «aaaaa
3963 bb
3964 ccc
3965 ddddˇ»
3966 "});
3967
3968 // Manipulate with multiple disjoin selections
3969 cx.set_state(indoc! {"
3970 5«
3971 4
3972 3
3973 2
3974 1ˇ»
3975
3976 dd«dd
3977 ccc
3978 bb
3979 aaaˇ»aa
3980 "});
3981 cx.update_editor(|e, window, cx| {
3982 e.sort_lines_case_sensitive(&SortLinesCaseSensitive, window, cx)
3983 });
3984 cx.assert_editor_state(indoc! {"
3985 «1
3986 2
3987 3
3988 4
3989 5ˇ»
3990
3991 «aaaaa
3992 bb
3993 ccc
3994 ddddˇ»
3995 "});
3996
3997 // Adding lines on each selection
3998 cx.set_state(indoc! {"
3999 2«
4000 1ˇ»
4001
4002 bb«bb
4003 aaaˇ»aa
4004 "});
4005 cx.update_editor(|e, window, cx| {
4006 e.manipulate_lines(window, cx, |lines| lines.push("added line"))
4007 });
4008 cx.assert_editor_state(indoc! {"
4009 «2
4010 1
4011 added lineˇ»
4012
4013 «bbbb
4014 aaaaa
4015 added lineˇ»
4016 "});
4017
4018 // Removing lines on each selection
4019 cx.set_state(indoc! {"
4020 2«
4021 1ˇ»
4022
4023 bb«bb
4024 aaaˇ»aa
4025 "});
4026 cx.update_editor(|e, window, cx| {
4027 e.manipulate_lines(window, cx, |lines| {
4028 lines.pop();
4029 })
4030 });
4031 cx.assert_editor_state(indoc! {"
4032 «2ˇ»
4033
4034 «bbbbˇ»
4035 "});
4036}
4037
4038#[gpui::test]
4039async fn test_toggle_case(cx: &mut TestAppContext) {
4040 init_test(cx, |_| {});
4041
4042 let mut cx = EditorTestContext::new(cx).await;
4043
4044 // If all lower case -> upper case
4045 cx.set_state(indoc! {"
4046 «hello worldˇ»
4047 "});
4048 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4049 cx.assert_editor_state(indoc! {"
4050 «HELLO WORLDˇ»
4051 "});
4052
4053 // If all upper case -> lower case
4054 cx.set_state(indoc! {"
4055 «HELLO WORLDˇ»
4056 "});
4057 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4058 cx.assert_editor_state(indoc! {"
4059 «hello worldˇ»
4060 "});
4061
4062 // If any upper case characters are identified -> lower case
4063 // This matches JetBrains IDEs
4064 cx.set_state(indoc! {"
4065 «hEllo worldˇ»
4066 "});
4067 cx.update_editor(|e, window, cx| e.toggle_case(&ToggleCase, window, cx));
4068 cx.assert_editor_state(indoc! {"
4069 «hello worldˇ»
4070 "});
4071}
4072
4073#[gpui::test]
4074async fn test_manipulate_text(cx: &mut TestAppContext) {
4075 init_test(cx, |_| {});
4076
4077 let mut cx = EditorTestContext::new(cx).await;
4078
4079 // Test convert_to_upper_case()
4080 cx.set_state(indoc! {"
4081 «hello worldˇ»
4082 "});
4083 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4084 cx.assert_editor_state(indoc! {"
4085 «HELLO WORLDˇ»
4086 "});
4087
4088 // Test convert_to_lower_case()
4089 cx.set_state(indoc! {"
4090 «HELLO WORLDˇ»
4091 "});
4092 cx.update_editor(|e, window, cx| e.convert_to_lower_case(&ConvertToLowerCase, window, cx));
4093 cx.assert_editor_state(indoc! {"
4094 «hello worldˇ»
4095 "});
4096
4097 // Test multiple line, single selection case
4098 cx.set_state(indoc! {"
4099 «The quick brown
4100 fox jumps over
4101 the lazy dogˇ»
4102 "});
4103 cx.update_editor(|e, window, cx| e.convert_to_title_case(&ConvertToTitleCase, window, cx));
4104 cx.assert_editor_state(indoc! {"
4105 «The Quick Brown
4106 Fox Jumps Over
4107 The Lazy Dogˇ»
4108 "});
4109
4110 // Test multiple line, single selection case
4111 cx.set_state(indoc! {"
4112 «The quick brown
4113 fox jumps over
4114 the lazy dogˇ»
4115 "});
4116 cx.update_editor(|e, window, cx| {
4117 e.convert_to_upper_camel_case(&ConvertToUpperCamelCase, window, cx)
4118 });
4119 cx.assert_editor_state(indoc! {"
4120 «TheQuickBrown
4121 FoxJumpsOver
4122 TheLazyDogˇ»
4123 "});
4124
4125 // From here on out, test more complex cases of manipulate_text()
4126
4127 // Test no selection case - should affect words cursors are in
4128 // Cursor at beginning, middle, and end of word
4129 cx.set_state(indoc! {"
4130 ˇhello big beauˇtiful worldˇ
4131 "});
4132 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4133 cx.assert_editor_state(indoc! {"
4134 «HELLOˇ» big «BEAUTIFULˇ» «WORLDˇ»
4135 "});
4136
4137 // Test multiple selections on a single line and across multiple lines
4138 cx.set_state(indoc! {"
4139 «Theˇ» quick «brown
4140 foxˇ» jumps «overˇ»
4141 the «lazyˇ» dog
4142 "});
4143 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4144 cx.assert_editor_state(indoc! {"
4145 «THEˇ» quick «BROWN
4146 FOXˇ» jumps «OVERˇ»
4147 the «LAZYˇ» dog
4148 "});
4149
4150 // Test case where text length grows
4151 cx.set_state(indoc! {"
4152 «tschüߡ»
4153 "});
4154 cx.update_editor(|e, window, cx| e.convert_to_upper_case(&ConvertToUpperCase, window, cx));
4155 cx.assert_editor_state(indoc! {"
4156 «TSCHÜSSˇ»
4157 "});
4158
4159 // Test to make sure we don't crash when text shrinks
4160 cx.set_state(indoc! {"
4161 aaa_bbbˇ
4162 "});
4163 cx.update_editor(|e, window, cx| {
4164 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4165 });
4166 cx.assert_editor_state(indoc! {"
4167 «aaaBbbˇ»
4168 "});
4169
4170 // Test to make sure we all aware of the fact that each word can grow and shrink
4171 // Final selections should be aware of this fact
4172 cx.set_state(indoc! {"
4173 aaa_bˇbb bbˇb_ccc ˇccc_ddd
4174 "});
4175 cx.update_editor(|e, window, cx| {
4176 e.convert_to_lower_camel_case(&ConvertToLowerCamelCase, window, cx)
4177 });
4178 cx.assert_editor_state(indoc! {"
4179 «aaaBbbˇ» «bbbCccˇ» «cccDddˇ»
4180 "});
4181
4182 cx.set_state(indoc! {"
4183 «hElLo, WoRld!ˇ»
4184 "});
4185 cx.update_editor(|e, window, cx| {
4186 e.convert_to_opposite_case(&ConvertToOppositeCase, window, cx)
4187 });
4188 cx.assert_editor_state(indoc! {"
4189 «HeLlO, wOrLD!ˇ»
4190 "});
4191}
4192
4193#[gpui::test]
4194fn test_duplicate_line(cx: &mut TestAppContext) {
4195 init_test(cx, |_| {});
4196
4197 let editor = cx.add_window(|window, cx| {
4198 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4199 build_editor(buffer, window, cx)
4200 });
4201 _ = editor.update(cx, |editor, window, cx| {
4202 editor.change_selections(None, window, cx, |s| {
4203 s.select_display_ranges([
4204 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4205 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4206 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4207 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4208 ])
4209 });
4210 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4211 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4212 assert_eq!(
4213 editor.selections.display_ranges(cx),
4214 vec![
4215 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
4216 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(1), 2),
4217 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4218 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4219 ]
4220 );
4221 });
4222
4223 let editor = cx.add_window(|window, cx| {
4224 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4225 build_editor(buffer, window, cx)
4226 });
4227 _ = editor.update(cx, |editor, window, cx| {
4228 editor.change_selections(None, window, cx, |s| {
4229 s.select_display_ranges([
4230 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4231 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4232 ])
4233 });
4234 editor.duplicate_line_down(&DuplicateLineDown, window, cx);
4235 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4236 assert_eq!(
4237 editor.selections.display_ranges(cx),
4238 vec![
4239 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(4), 1),
4240 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(5), 1),
4241 ]
4242 );
4243 });
4244
4245 // With `move_upwards` the selections stay in place, except for
4246 // the lines inserted above them
4247 let editor = cx.add_window(|window, cx| {
4248 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4249 build_editor(buffer, window, cx)
4250 });
4251 _ = editor.update(cx, |editor, window, cx| {
4252 editor.change_selections(None, window, cx, |s| {
4253 s.select_display_ranges([
4254 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4255 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4256 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
4257 DisplayPoint::new(DisplayRow(3), 0)..DisplayPoint::new(DisplayRow(3), 0),
4258 ])
4259 });
4260 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4261 assert_eq!(editor.display_text(cx), "abc\nabc\ndef\ndef\nghi\n\n");
4262 assert_eq!(
4263 editor.selections.display_ranges(cx),
4264 vec![
4265 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
4266 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
4267 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 0),
4268 DisplayPoint::new(DisplayRow(6), 0)..DisplayPoint::new(DisplayRow(6), 0),
4269 ]
4270 );
4271 });
4272
4273 let editor = cx.add_window(|window, cx| {
4274 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4275 build_editor(buffer, window, cx)
4276 });
4277 _ = editor.update(cx, |editor, window, cx| {
4278 editor.change_selections(None, window, cx, |s| {
4279 s.select_display_ranges([
4280 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4281 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4282 ])
4283 });
4284 editor.duplicate_line_up(&DuplicateLineUp, window, cx);
4285 assert_eq!(editor.display_text(cx), "abc\ndef\nghi\nabc\ndef\nghi\n");
4286 assert_eq!(
4287 editor.selections.display_ranges(cx),
4288 vec![
4289 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4290 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4291 ]
4292 );
4293 });
4294
4295 let editor = cx.add_window(|window, cx| {
4296 let buffer = MultiBuffer::build_simple("abc\ndef\nghi\n", cx);
4297 build_editor(buffer, window, cx)
4298 });
4299 _ = editor.update(cx, |editor, window, cx| {
4300 editor.change_selections(None, window, cx, |s| {
4301 s.select_display_ranges([
4302 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4303 DisplayPoint::new(DisplayRow(1), 2)..DisplayPoint::new(DisplayRow(2), 1),
4304 ])
4305 });
4306 editor.duplicate_selection(&DuplicateSelection, window, cx);
4307 assert_eq!(editor.display_text(cx), "abc\ndbc\ndef\ngf\nghi\n");
4308 assert_eq!(
4309 editor.selections.display_ranges(cx),
4310 vec![
4311 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(1), 1),
4312 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 1),
4313 ]
4314 );
4315 });
4316}
4317
4318#[gpui::test]
4319fn test_move_line_up_down(cx: &mut TestAppContext) {
4320 init_test(cx, |_| {});
4321
4322 let editor = cx.add_window(|window, cx| {
4323 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4324 build_editor(buffer, window, cx)
4325 });
4326 _ = editor.update(cx, |editor, window, cx| {
4327 editor.fold_creases(
4328 vec![
4329 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
4330 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
4331 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
4332 ],
4333 true,
4334 window,
4335 cx,
4336 );
4337 editor.change_selections(None, window, cx, |s| {
4338 s.select_display_ranges([
4339 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4340 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4341 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4342 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2),
4343 ])
4344 });
4345 assert_eq!(
4346 editor.display_text(cx),
4347 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i\njjjjj"
4348 );
4349
4350 editor.move_line_up(&MoveLineUp, window, cx);
4351 assert_eq!(
4352 editor.display_text(cx),
4353 "aa⋯bbb\nccc⋯eeee\nggggg\n⋯i\njjjjj\nfffff"
4354 );
4355 assert_eq!(
4356 editor.selections.display_ranges(cx),
4357 vec![
4358 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
4359 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4360 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4361 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4362 ]
4363 );
4364 });
4365
4366 _ = editor.update(cx, |editor, window, cx| {
4367 editor.move_line_down(&MoveLineDown, window, cx);
4368 assert_eq!(
4369 editor.display_text(cx),
4370 "ccc⋯eeee\naa⋯bbb\nfffff\nggggg\n⋯i\njjjjj"
4371 );
4372 assert_eq!(
4373 editor.selections.display_ranges(cx),
4374 vec![
4375 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4376 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4377 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4378 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4379 ]
4380 );
4381 });
4382
4383 _ = editor.update(cx, |editor, window, cx| {
4384 editor.move_line_down(&MoveLineDown, window, cx);
4385 assert_eq!(
4386 editor.display_text(cx),
4387 "ccc⋯eeee\nfffff\naa⋯bbb\nggggg\n⋯i\njjjjj"
4388 );
4389 assert_eq!(
4390 editor.selections.display_ranges(cx),
4391 vec![
4392 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4393 DisplayPoint::new(DisplayRow(3), 1)..DisplayPoint::new(DisplayRow(3), 1),
4394 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(4), 3),
4395 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(5), 2)
4396 ]
4397 );
4398 });
4399
4400 _ = editor.update(cx, |editor, window, cx| {
4401 editor.move_line_up(&MoveLineUp, window, cx);
4402 assert_eq!(
4403 editor.display_text(cx),
4404 "ccc⋯eeee\naa⋯bbb\nggggg\n⋯i\njjjjj\nfffff"
4405 );
4406 assert_eq!(
4407 editor.selections.display_ranges(cx),
4408 vec![
4409 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
4410 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1),
4411 DisplayPoint::new(DisplayRow(2), 2)..DisplayPoint::new(DisplayRow(3), 3),
4412 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(4), 2)
4413 ]
4414 );
4415 });
4416}
4417
4418#[gpui::test]
4419fn test_move_line_up_down_with_blocks(cx: &mut TestAppContext) {
4420 init_test(cx, |_| {});
4421
4422 let editor = cx.add_window(|window, cx| {
4423 let buffer = MultiBuffer::build_simple(&sample_text(10, 5, 'a'), cx);
4424 build_editor(buffer, window, cx)
4425 });
4426 _ = editor.update(cx, |editor, window, cx| {
4427 let snapshot = editor.buffer.read(cx).snapshot(cx);
4428 editor.insert_blocks(
4429 [BlockProperties {
4430 style: BlockStyle::Fixed,
4431 placement: BlockPlacement::Below(snapshot.anchor_after(Point::new(2, 0))),
4432 height: Some(1),
4433 render: Arc::new(|_| div().into_any()),
4434 priority: 0,
4435 render_in_minimap: true,
4436 }],
4437 Some(Autoscroll::fit()),
4438 cx,
4439 );
4440 editor.change_selections(None, window, cx, |s| {
4441 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
4442 });
4443 editor.move_line_down(&MoveLineDown, window, cx);
4444 });
4445}
4446
4447#[gpui::test]
4448async fn test_selections_and_replace_blocks(cx: &mut TestAppContext) {
4449 init_test(cx, |_| {});
4450
4451 let mut cx = EditorTestContext::new(cx).await;
4452 cx.set_state(
4453 &"
4454 ˇzero
4455 one
4456 two
4457 three
4458 four
4459 five
4460 "
4461 .unindent(),
4462 );
4463
4464 // Create a four-line block that replaces three lines of text.
4465 cx.update_editor(|editor, window, cx| {
4466 let snapshot = editor.snapshot(window, cx);
4467 let snapshot = &snapshot.buffer_snapshot;
4468 let placement = BlockPlacement::Replace(
4469 snapshot.anchor_after(Point::new(1, 0))..=snapshot.anchor_after(Point::new(3, 0)),
4470 );
4471 editor.insert_blocks(
4472 [BlockProperties {
4473 placement,
4474 height: Some(4),
4475 style: BlockStyle::Sticky,
4476 render: Arc::new(|_| gpui::div().into_any_element()),
4477 priority: 0,
4478 render_in_minimap: true,
4479 }],
4480 None,
4481 cx,
4482 );
4483 });
4484
4485 // Move down so that the cursor touches the block.
4486 cx.update_editor(|editor, window, cx| {
4487 editor.move_down(&Default::default(), window, cx);
4488 });
4489 cx.assert_editor_state(
4490 &"
4491 zero
4492 «one
4493 two
4494 threeˇ»
4495 four
4496 five
4497 "
4498 .unindent(),
4499 );
4500
4501 // Move down past the block.
4502 cx.update_editor(|editor, window, cx| {
4503 editor.move_down(&Default::default(), window, cx);
4504 });
4505 cx.assert_editor_state(
4506 &"
4507 zero
4508 one
4509 two
4510 three
4511 ˇfour
4512 five
4513 "
4514 .unindent(),
4515 );
4516}
4517
4518#[gpui::test]
4519fn test_transpose(cx: &mut TestAppContext) {
4520 init_test(cx, |_| {});
4521
4522 _ = cx.add_window(|window, cx| {
4523 let mut editor = build_editor(MultiBuffer::build_simple("abc", cx), window, cx);
4524 editor.set_style(EditorStyle::default(), window, cx);
4525 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
4526 editor.transpose(&Default::default(), window, cx);
4527 assert_eq!(editor.text(cx), "bac");
4528 assert_eq!(editor.selections.ranges(cx), [2..2]);
4529
4530 editor.transpose(&Default::default(), window, cx);
4531 assert_eq!(editor.text(cx), "bca");
4532 assert_eq!(editor.selections.ranges(cx), [3..3]);
4533
4534 editor.transpose(&Default::default(), window, cx);
4535 assert_eq!(editor.text(cx), "bac");
4536 assert_eq!(editor.selections.ranges(cx), [3..3]);
4537
4538 editor
4539 });
4540
4541 _ = cx.add_window(|window, cx| {
4542 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4543 editor.set_style(EditorStyle::default(), window, cx);
4544 editor.change_selections(None, window, cx, |s| s.select_ranges([3..3]));
4545 editor.transpose(&Default::default(), window, cx);
4546 assert_eq!(editor.text(cx), "acb\nde");
4547 assert_eq!(editor.selections.ranges(cx), [3..3]);
4548
4549 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4550 editor.transpose(&Default::default(), window, cx);
4551 assert_eq!(editor.text(cx), "acbd\ne");
4552 assert_eq!(editor.selections.ranges(cx), [5..5]);
4553
4554 editor.transpose(&Default::default(), window, cx);
4555 assert_eq!(editor.text(cx), "acbde\n");
4556 assert_eq!(editor.selections.ranges(cx), [6..6]);
4557
4558 editor.transpose(&Default::default(), window, cx);
4559 assert_eq!(editor.text(cx), "acbd\ne");
4560 assert_eq!(editor.selections.ranges(cx), [6..6]);
4561
4562 editor
4563 });
4564
4565 _ = cx.add_window(|window, cx| {
4566 let mut editor = build_editor(MultiBuffer::build_simple("abc\nde", cx), window, cx);
4567 editor.set_style(EditorStyle::default(), window, cx);
4568 editor.change_selections(None, window, cx, |s| s.select_ranges([1..1, 2..2, 4..4]));
4569 editor.transpose(&Default::default(), window, cx);
4570 assert_eq!(editor.text(cx), "bacd\ne");
4571 assert_eq!(editor.selections.ranges(cx), [2..2, 3..3, 5..5]);
4572
4573 editor.transpose(&Default::default(), window, cx);
4574 assert_eq!(editor.text(cx), "bcade\n");
4575 assert_eq!(editor.selections.ranges(cx), [3..3, 4..4, 6..6]);
4576
4577 editor.transpose(&Default::default(), window, cx);
4578 assert_eq!(editor.text(cx), "bcda\ne");
4579 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4580
4581 editor.transpose(&Default::default(), window, cx);
4582 assert_eq!(editor.text(cx), "bcade\n");
4583 assert_eq!(editor.selections.ranges(cx), [4..4, 6..6]);
4584
4585 editor.transpose(&Default::default(), window, cx);
4586 assert_eq!(editor.text(cx), "bcaed\n");
4587 assert_eq!(editor.selections.ranges(cx), [5..5, 6..6]);
4588
4589 editor
4590 });
4591
4592 _ = cx.add_window(|window, cx| {
4593 let mut editor = build_editor(MultiBuffer::build_simple("🍐🏀✋", cx), window, cx);
4594 editor.set_style(EditorStyle::default(), window, cx);
4595 editor.change_selections(None, window, cx, |s| s.select_ranges([4..4]));
4596 editor.transpose(&Default::default(), window, cx);
4597 assert_eq!(editor.text(cx), "🏀🍐✋");
4598 assert_eq!(editor.selections.ranges(cx), [8..8]);
4599
4600 editor.transpose(&Default::default(), window, cx);
4601 assert_eq!(editor.text(cx), "🏀✋🍐");
4602 assert_eq!(editor.selections.ranges(cx), [11..11]);
4603
4604 editor.transpose(&Default::default(), window, cx);
4605 assert_eq!(editor.text(cx), "🏀🍐✋");
4606 assert_eq!(editor.selections.ranges(cx), [11..11]);
4607
4608 editor
4609 });
4610}
4611
4612#[gpui::test]
4613async fn test_rewrap(cx: &mut TestAppContext) {
4614 init_test(cx, |settings| {
4615 settings.languages.extend([
4616 (
4617 "Markdown".into(),
4618 LanguageSettingsContent {
4619 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4620 ..Default::default()
4621 },
4622 ),
4623 (
4624 "Plain Text".into(),
4625 LanguageSettingsContent {
4626 allow_rewrap: Some(language_settings::RewrapBehavior::Anywhere),
4627 ..Default::default()
4628 },
4629 ),
4630 ])
4631 });
4632
4633 let mut cx = EditorTestContext::new(cx).await;
4634
4635 let language_with_c_comments = Arc::new(Language::new(
4636 LanguageConfig {
4637 line_comments: vec!["// ".into()],
4638 ..LanguageConfig::default()
4639 },
4640 None,
4641 ));
4642 let language_with_pound_comments = Arc::new(Language::new(
4643 LanguageConfig {
4644 line_comments: vec!["# ".into()],
4645 ..LanguageConfig::default()
4646 },
4647 None,
4648 ));
4649 let markdown_language = Arc::new(Language::new(
4650 LanguageConfig {
4651 name: "Markdown".into(),
4652 ..LanguageConfig::default()
4653 },
4654 None,
4655 ));
4656 let language_with_doc_comments = Arc::new(Language::new(
4657 LanguageConfig {
4658 line_comments: vec!["// ".into(), "/// ".into()],
4659 ..LanguageConfig::default()
4660 },
4661 Some(tree_sitter_rust::LANGUAGE.into()),
4662 ));
4663
4664 let plaintext_language = Arc::new(Language::new(
4665 LanguageConfig {
4666 name: "Plain Text".into(),
4667 ..LanguageConfig::default()
4668 },
4669 None,
4670 ));
4671
4672 assert_rewrap(
4673 indoc! {"
4674 // ˇ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.
4675 "},
4676 indoc! {"
4677 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4678 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4679 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4680 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4681 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4682 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4683 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4684 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4685 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4686 // porttitor id. Aliquam id accumsan eros.
4687 "},
4688 language_with_c_comments.clone(),
4689 &mut cx,
4690 );
4691
4692 // Test that rewrapping works inside of a selection
4693 assert_rewrap(
4694 indoc! {"
4695 «// 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.ˇ»
4696 "},
4697 indoc! {"
4698 «// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4699 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4700 // auctor, eu lacinia sapien scelerisque. Vivamus sit amet neque et quam
4701 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4702 // Pellentesque odio lectus, iaculis ac volutpat et, blandit quis urna. Sed
4703 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4704 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4705 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4706 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4707 // porttitor id. Aliquam id accumsan eros.ˇ»
4708 "},
4709 language_with_c_comments.clone(),
4710 &mut cx,
4711 );
4712
4713 // Test that cursors that expand to the same region are collapsed.
4714 assert_rewrap(
4715 indoc! {"
4716 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4717 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4718 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4719 // ˇ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.
4720 "},
4721 indoc! {"
4722 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4723 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4724 // auctor, eu lacinia sapien scelerisque. ˇVivamus sit amet neque et quam
4725 // tincidunt hendrerit. Praesent semper egestas tellus id dignissim.
4726 // Pellentesque odio lectus, iaculis ac volutpat et, ˇblandit quis urna. Sed
4727 // vestibulum nisi sit amet nisl venenatis tempus. Donec molestie blandit quam,
4728 // et porta nunc laoreet in. Integer sit amet scelerisque nisi. Lorem ipsum
4729 // dolor sit amet, consectetur adipiscing elit. Cras egestas porta metus, eu
4730 // viverra ipsum efficitur quis. Donec luctus eros turpis, id vulputate turpis
4731 // porttitor id. Aliquam id accumsan eros.
4732 "},
4733 language_with_c_comments.clone(),
4734 &mut cx,
4735 );
4736
4737 // Test that non-contiguous selections are treated separately.
4738 assert_rewrap(
4739 indoc! {"
4740 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit.
4741 // ˇVivamus mollis elit purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor, eu lacinia sapien scelerisque.
4742 //
4743 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4744 // ˇ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.
4745 "},
4746 indoc! {"
4747 // ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. ˇVivamus mollis elit
4748 // purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus
4749 // auctor, eu lacinia sapien scelerisque.
4750 //
4751 // ˇVivamus sit amet neque et quam tincidunt hendrerit. Praesent semper egestas
4752 // tellus id dignissim. Pellentesque odio lectus, iaculis ac volutpat et,
4753 // ˇblandit quis urna. Sed vestibulum nisi sit amet nisl venenatis tempus. Donec
4754 // molestie blandit quam, et porta nunc laoreet in. Integer sit amet scelerisque
4755 // nisi. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras egestas
4756 // porta metus, eu viverra ipsum efficitur quis. Donec luctus eros turpis, id
4757 // vulputate turpis porttitor id. Aliquam id accumsan eros.
4758 "},
4759 language_with_c_comments.clone(),
4760 &mut cx,
4761 );
4762
4763 // Test that different comment prefixes are supported.
4764 assert_rewrap(
4765 indoc! {"
4766 # ˇ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.
4767 "},
4768 indoc! {"
4769 # ˇLorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit
4770 # purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4771 # eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4772 # hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4773 # lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit
4774 # amet nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet
4775 # in. Integer sit amet scelerisque nisi. Lorem ipsum dolor sit amet, consectetur
4776 # adipiscing elit. Cras egestas porta metus, eu viverra ipsum efficitur quis.
4777 # Donec luctus eros turpis, id vulputate turpis porttitor id. Aliquam id
4778 # accumsan eros.
4779 "},
4780 language_with_pound_comments.clone(),
4781 &mut cx,
4782 );
4783
4784 // Test that rewrapping is ignored outside of comments in most languages.
4785 assert_rewrap(
4786 indoc! {"
4787 /// Adds two numbers.
4788 /// Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4789 fn add(a: u32, b: u32) -> u32 {
4790 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ˇ
4791 }
4792 "},
4793 indoc! {"
4794 /// Adds two numbers. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
4795 /// Vivamus mollis elit purus, a ornare lacus gravida vitae.ˇ
4796 fn add(a: u32, b: u32) -> u32 {
4797 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ˇ
4798 }
4799 "},
4800 language_with_doc_comments.clone(),
4801 &mut cx,
4802 );
4803
4804 // Test that rewrapping works in Markdown and Plain Text languages.
4805 assert_rewrap(
4806 indoc! {"
4807 # Hello
4808
4809 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.
4810 "},
4811 indoc! {"
4812 # Hello
4813
4814 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4815 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4816 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4817 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4818 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4819 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4820 Integer sit amet scelerisque nisi.
4821 "},
4822 markdown_language,
4823 &mut cx,
4824 );
4825
4826 assert_rewrap(
4827 indoc! {"
4828 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.
4829 "},
4830 indoc! {"
4831 Lorem ipsum dolor sit amet, ˇconsectetur adipiscing elit. Vivamus mollis elit
4832 purus, a ornare lacus gravida vitae. Proin consectetur felis vel purus auctor,
4833 eu lacinia sapien scelerisque. Vivamus sit amet neque et quam tincidunt
4834 hendrerit. Praesent semper egestas tellus id dignissim. Pellentesque odio
4835 lectus, iaculis ac volutpat et, blandit quis urna. Sed vestibulum nisi sit amet
4836 nisl venenatis tempus. Donec molestie blandit quam, et porta nunc laoreet in.
4837 Integer sit amet scelerisque nisi.
4838 "},
4839 plaintext_language,
4840 &mut cx,
4841 );
4842
4843 // Test rewrapping unaligned comments in a selection.
4844 assert_rewrap(
4845 indoc! {"
4846 fn foo() {
4847 if true {
4848 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4849 // Praesent semper egestas tellus id dignissim.ˇ»
4850 do_something();
4851 } else {
4852 //
4853 }
4854 }
4855 "},
4856 indoc! {"
4857 fn foo() {
4858 if true {
4859 « // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4860 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4861 // egestas tellus id dignissim.ˇ»
4862 do_something();
4863 } else {
4864 //
4865 }
4866 }
4867 "},
4868 language_with_doc_comments.clone(),
4869 &mut cx,
4870 );
4871
4872 assert_rewrap(
4873 indoc! {"
4874 fn foo() {
4875 if true {
4876 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus mollis elit purus, a ornare lacus gravida vitae.
4877 // Praesent semper egestas tellus id dignissim.»
4878 do_something();
4879 } else {
4880 //
4881 }
4882
4883 }
4884 "},
4885 indoc! {"
4886 fn foo() {
4887 if true {
4888 «ˇ // Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus
4889 // mollis elit purus, a ornare lacus gravida vitae. Praesent semper
4890 // egestas tellus id dignissim.»
4891 do_something();
4892 } else {
4893 //
4894 }
4895
4896 }
4897 "},
4898 language_with_doc_comments.clone(),
4899 &mut cx,
4900 );
4901
4902 #[track_caller]
4903 fn assert_rewrap(
4904 unwrapped_text: &str,
4905 wrapped_text: &str,
4906 language: Arc<Language>,
4907 cx: &mut EditorTestContext,
4908 ) {
4909 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
4910 cx.set_state(unwrapped_text);
4911 cx.update_editor(|e, window, cx| e.rewrap(&Rewrap, window, cx));
4912 cx.assert_editor_state(wrapped_text);
4913 }
4914}
4915
4916#[gpui::test]
4917async fn test_hard_wrap(cx: &mut TestAppContext) {
4918 init_test(cx, |_| {});
4919 let mut cx = EditorTestContext::new(cx).await;
4920
4921 cx.update_buffer(|buffer, cx| buffer.set_language(Some(git_commit_lang()), cx));
4922 cx.update_editor(|editor, _, cx| {
4923 editor.set_hard_wrap(Some(14), cx);
4924 });
4925
4926 cx.set_state(indoc!(
4927 "
4928 one two three ˇ
4929 "
4930 ));
4931 cx.simulate_input("four");
4932 cx.run_until_parked();
4933
4934 cx.assert_editor_state(indoc!(
4935 "
4936 one two three
4937 fourˇ
4938 "
4939 ));
4940
4941 cx.update_editor(|editor, window, cx| {
4942 editor.newline(&Default::default(), window, cx);
4943 });
4944 cx.run_until_parked();
4945 cx.assert_editor_state(indoc!(
4946 "
4947 one two three
4948 four
4949 ˇ
4950 "
4951 ));
4952
4953 cx.simulate_input("five");
4954 cx.run_until_parked();
4955 cx.assert_editor_state(indoc!(
4956 "
4957 one two three
4958 four
4959 fiveˇ
4960 "
4961 ));
4962
4963 cx.update_editor(|editor, window, cx| {
4964 editor.newline(&Default::default(), window, cx);
4965 });
4966 cx.run_until_parked();
4967 cx.simulate_input("# ");
4968 cx.run_until_parked();
4969 cx.assert_editor_state(indoc!(
4970 "
4971 one two three
4972 four
4973 five
4974 # ˇ
4975 "
4976 ));
4977
4978 cx.update_editor(|editor, window, cx| {
4979 editor.newline(&Default::default(), window, cx);
4980 });
4981 cx.run_until_parked();
4982 cx.assert_editor_state(indoc!(
4983 "
4984 one two three
4985 four
4986 five
4987 #\x20
4988 #ˇ
4989 "
4990 ));
4991
4992 cx.simulate_input(" 6");
4993 cx.run_until_parked();
4994 cx.assert_editor_state(indoc!(
4995 "
4996 one two three
4997 four
4998 five
4999 #
5000 # 6ˇ
5001 "
5002 ));
5003}
5004
5005#[gpui::test]
5006async fn test_clipboard(cx: &mut TestAppContext) {
5007 init_test(cx, |_| {});
5008
5009 let mut cx = EditorTestContext::new(cx).await;
5010
5011 cx.set_state("«one✅ ˇ»two «three ˇ»four «five ˇ»six ");
5012 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5013 cx.assert_editor_state("ˇtwo ˇfour ˇsix ");
5014
5015 // Paste with three cursors. Each cursor pastes one slice of the clipboard text.
5016 cx.set_state("two ˇfour ˇsix ˇ");
5017 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5018 cx.assert_editor_state("two one✅ ˇfour three ˇsix five ˇ");
5019
5020 // Paste again but with only two cursors. Since the number of cursors doesn't
5021 // match the number of slices in the clipboard, the entire clipboard text
5022 // is pasted at each cursor.
5023 cx.set_state("ˇtwo one✅ four three six five ˇ");
5024 cx.update_editor(|e, window, cx| {
5025 e.handle_input("( ", window, cx);
5026 e.paste(&Paste, window, cx);
5027 e.handle_input(") ", window, cx);
5028 });
5029 cx.assert_editor_state(
5030 &([
5031 "( one✅ ",
5032 "three ",
5033 "five ) ˇtwo one✅ four three six five ( one✅ ",
5034 "three ",
5035 "five ) ˇ",
5036 ]
5037 .join("\n")),
5038 );
5039
5040 // Cut with three selections, one of which is full-line.
5041 cx.set_state(indoc! {"
5042 1«2ˇ»3
5043 4ˇ567
5044 «8ˇ»9"});
5045 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5046 cx.assert_editor_state(indoc! {"
5047 1ˇ3
5048 ˇ9"});
5049
5050 // Paste with three selections, noticing how the copied selection that was full-line
5051 // gets inserted before the second cursor.
5052 cx.set_state(indoc! {"
5053 1ˇ3
5054 9ˇ
5055 «oˇ»ne"});
5056 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5057 cx.assert_editor_state(indoc! {"
5058 12ˇ3
5059 4567
5060 9ˇ
5061 8ˇne"});
5062
5063 // Copy with a single cursor only, which writes the whole line into the clipboard.
5064 cx.set_state(indoc! {"
5065 The quick brown
5066 fox juˇmps over
5067 the lazy dog"});
5068 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5069 assert_eq!(
5070 cx.read_from_clipboard()
5071 .and_then(|item| item.text().as_deref().map(str::to_string)),
5072 Some("fox jumps over\n".to_string())
5073 );
5074
5075 // Paste with three selections, noticing how the copied full-line selection is inserted
5076 // before the empty selections but replaces the selection that is non-empty.
5077 cx.set_state(indoc! {"
5078 Tˇhe quick brown
5079 «foˇ»x jumps over
5080 tˇhe lazy dog"});
5081 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5082 cx.assert_editor_state(indoc! {"
5083 fox jumps over
5084 Tˇhe quick brown
5085 fox jumps over
5086 ˇx jumps over
5087 fox jumps over
5088 tˇhe lazy dog"});
5089}
5090
5091#[gpui::test]
5092async fn test_copy_trim(cx: &mut TestAppContext) {
5093 init_test(cx, |_| {});
5094
5095 let mut cx = EditorTestContext::new(cx).await;
5096 cx.set_state(
5097 r#" «for selection in selections.iter() {
5098 let mut start = selection.start;
5099 let mut end = selection.end;
5100 let is_entire_line = selection.is_empty();
5101 if is_entire_line {
5102 start = Point::new(start.row, 0);ˇ»
5103 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5104 }
5105 "#,
5106 );
5107 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5108 assert_eq!(
5109 cx.read_from_clipboard()
5110 .and_then(|item| item.text().as_deref().map(str::to_string)),
5111 Some(
5112 "for selection in selections.iter() {
5113 let mut start = selection.start;
5114 let mut end = selection.end;
5115 let is_entire_line = selection.is_empty();
5116 if is_entire_line {
5117 start = Point::new(start.row, 0);"
5118 .to_string()
5119 ),
5120 "Regular copying preserves all indentation selected",
5121 );
5122 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5123 assert_eq!(
5124 cx.read_from_clipboard()
5125 .and_then(|item| item.text().as_deref().map(str::to_string)),
5126 Some(
5127 "for selection in selections.iter() {
5128let mut start = selection.start;
5129let mut end = selection.end;
5130let is_entire_line = selection.is_empty();
5131if is_entire_line {
5132 start = Point::new(start.row, 0);"
5133 .to_string()
5134 ),
5135 "Copying with stripping should strip all leading whitespaces"
5136 );
5137
5138 cx.set_state(
5139 r#" « for selection in selections.iter() {
5140 let mut start = selection.start;
5141 let mut end = selection.end;
5142 let is_entire_line = selection.is_empty();
5143 if is_entire_line {
5144 start = Point::new(start.row, 0);ˇ»
5145 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5146 }
5147 "#,
5148 );
5149 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5150 assert_eq!(
5151 cx.read_from_clipboard()
5152 .and_then(|item| item.text().as_deref().map(str::to_string)),
5153 Some(
5154 " for selection in selections.iter() {
5155 let mut start = selection.start;
5156 let mut end = selection.end;
5157 let is_entire_line = selection.is_empty();
5158 if is_entire_line {
5159 start = Point::new(start.row, 0);"
5160 .to_string()
5161 ),
5162 "Regular copying preserves all indentation selected",
5163 );
5164 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5165 assert_eq!(
5166 cx.read_from_clipboard()
5167 .and_then(|item| item.text().as_deref().map(str::to_string)),
5168 Some(
5169 "for selection in selections.iter() {
5170let mut start = selection.start;
5171let mut end = selection.end;
5172let is_entire_line = selection.is_empty();
5173if is_entire_line {
5174 start = Point::new(start.row, 0);"
5175 .to_string()
5176 ),
5177 "Copying with stripping should strip all leading whitespaces, even if some of it was selected"
5178 );
5179
5180 cx.set_state(
5181 r#" «ˇ for selection in selections.iter() {
5182 let mut start = selection.start;
5183 let mut end = selection.end;
5184 let is_entire_line = selection.is_empty();
5185 if is_entire_line {
5186 start = Point::new(start.row, 0);»
5187 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5188 }
5189 "#,
5190 );
5191 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5192 assert_eq!(
5193 cx.read_from_clipboard()
5194 .and_then(|item| item.text().as_deref().map(str::to_string)),
5195 Some(
5196 " for selection in selections.iter() {
5197 let mut start = selection.start;
5198 let mut end = selection.end;
5199 let is_entire_line = selection.is_empty();
5200 if is_entire_line {
5201 start = Point::new(start.row, 0);"
5202 .to_string()
5203 ),
5204 "Regular copying for reverse selection works the same",
5205 );
5206 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5207 assert_eq!(
5208 cx.read_from_clipboard()
5209 .and_then(|item| item.text().as_deref().map(str::to_string)),
5210 Some(
5211 "for selection in selections.iter() {
5212let mut start = selection.start;
5213let mut end = selection.end;
5214let is_entire_line = selection.is_empty();
5215if is_entire_line {
5216 start = Point::new(start.row, 0);"
5217 .to_string()
5218 ),
5219 "Copying with stripping for reverse selection works the same"
5220 );
5221
5222 cx.set_state(
5223 r#" for selection «in selections.iter() {
5224 let mut start = selection.start;
5225 let mut end = selection.end;
5226 let is_entire_line = selection.is_empty();
5227 if is_entire_line {
5228 start = Point::new(start.row, 0);ˇ»
5229 end = cmp::min(max_point, Point::new(end.row + 1, 0));
5230 }
5231 "#,
5232 );
5233 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5234 assert_eq!(
5235 cx.read_from_clipboard()
5236 .and_then(|item| item.text().as_deref().map(str::to_string)),
5237 Some(
5238 "in selections.iter() {
5239 let mut start = selection.start;
5240 let mut end = selection.end;
5241 let is_entire_line = selection.is_empty();
5242 if is_entire_line {
5243 start = Point::new(start.row, 0);"
5244 .to_string()
5245 ),
5246 "When selecting past the indent, the copying works as usual",
5247 );
5248 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5249 assert_eq!(
5250 cx.read_from_clipboard()
5251 .and_then(|item| item.text().as_deref().map(str::to_string)),
5252 Some(
5253 "in selections.iter() {
5254 let mut start = selection.start;
5255 let mut end = selection.end;
5256 let is_entire_line = selection.is_empty();
5257 if is_entire_line {
5258 start = Point::new(start.row, 0);"
5259 .to_string()
5260 ),
5261 "When selecting past the indent, nothing is trimmed"
5262 );
5263
5264 cx.set_state(
5265 r#" «for selection in selections.iter() {
5266 let mut start = selection.start;
5267
5268 let mut end = selection.end;
5269 let is_entire_line = selection.is_empty();
5270 if is_entire_line {
5271 start = Point::new(start.row, 0);
5272ˇ» end = cmp::min(max_point, Point::new(end.row + 1, 0));
5273 }
5274 "#,
5275 );
5276 cx.update_editor(|e, window, cx| e.copy_and_trim(&CopyAndTrim, window, cx));
5277 assert_eq!(
5278 cx.read_from_clipboard()
5279 .and_then(|item| item.text().as_deref().map(str::to_string)),
5280 Some(
5281 "for selection in selections.iter() {
5282let mut start = selection.start;
5283
5284let mut end = selection.end;
5285let is_entire_line = selection.is_empty();
5286if is_entire_line {
5287 start = Point::new(start.row, 0);
5288"
5289 .to_string()
5290 ),
5291 "Copying with stripping should ignore empty lines"
5292 );
5293}
5294
5295#[gpui::test]
5296async fn test_paste_multiline(cx: &mut TestAppContext) {
5297 init_test(cx, |_| {});
5298
5299 let mut cx = EditorTestContext::new(cx).await;
5300 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5301
5302 // Cut an indented block, without the leading whitespace.
5303 cx.set_state(indoc! {"
5304 const a: B = (
5305 c(),
5306 «d(
5307 e,
5308 f
5309 )ˇ»
5310 );
5311 "});
5312 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5313 cx.assert_editor_state(indoc! {"
5314 const a: B = (
5315 c(),
5316 ˇ
5317 );
5318 "});
5319
5320 // Paste it at the same position.
5321 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5322 cx.assert_editor_state(indoc! {"
5323 const a: B = (
5324 c(),
5325 d(
5326 e,
5327 f
5328 )ˇ
5329 );
5330 "});
5331
5332 // Paste it at a line with a lower indent level.
5333 cx.set_state(indoc! {"
5334 ˇ
5335 const a: B = (
5336 c(),
5337 );
5338 "});
5339 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5340 cx.assert_editor_state(indoc! {"
5341 d(
5342 e,
5343 f
5344 )ˇ
5345 const a: B = (
5346 c(),
5347 );
5348 "});
5349
5350 // Cut an indented block, with the leading whitespace.
5351 cx.set_state(indoc! {"
5352 const a: B = (
5353 c(),
5354 « d(
5355 e,
5356 f
5357 )
5358 ˇ»);
5359 "});
5360 cx.update_editor(|e, window, cx| e.cut(&Cut, window, cx));
5361 cx.assert_editor_state(indoc! {"
5362 const a: B = (
5363 c(),
5364 ˇ);
5365 "});
5366
5367 // Paste it at the same position.
5368 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5369 cx.assert_editor_state(indoc! {"
5370 const a: B = (
5371 c(),
5372 d(
5373 e,
5374 f
5375 )
5376 ˇ);
5377 "});
5378
5379 // Paste it at a line with a higher indent level.
5380 cx.set_state(indoc! {"
5381 const a: B = (
5382 c(),
5383 d(
5384 e,
5385 fˇ
5386 )
5387 );
5388 "});
5389 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5390 cx.assert_editor_state(indoc! {"
5391 const a: B = (
5392 c(),
5393 d(
5394 e,
5395 f d(
5396 e,
5397 f
5398 )
5399 ˇ
5400 )
5401 );
5402 "});
5403
5404 // Copy an indented block, starting mid-line
5405 cx.set_state(indoc! {"
5406 const a: B = (
5407 c(),
5408 somethin«g(
5409 e,
5410 f
5411 )ˇ»
5412 );
5413 "});
5414 cx.update_editor(|e, window, cx| e.copy(&Copy, window, cx));
5415
5416 // Paste it on a line with a lower indent level
5417 cx.update_editor(|e, window, cx| e.move_to_end(&Default::default(), window, cx));
5418 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5419 cx.assert_editor_state(indoc! {"
5420 const a: B = (
5421 c(),
5422 something(
5423 e,
5424 f
5425 )
5426 );
5427 g(
5428 e,
5429 f
5430 )ˇ"});
5431}
5432
5433#[gpui::test]
5434async fn test_paste_content_from_other_app(cx: &mut TestAppContext) {
5435 init_test(cx, |_| {});
5436
5437 cx.write_to_clipboard(ClipboardItem::new_string(
5438 " d(\n e\n );\n".into(),
5439 ));
5440
5441 let mut cx = EditorTestContext::new(cx).await;
5442 cx.update_buffer(|buffer, cx| buffer.set_language(Some(rust_lang()), cx));
5443
5444 cx.set_state(indoc! {"
5445 fn a() {
5446 b();
5447 if c() {
5448 ˇ
5449 }
5450 }
5451 "});
5452
5453 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5454 cx.assert_editor_state(indoc! {"
5455 fn a() {
5456 b();
5457 if c() {
5458 d(
5459 e
5460 );
5461 ˇ
5462 }
5463 }
5464 "});
5465
5466 cx.set_state(indoc! {"
5467 fn a() {
5468 b();
5469 ˇ
5470 }
5471 "});
5472
5473 cx.update_editor(|e, window, cx| e.paste(&Paste, window, cx));
5474 cx.assert_editor_state(indoc! {"
5475 fn a() {
5476 b();
5477 d(
5478 e
5479 );
5480 ˇ
5481 }
5482 "});
5483}
5484
5485#[gpui::test]
5486fn test_select_all(cx: &mut TestAppContext) {
5487 init_test(cx, |_| {});
5488
5489 let editor = cx.add_window(|window, cx| {
5490 let buffer = MultiBuffer::build_simple("abc\nde\nfgh", cx);
5491 build_editor(buffer, window, cx)
5492 });
5493 _ = editor.update(cx, |editor, window, cx| {
5494 editor.select_all(&SelectAll, window, cx);
5495 assert_eq!(
5496 editor.selections.display_ranges(cx),
5497 &[DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 3)]
5498 );
5499 });
5500}
5501
5502#[gpui::test]
5503fn test_select_line(cx: &mut TestAppContext) {
5504 init_test(cx, |_| {});
5505
5506 let editor = cx.add_window(|window, cx| {
5507 let buffer = MultiBuffer::build_simple(&sample_text(6, 5, 'a'), cx);
5508 build_editor(buffer, window, cx)
5509 });
5510 _ = editor.update(cx, |editor, window, cx| {
5511 editor.change_selections(None, window, cx, |s| {
5512 s.select_display_ranges([
5513 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5514 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5515 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5516 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 2),
5517 ])
5518 });
5519 editor.select_line(&SelectLine, window, cx);
5520 assert_eq!(
5521 editor.selections.display_ranges(cx),
5522 vec![
5523 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(2), 0),
5524 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 0),
5525 ]
5526 );
5527 });
5528
5529 _ = editor.update(cx, |editor, window, cx| {
5530 editor.select_line(&SelectLine, window, cx);
5531 assert_eq!(
5532 editor.selections.display_ranges(cx),
5533 vec![
5534 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(3), 0),
5535 DisplayPoint::new(DisplayRow(4), 0)..DisplayPoint::new(DisplayRow(5), 5),
5536 ]
5537 );
5538 });
5539
5540 _ = editor.update(cx, |editor, window, cx| {
5541 editor.select_line(&SelectLine, window, cx);
5542 assert_eq!(
5543 editor.selections.display_ranges(cx),
5544 vec![DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(5), 5)]
5545 );
5546 });
5547}
5548
5549#[gpui::test]
5550async fn test_split_selection_into_lines(cx: &mut TestAppContext) {
5551 init_test(cx, |_| {});
5552 let mut cx = EditorTestContext::new(cx).await;
5553
5554 #[track_caller]
5555 fn test(cx: &mut EditorTestContext, initial_state: &'static str, expected_state: &'static str) {
5556 cx.set_state(initial_state);
5557 cx.update_editor(|e, window, cx| {
5558 e.split_selection_into_lines(&SplitSelectionIntoLines, window, cx)
5559 });
5560 cx.assert_editor_state(expected_state);
5561 }
5562
5563 // Selection starts and ends at the middle of lines, left-to-right
5564 test(
5565 &mut cx,
5566 "aa\nb«ˇb\ncc\ndd\ne»e\nff",
5567 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5568 );
5569 // Same thing, right-to-left
5570 test(
5571 &mut cx,
5572 "aa\nb«b\ncc\ndd\neˇ»e\nff",
5573 "aa\nbbˇ\nccˇ\nddˇ\neˇe\nff",
5574 );
5575
5576 // Whole buffer, left-to-right, last line *doesn't* end with newline
5577 test(
5578 &mut cx,
5579 "«ˇaa\nbb\ncc\ndd\nee\nff»",
5580 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5581 );
5582 // Same thing, right-to-left
5583 test(
5584 &mut cx,
5585 "«aa\nbb\ncc\ndd\nee\nffˇ»",
5586 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ",
5587 );
5588
5589 // Whole buffer, left-to-right, last line ends with newline
5590 test(
5591 &mut cx,
5592 "«ˇaa\nbb\ncc\ndd\nee\nff\n»",
5593 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5594 );
5595 // Same thing, right-to-left
5596 test(
5597 &mut cx,
5598 "«aa\nbb\ncc\ndd\nee\nff\nˇ»",
5599 "aaˇ\nbbˇ\nccˇ\nddˇ\neeˇ\nffˇ\n",
5600 );
5601
5602 // Starts at the end of a line, ends at the start of another
5603 test(
5604 &mut cx,
5605 "aa\nbb«ˇ\ncc\ndd\nee\n»ff\n",
5606 "aa\nbbˇ\nccˇ\nddˇ\neeˇ\nff\n",
5607 );
5608}
5609
5610#[gpui::test]
5611async fn test_split_selection_into_lines_interacting_with_creases(cx: &mut TestAppContext) {
5612 init_test(cx, |_| {});
5613
5614 let editor = cx.add_window(|window, cx| {
5615 let buffer = MultiBuffer::build_simple(&sample_text(9, 5, 'a'), cx);
5616 build_editor(buffer, window, cx)
5617 });
5618
5619 // setup
5620 _ = editor.update(cx, |editor, window, cx| {
5621 editor.fold_creases(
5622 vec![
5623 Crease::simple(Point::new(0, 2)..Point::new(1, 2), FoldPlaceholder::test()),
5624 Crease::simple(Point::new(2, 3)..Point::new(4, 1), FoldPlaceholder::test()),
5625 Crease::simple(Point::new(7, 0)..Point::new(8, 4), FoldPlaceholder::test()),
5626 ],
5627 true,
5628 window,
5629 cx,
5630 );
5631 assert_eq!(
5632 editor.display_text(cx),
5633 "aa⋯bbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5634 );
5635 });
5636
5637 _ = editor.update(cx, |editor, window, cx| {
5638 editor.change_selections(None, window, cx, |s| {
5639 s.select_display_ranges([
5640 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
5641 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 2),
5642 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 0),
5643 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
5644 ])
5645 });
5646 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5647 assert_eq!(
5648 editor.display_text(cx),
5649 "aaaaa\nbbbbb\nccc⋯eeee\nfffff\nggggg\n⋯i"
5650 );
5651 });
5652 EditorTestContext::for_editor(editor, cx)
5653 .await
5654 .assert_editor_state("aˇaˇaaa\nbbbbb\nˇccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiiiˇ");
5655
5656 _ = editor.update(cx, |editor, window, cx| {
5657 editor.change_selections(None, window, cx, |s| {
5658 s.select_display_ranges([
5659 DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 1)
5660 ])
5661 });
5662 editor.split_selection_into_lines(&SplitSelectionIntoLines, window, cx);
5663 assert_eq!(
5664 editor.display_text(cx),
5665 "aaaaa\nbbbbb\nccccc\nddddd\neeeee\nfffff\nggggg\nhhhhh\niiiii"
5666 );
5667 assert_eq!(
5668 editor.selections.display_ranges(cx),
5669 [
5670 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5),
5671 DisplayPoint::new(DisplayRow(1), 5)..DisplayPoint::new(DisplayRow(1), 5),
5672 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
5673 DisplayPoint::new(DisplayRow(3), 5)..DisplayPoint::new(DisplayRow(3), 5),
5674 DisplayPoint::new(DisplayRow(4), 5)..DisplayPoint::new(DisplayRow(4), 5),
5675 DisplayPoint::new(DisplayRow(5), 5)..DisplayPoint::new(DisplayRow(5), 5),
5676 DisplayPoint::new(DisplayRow(6), 5)..DisplayPoint::new(DisplayRow(6), 5)
5677 ]
5678 );
5679 });
5680 EditorTestContext::for_editor(editor, cx)
5681 .await
5682 .assert_editor_state(
5683 "aaaaaˇ\nbbbbbˇ\ncccccˇ\ndddddˇ\neeeeeˇ\nfffffˇ\ngggggˇ\nhhhhh\niiiii",
5684 );
5685}
5686
5687#[gpui::test]
5688async fn test_add_selection_above_below(cx: &mut TestAppContext) {
5689 init_test(cx, |_| {});
5690
5691 let mut cx = EditorTestContext::new(cx).await;
5692
5693 cx.set_state(indoc!(
5694 r#"abc
5695 defˇghi
5696
5697 jk
5698 nlmo
5699 "#
5700 ));
5701
5702 cx.update_editor(|editor, window, cx| {
5703 editor.add_selection_above(&Default::default(), window, cx);
5704 });
5705
5706 cx.assert_editor_state(indoc!(
5707 r#"abcˇ
5708 defˇghi
5709
5710 jk
5711 nlmo
5712 "#
5713 ));
5714
5715 cx.update_editor(|editor, window, cx| {
5716 editor.add_selection_above(&Default::default(), window, cx);
5717 });
5718
5719 cx.assert_editor_state(indoc!(
5720 r#"abcˇ
5721 defˇghi
5722
5723 jk
5724 nlmo
5725 "#
5726 ));
5727
5728 cx.update_editor(|editor, window, cx| {
5729 editor.add_selection_below(&Default::default(), window, cx);
5730 });
5731
5732 cx.assert_editor_state(indoc!(
5733 r#"abc
5734 defˇghi
5735
5736 jk
5737 nlmo
5738 "#
5739 ));
5740
5741 cx.update_editor(|editor, window, cx| {
5742 editor.undo_selection(&Default::default(), window, cx);
5743 });
5744
5745 cx.assert_editor_state(indoc!(
5746 r#"abcˇ
5747 defˇghi
5748
5749 jk
5750 nlmo
5751 "#
5752 ));
5753
5754 cx.update_editor(|editor, window, cx| {
5755 editor.redo_selection(&Default::default(), window, cx);
5756 });
5757
5758 cx.assert_editor_state(indoc!(
5759 r#"abc
5760 defˇghi
5761
5762 jk
5763 nlmo
5764 "#
5765 ));
5766
5767 cx.update_editor(|editor, window, cx| {
5768 editor.add_selection_below(&Default::default(), window, cx);
5769 });
5770
5771 cx.assert_editor_state(indoc!(
5772 r#"abc
5773 defˇghi
5774
5775 jk
5776 nlmˇo
5777 "#
5778 ));
5779
5780 cx.update_editor(|editor, window, cx| {
5781 editor.add_selection_below(&Default::default(), window, cx);
5782 });
5783
5784 cx.assert_editor_state(indoc!(
5785 r#"abc
5786 defˇghi
5787
5788 jk
5789 nlmˇo
5790 "#
5791 ));
5792
5793 // change selections
5794 cx.set_state(indoc!(
5795 r#"abc
5796 def«ˇg»hi
5797
5798 jk
5799 nlmo
5800 "#
5801 ));
5802
5803 cx.update_editor(|editor, window, cx| {
5804 editor.add_selection_below(&Default::default(), window, cx);
5805 });
5806
5807 cx.assert_editor_state(indoc!(
5808 r#"abc
5809 def«ˇg»hi
5810
5811 jk
5812 nlm«ˇo»
5813 "#
5814 ));
5815
5816 cx.update_editor(|editor, window, cx| {
5817 editor.add_selection_below(&Default::default(), window, cx);
5818 });
5819
5820 cx.assert_editor_state(indoc!(
5821 r#"abc
5822 def«ˇg»hi
5823
5824 jk
5825 nlm«ˇo»
5826 "#
5827 ));
5828
5829 cx.update_editor(|editor, window, cx| {
5830 editor.add_selection_above(&Default::default(), window, cx);
5831 });
5832
5833 cx.assert_editor_state(indoc!(
5834 r#"abc
5835 def«ˇg»hi
5836
5837 jk
5838 nlmo
5839 "#
5840 ));
5841
5842 cx.update_editor(|editor, window, cx| {
5843 editor.add_selection_above(&Default::default(), window, cx);
5844 });
5845
5846 cx.assert_editor_state(indoc!(
5847 r#"abc
5848 def«ˇg»hi
5849
5850 jk
5851 nlmo
5852 "#
5853 ));
5854
5855 // Change selections again
5856 cx.set_state(indoc!(
5857 r#"a«bc
5858 defgˇ»hi
5859
5860 jk
5861 nlmo
5862 "#
5863 ));
5864
5865 cx.update_editor(|editor, window, cx| {
5866 editor.add_selection_below(&Default::default(), window, cx);
5867 });
5868
5869 cx.assert_editor_state(indoc!(
5870 r#"a«bcˇ»
5871 d«efgˇ»hi
5872
5873 j«kˇ»
5874 nlmo
5875 "#
5876 ));
5877
5878 cx.update_editor(|editor, window, cx| {
5879 editor.add_selection_below(&Default::default(), window, cx);
5880 });
5881 cx.assert_editor_state(indoc!(
5882 r#"a«bcˇ»
5883 d«efgˇ»hi
5884
5885 j«kˇ»
5886 n«lmoˇ»
5887 "#
5888 ));
5889 cx.update_editor(|editor, window, cx| {
5890 editor.add_selection_above(&Default::default(), window, cx);
5891 });
5892
5893 cx.assert_editor_state(indoc!(
5894 r#"a«bcˇ»
5895 d«efgˇ»hi
5896
5897 j«kˇ»
5898 nlmo
5899 "#
5900 ));
5901
5902 // Change selections again
5903 cx.set_state(indoc!(
5904 r#"abc
5905 d«ˇefghi
5906
5907 jk
5908 nlm»o
5909 "#
5910 ));
5911
5912 cx.update_editor(|editor, window, cx| {
5913 editor.add_selection_above(&Default::default(), window, cx);
5914 });
5915
5916 cx.assert_editor_state(indoc!(
5917 r#"a«ˇbc»
5918 d«ˇef»ghi
5919
5920 j«ˇk»
5921 n«ˇlm»o
5922 "#
5923 ));
5924
5925 cx.update_editor(|editor, window, cx| {
5926 editor.add_selection_below(&Default::default(), window, cx);
5927 });
5928
5929 cx.assert_editor_state(indoc!(
5930 r#"abc
5931 d«ˇef»ghi
5932
5933 j«ˇk»
5934 n«ˇlm»o
5935 "#
5936 ));
5937}
5938
5939#[gpui::test]
5940async fn test_select_next(cx: &mut TestAppContext) {
5941 init_test(cx, |_| {});
5942
5943 let mut cx = EditorTestContext::new(cx).await;
5944 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5945
5946 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5947 .unwrap();
5948 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5949
5950 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5951 .unwrap();
5952 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5953
5954 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
5955 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
5956
5957 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
5958 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\nabc");
5959
5960 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5961 .unwrap();
5962 cx.assert_editor_state("abc\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5963
5964 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5965 .unwrap();
5966 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5967
5968 // Test selection direction should be preserved
5969 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
5970
5971 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
5972 .unwrap();
5973 cx.assert_editor_state("abc\n«ˇabc» «ˇabc»\ndefabc\nabc");
5974}
5975
5976#[gpui::test]
5977async fn test_select_all_matches(cx: &mut TestAppContext) {
5978 init_test(cx, |_| {});
5979
5980 let mut cx = EditorTestContext::new(cx).await;
5981
5982 // Test caret-only selections
5983 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
5984 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5985 .unwrap();
5986 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
5987
5988 // Test left-to-right selections
5989 cx.set_state("abc\n«abcˇ»\nabc");
5990 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5991 .unwrap();
5992 cx.assert_editor_state("«abcˇ»\n«abcˇ»\n«abcˇ»");
5993
5994 // Test right-to-left selections
5995 cx.set_state("abc\n«ˇabc»\nabc");
5996 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
5997 .unwrap();
5998 cx.assert_editor_state("«ˇabc»\n«ˇabc»\n«ˇabc»");
5999
6000 // Test selecting whitespace with caret selection
6001 cx.set_state("abc\nˇ abc\nabc");
6002 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6003 .unwrap();
6004 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6005
6006 // Test selecting whitespace with left-to-right selection
6007 cx.set_state("abc\n«ˇ »abc\nabc");
6008 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6009 .unwrap();
6010 cx.assert_editor_state("abc\n«ˇ »abc\nabc");
6011
6012 // Test no matches with right-to-left selection
6013 cx.set_state("abc\n« ˇ»abc\nabc");
6014 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6015 .unwrap();
6016 cx.assert_editor_state("abc\n« ˇ»abc\nabc");
6017}
6018
6019#[gpui::test]
6020async fn test_select_all_matches_does_not_scroll(cx: &mut TestAppContext) {
6021 init_test(cx, |_| {});
6022
6023 let mut cx = EditorTestContext::new(cx).await;
6024
6025 let large_body_1 = "\nd".repeat(200);
6026 let large_body_2 = "\ne".repeat(200);
6027
6028 cx.set_state(&format!(
6029 "abc\nabc{large_body_1} «ˇa»bc{large_body_2}\nefabc\nabc"
6030 ));
6031 let initial_scroll_position = cx.update_editor(|editor, _, cx| {
6032 let scroll_position = editor.scroll_position(cx);
6033 assert!(scroll_position.y > 0.0, "Initial selection is between two large bodies and should have the editor scrolled to it");
6034 scroll_position
6035 });
6036
6037 cx.update_editor(|e, window, cx| e.select_all_matches(&SelectAllMatches, window, cx))
6038 .unwrap();
6039 cx.assert_editor_state(&format!(
6040 "«ˇa»bc\n«ˇa»bc{large_body_1} «ˇa»bc{large_body_2}\nef«ˇa»bc\n«ˇa»bc"
6041 ));
6042 let scroll_position_after_selection =
6043 cx.update_editor(|editor, _, cx| editor.scroll_position(cx));
6044 assert_eq!(
6045 initial_scroll_position, scroll_position_after_selection,
6046 "Scroll position should not change after selecting all matches"
6047 );
6048}
6049
6050#[gpui::test]
6051async fn test_undo_format_scrolls_to_last_edit_pos(cx: &mut TestAppContext) {
6052 init_test(cx, |_| {});
6053
6054 let mut cx = EditorLspTestContext::new_rust(
6055 lsp::ServerCapabilities {
6056 document_formatting_provider: Some(lsp::OneOf::Left(true)),
6057 ..Default::default()
6058 },
6059 cx,
6060 )
6061 .await;
6062
6063 cx.set_state(indoc! {"
6064 line 1
6065 line 2
6066 linˇe 3
6067 line 4
6068 line 5
6069 "});
6070
6071 // Make an edit
6072 cx.update_editor(|editor, window, cx| {
6073 editor.handle_input("X", window, cx);
6074 });
6075
6076 // Move cursor to a different position
6077 cx.update_editor(|editor, window, cx| {
6078 editor.change_selections(None, window, cx, |s| {
6079 s.select_ranges([Point::new(4, 2)..Point::new(4, 2)]);
6080 });
6081 });
6082
6083 cx.assert_editor_state(indoc! {"
6084 line 1
6085 line 2
6086 linXe 3
6087 line 4
6088 liˇne 5
6089 "});
6090
6091 cx.lsp
6092 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
6093 Ok(Some(vec![lsp::TextEdit::new(
6094 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
6095 "PREFIX ".to_string(),
6096 )]))
6097 });
6098
6099 cx.update_editor(|editor, window, cx| editor.format(&Default::default(), window, cx))
6100 .unwrap()
6101 .await
6102 .unwrap();
6103
6104 cx.assert_editor_state(indoc! {"
6105 PREFIX line 1
6106 line 2
6107 linXe 3
6108 line 4
6109 liˇne 5
6110 "});
6111
6112 // Undo formatting
6113 cx.update_editor(|editor, window, cx| {
6114 editor.undo(&Default::default(), window, cx);
6115 });
6116
6117 // Verify cursor moved back to position after edit
6118 cx.assert_editor_state(indoc! {"
6119 line 1
6120 line 2
6121 linXˇe 3
6122 line 4
6123 line 5
6124 "});
6125}
6126
6127#[gpui::test]
6128async fn test_select_next_with_multiple_carets(cx: &mut TestAppContext) {
6129 init_test(cx, |_| {});
6130
6131 let mut cx = EditorTestContext::new(cx).await;
6132 cx.set_state(
6133 r#"let foo = 2;
6134lˇet foo = 2;
6135let fooˇ = 2;
6136let foo = 2;
6137let foo = ˇ2;"#,
6138 );
6139
6140 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6141 .unwrap();
6142 cx.assert_editor_state(
6143 r#"let foo = 2;
6144«letˇ» foo = 2;
6145let «fooˇ» = 2;
6146let foo = 2;
6147let foo = «2ˇ»;"#,
6148 );
6149
6150 // noop for multiple selections with different contents
6151 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6152 .unwrap();
6153 cx.assert_editor_state(
6154 r#"let foo = 2;
6155«letˇ» foo = 2;
6156let «fooˇ» = 2;
6157let foo = 2;
6158let foo = «2ˇ»;"#,
6159 );
6160
6161 // Test last selection direction should be preserved
6162 cx.set_state(
6163 r#"let foo = 2;
6164let foo = 2;
6165let «fooˇ» = 2;
6166let «ˇfoo» = 2;
6167let foo = 2;"#,
6168 );
6169
6170 cx.update_editor(|e, window, cx| e.select_next(&SelectNext::default(), window, cx))
6171 .unwrap();
6172 cx.assert_editor_state(
6173 r#"let foo = 2;
6174let foo = 2;
6175let «fooˇ» = 2;
6176let «ˇfoo» = 2;
6177let «ˇfoo» = 2;"#,
6178 );
6179}
6180
6181#[gpui::test]
6182async fn test_select_previous_multibuffer(cx: &mut TestAppContext) {
6183 init_test(cx, |_| {});
6184
6185 let mut cx =
6186 EditorTestContext::new_multibuffer(cx, ["aaa\n«bbb\nccc\n»ddd", "aaa\n«bbb\nccc\n»ddd"]);
6187
6188 cx.assert_editor_state(indoc! {"
6189 ˇbbb
6190 ccc
6191
6192 bbb
6193 ccc
6194 "});
6195 cx.dispatch_action(SelectPrevious::default());
6196 cx.assert_editor_state(indoc! {"
6197 «bbbˇ»
6198 ccc
6199
6200 bbb
6201 ccc
6202 "});
6203 cx.dispatch_action(SelectPrevious::default());
6204 cx.assert_editor_state(indoc! {"
6205 «bbbˇ»
6206 ccc
6207
6208 «bbbˇ»
6209 ccc
6210 "});
6211}
6212
6213#[gpui::test]
6214async fn test_select_previous_with_single_caret(cx: &mut TestAppContext) {
6215 init_test(cx, |_| {});
6216
6217 let mut cx = EditorTestContext::new(cx).await;
6218 cx.set_state("abc\nˇabc abc\ndefabc\nabc");
6219
6220 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6221 .unwrap();
6222 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6223
6224 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6225 .unwrap();
6226 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6227
6228 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6229 cx.assert_editor_state("abc\n«abcˇ» abc\ndefabc\nabc");
6230
6231 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6232 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\nabc");
6233
6234 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6235 .unwrap();
6236 cx.assert_editor_state("«abcˇ»\n«abcˇ» abc\ndefabc\n«abcˇ»");
6237
6238 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6239 .unwrap();
6240 cx.assert_editor_state("«abcˇ»\n«abcˇ» «abcˇ»\ndefabc\n«abcˇ»");
6241}
6242
6243#[gpui::test]
6244async fn test_select_previous_empty_buffer(cx: &mut TestAppContext) {
6245 init_test(cx, |_| {});
6246
6247 let mut cx = EditorTestContext::new(cx).await;
6248 cx.set_state("aˇ");
6249
6250 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6251 .unwrap();
6252 cx.assert_editor_state("«aˇ»");
6253 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6254 .unwrap();
6255 cx.assert_editor_state("«aˇ»");
6256}
6257
6258#[gpui::test]
6259async fn test_select_previous_with_multiple_carets(cx: &mut TestAppContext) {
6260 init_test(cx, |_| {});
6261
6262 let mut cx = EditorTestContext::new(cx).await;
6263 cx.set_state(
6264 r#"let foo = 2;
6265lˇet foo = 2;
6266let fooˇ = 2;
6267let foo = 2;
6268let foo = ˇ2;"#,
6269 );
6270
6271 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6272 .unwrap();
6273 cx.assert_editor_state(
6274 r#"let foo = 2;
6275«letˇ» foo = 2;
6276let «fooˇ» = 2;
6277let foo = 2;
6278let foo = «2ˇ»;"#,
6279 );
6280
6281 // noop for multiple selections with different contents
6282 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6283 .unwrap();
6284 cx.assert_editor_state(
6285 r#"let foo = 2;
6286«letˇ» foo = 2;
6287let «fooˇ» = 2;
6288let foo = 2;
6289let foo = «2ˇ»;"#,
6290 );
6291}
6292
6293#[gpui::test]
6294async fn test_select_previous_with_single_selection(cx: &mut TestAppContext) {
6295 init_test(cx, |_| {});
6296
6297 let mut cx = EditorTestContext::new(cx).await;
6298 cx.set_state("abc\n«ˇabc» abc\ndefabc\nabc");
6299
6300 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6301 .unwrap();
6302 // selection direction is preserved
6303 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6304
6305 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6306 .unwrap();
6307 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6308
6309 cx.update_editor(|editor, window, cx| editor.undo_selection(&UndoSelection, window, cx));
6310 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\nabc");
6311
6312 cx.update_editor(|editor, window, cx| editor.redo_selection(&RedoSelection, window, cx));
6313 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndefabc\n«ˇabc»");
6314
6315 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6316 .unwrap();
6317 cx.assert_editor_state("«ˇabc»\n«ˇabc» abc\ndef«ˇabc»\n«ˇabc»");
6318
6319 cx.update_editor(|e, window, cx| e.select_previous(&SelectPrevious::default(), window, cx))
6320 .unwrap();
6321 cx.assert_editor_state("«ˇabc»\n«ˇabc» «ˇabc»\ndef«ˇabc»\n«ˇabc»");
6322}
6323
6324#[gpui::test]
6325async fn test_select_larger_smaller_syntax_node(cx: &mut TestAppContext) {
6326 init_test(cx, |_| {});
6327
6328 let language = Arc::new(Language::new(
6329 LanguageConfig::default(),
6330 Some(tree_sitter_rust::LANGUAGE.into()),
6331 ));
6332
6333 let text = r#"
6334 use mod1::mod2::{mod3, mod4};
6335
6336 fn fn_1(param1: bool, param2: &str) {
6337 let var1 = "text";
6338 }
6339 "#
6340 .unindent();
6341
6342 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6343 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6344 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6345
6346 editor
6347 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6348 .await;
6349
6350 editor.update_in(cx, |editor, window, cx| {
6351 editor.change_selections(None, window, cx, |s| {
6352 s.select_display_ranges([
6353 DisplayPoint::new(DisplayRow(0), 25)..DisplayPoint::new(DisplayRow(0), 25),
6354 DisplayPoint::new(DisplayRow(2), 24)..DisplayPoint::new(DisplayRow(2), 12),
6355 DisplayPoint::new(DisplayRow(3), 18)..DisplayPoint::new(DisplayRow(3), 18),
6356 ]);
6357 });
6358 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6359 });
6360 editor.update(cx, |editor, cx| {
6361 assert_text_with_selections(
6362 editor,
6363 indoc! {r#"
6364 use mod1::mod2::{mod3, «mod4ˇ»};
6365
6366 fn fn_1«ˇ(param1: bool, param2: &str)» {
6367 let var1 = "«ˇtext»";
6368 }
6369 "#},
6370 cx,
6371 );
6372 });
6373
6374 editor.update_in(cx, |editor, window, cx| {
6375 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6376 });
6377 editor.update(cx, |editor, cx| {
6378 assert_text_with_selections(
6379 editor,
6380 indoc! {r#"
6381 use mod1::mod2::«{mod3, mod4}ˇ»;
6382
6383 «ˇfn fn_1(param1: bool, param2: &str) {
6384 let var1 = "text";
6385 }»
6386 "#},
6387 cx,
6388 );
6389 });
6390
6391 editor.update_in(cx, |editor, window, cx| {
6392 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6393 });
6394 assert_eq!(
6395 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6396 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6397 );
6398
6399 // Trying to expand the selected syntax node one more time has no effect.
6400 editor.update_in(cx, |editor, window, cx| {
6401 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6402 });
6403 assert_eq!(
6404 editor.update(cx, |editor, cx| editor.selections.display_ranges(cx)),
6405 &[DisplayPoint::new(DisplayRow(5), 0)..DisplayPoint::new(DisplayRow(0), 0)]
6406 );
6407
6408 editor.update_in(cx, |editor, window, cx| {
6409 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6410 });
6411 editor.update(cx, |editor, cx| {
6412 assert_text_with_selections(
6413 editor,
6414 indoc! {r#"
6415 use mod1::mod2::«{mod3, mod4}ˇ»;
6416
6417 «ˇfn fn_1(param1: bool, param2: &str) {
6418 let var1 = "text";
6419 }»
6420 "#},
6421 cx,
6422 );
6423 });
6424
6425 editor.update_in(cx, |editor, window, cx| {
6426 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6427 });
6428 editor.update(cx, |editor, cx| {
6429 assert_text_with_selections(
6430 editor,
6431 indoc! {r#"
6432 use mod1::mod2::{mod3, «mod4ˇ»};
6433
6434 fn fn_1«ˇ(param1: bool, param2: &str)» {
6435 let var1 = "«ˇtext»";
6436 }
6437 "#},
6438 cx,
6439 );
6440 });
6441
6442 editor.update_in(cx, |editor, window, cx| {
6443 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6444 });
6445 editor.update(cx, |editor, cx| {
6446 assert_text_with_selections(
6447 editor,
6448 indoc! {r#"
6449 use mod1::mod2::{mod3, mo«ˇ»d4};
6450
6451 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6452 let var1 = "te«ˇ»xt";
6453 }
6454 "#},
6455 cx,
6456 );
6457 });
6458
6459 // Trying to shrink the selected syntax node one more time has no effect.
6460 editor.update_in(cx, |editor, window, cx| {
6461 editor.select_smaller_syntax_node(&SelectSmallerSyntaxNode, window, cx);
6462 });
6463 editor.update_in(cx, |editor, _, cx| {
6464 assert_text_with_selections(
6465 editor,
6466 indoc! {r#"
6467 use mod1::mod2::{mod3, mo«ˇ»d4};
6468
6469 fn fn_1(para«ˇm1: bool, pa»ram2: &str) {
6470 let var1 = "te«ˇ»xt";
6471 }
6472 "#},
6473 cx,
6474 );
6475 });
6476
6477 // Ensure that we keep expanding the selection if the larger selection starts or ends within
6478 // a fold.
6479 editor.update_in(cx, |editor, window, cx| {
6480 editor.fold_creases(
6481 vec![
6482 Crease::simple(
6483 Point::new(0, 21)..Point::new(0, 24),
6484 FoldPlaceholder::test(),
6485 ),
6486 Crease::simple(
6487 Point::new(3, 20)..Point::new(3, 22),
6488 FoldPlaceholder::test(),
6489 ),
6490 ],
6491 true,
6492 window,
6493 cx,
6494 );
6495 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6496 });
6497 editor.update(cx, |editor, cx| {
6498 assert_text_with_selections(
6499 editor,
6500 indoc! {r#"
6501 use mod1::mod2::«{mod3, mod4}ˇ»;
6502
6503 fn fn_1«ˇ(param1: bool, param2: &str)» {
6504 let var1 = "«ˇtext»";
6505 }
6506 "#},
6507 cx,
6508 );
6509 });
6510}
6511
6512#[gpui::test]
6513async fn test_select_larger_syntax_node_for_cursor_at_end(cx: &mut TestAppContext) {
6514 init_test(cx, |_| {});
6515
6516 let language = Arc::new(Language::new(
6517 LanguageConfig::default(),
6518 Some(tree_sitter_rust::LANGUAGE.into()),
6519 ));
6520
6521 let text = "let a = 2;";
6522
6523 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6524 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6525 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6526
6527 editor
6528 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6529 .await;
6530
6531 // Test case 1: Cursor at end of word
6532 editor.update_in(cx, |editor, window, cx| {
6533 editor.change_selections(None, window, cx, |s| {
6534 s.select_display_ranges([
6535 DisplayPoint::new(DisplayRow(0), 5)..DisplayPoint::new(DisplayRow(0), 5)
6536 ]);
6537 });
6538 });
6539 editor.update(cx, |editor, cx| {
6540 assert_text_with_selections(editor, "let aˇ = 2;", cx);
6541 });
6542 editor.update_in(cx, |editor, window, cx| {
6543 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6544 });
6545 editor.update(cx, |editor, cx| {
6546 assert_text_with_selections(editor, "let «ˇa» = 2;", cx);
6547 });
6548 editor.update_in(cx, |editor, window, cx| {
6549 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6550 });
6551 editor.update(cx, |editor, cx| {
6552 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6553 });
6554
6555 // Test case 2: Cursor at end of statement
6556 editor.update_in(cx, |editor, window, cx| {
6557 editor.change_selections(None, window, cx, |s| {
6558 s.select_display_ranges([
6559 DisplayPoint::new(DisplayRow(0), 11)..DisplayPoint::new(DisplayRow(0), 11)
6560 ]);
6561 });
6562 });
6563 editor.update(cx, |editor, cx| {
6564 assert_text_with_selections(editor, "let a = 2;ˇ", cx);
6565 });
6566 editor.update_in(cx, |editor, window, cx| {
6567 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6568 });
6569 editor.update(cx, |editor, cx| {
6570 assert_text_with_selections(editor, "«ˇlet a = 2;»", cx);
6571 });
6572}
6573
6574#[gpui::test]
6575async fn test_select_larger_smaller_syntax_node_for_string(cx: &mut TestAppContext) {
6576 init_test(cx, |_| {});
6577
6578 let language = Arc::new(Language::new(
6579 LanguageConfig::default(),
6580 Some(tree_sitter_rust::LANGUAGE.into()),
6581 ));
6582
6583 let text = r#"
6584 use mod1::mod2::{mod3, mod4};
6585
6586 fn fn_1(param1: bool, param2: &str) {
6587 let var1 = "hello world";
6588 }
6589 "#
6590 .unindent();
6591
6592 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6593 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6594 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6595
6596 editor
6597 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6598 .await;
6599
6600 // Test 1: Cursor on a letter of a string word
6601 editor.update_in(cx, |editor, window, cx| {
6602 editor.change_selections(None, window, cx, |s| {
6603 s.select_display_ranges([
6604 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 17)
6605 ]);
6606 });
6607 });
6608 editor.update_in(cx, |editor, window, cx| {
6609 assert_text_with_selections(
6610 editor,
6611 indoc! {r#"
6612 use mod1::mod2::{mod3, mod4};
6613
6614 fn fn_1(param1: bool, param2: &str) {
6615 let var1 = "hˇello world";
6616 }
6617 "#},
6618 cx,
6619 );
6620 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6621 assert_text_with_selections(
6622 editor,
6623 indoc! {r#"
6624 use mod1::mod2::{mod3, mod4};
6625
6626 fn fn_1(param1: bool, param2: &str) {
6627 let var1 = "«ˇhello» world";
6628 }
6629 "#},
6630 cx,
6631 );
6632 });
6633
6634 // Test 2: Partial selection within a word
6635 editor.update_in(cx, |editor, window, cx| {
6636 editor.change_selections(None, window, cx, |s| {
6637 s.select_display_ranges([
6638 DisplayPoint::new(DisplayRow(3), 17)..DisplayPoint::new(DisplayRow(3), 19)
6639 ]);
6640 });
6641 });
6642 editor.update_in(cx, |editor, window, cx| {
6643 assert_text_with_selections(
6644 editor,
6645 indoc! {r#"
6646 use mod1::mod2::{mod3, mod4};
6647
6648 fn fn_1(param1: bool, param2: &str) {
6649 let var1 = "h«elˇ»lo world";
6650 }
6651 "#},
6652 cx,
6653 );
6654 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6655 assert_text_with_selections(
6656 editor,
6657 indoc! {r#"
6658 use mod1::mod2::{mod3, mod4};
6659
6660 fn fn_1(param1: bool, param2: &str) {
6661 let var1 = "«ˇhello» world";
6662 }
6663 "#},
6664 cx,
6665 );
6666 });
6667
6668 // Test 3: Complete word already selected
6669 editor.update_in(cx, |editor, window, cx| {
6670 editor.change_selections(None, window, cx, |s| {
6671 s.select_display_ranges([
6672 DisplayPoint::new(DisplayRow(3), 16)..DisplayPoint::new(DisplayRow(3), 21)
6673 ]);
6674 });
6675 });
6676 editor.update_in(cx, |editor, window, cx| {
6677 assert_text_with_selections(
6678 editor,
6679 indoc! {r#"
6680 use mod1::mod2::{mod3, mod4};
6681
6682 fn fn_1(param1: bool, param2: &str) {
6683 let var1 = "«helloˇ» world";
6684 }
6685 "#},
6686 cx,
6687 );
6688 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6689 assert_text_with_selections(
6690 editor,
6691 indoc! {r#"
6692 use mod1::mod2::{mod3, mod4};
6693
6694 fn fn_1(param1: bool, param2: &str) {
6695 let var1 = "«hello worldˇ»";
6696 }
6697 "#},
6698 cx,
6699 );
6700 });
6701
6702 // Test 4: Selection spanning across words
6703 editor.update_in(cx, |editor, window, cx| {
6704 editor.change_selections(None, window, cx, |s| {
6705 s.select_display_ranges([
6706 DisplayPoint::new(DisplayRow(3), 19)..DisplayPoint::new(DisplayRow(3), 24)
6707 ]);
6708 });
6709 });
6710 editor.update_in(cx, |editor, window, cx| {
6711 assert_text_with_selections(
6712 editor,
6713 indoc! {r#"
6714 use mod1::mod2::{mod3, mod4};
6715
6716 fn fn_1(param1: bool, param2: &str) {
6717 let var1 = "hel«lo woˇ»rld";
6718 }
6719 "#},
6720 cx,
6721 );
6722 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6723 assert_text_with_selections(
6724 editor,
6725 indoc! {r#"
6726 use mod1::mod2::{mod3, mod4};
6727
6728 fn fn_1(param1: bool, param2: &str) {
6729 let var1 = "«ˇhello world»";
6730 }
6731 "#},
6732 cx,
6733 );
6734 });
6735
6736 // Test 5: Expansion beyond string
6737 editor.update_in(cx, |editor, window, cx| {
6738 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6739 editor.select_larger_syntax_node(&SelectLargerSyntaxNode, window, cx);
6740 assert_text_with_selections(
6741 editor,
6742 indoc! {r#"
6743 use mod1::mod2::{mod3, mod4};
6744
6745 fn fn_1(param1: bool, param2: &str) {
6746 «ˇlet var1 = "hello world";»
6747 }
6748 "#},
6749 cx,
6750 );
6751 });
6752}
6753
6754#[gpui::test]
6755async fn test_fold_function_bodies(cx: &mut TestAppContext) {
6756 init_test(cx, |_| {});
6757
6758 let base_text = r#"
6759 impl A {
6760 // this is an uncommitted comment
6761
6762 fn b() {
6763 c();
6764 }
6765
6766 // this is another uncommitted comment
6767
6768 fn d() {
6769 // e
6770 // f
6771 }
6772 }
6773
6774 fn g() {
6775 // h
6776 }
6777 "#
6778 .unindent();
6779
6780 let text = r#"
6781 ˇimpl A {
6782
6783 fn b() {
6784 c();
6785 }
6786
6787 fn d() {
6788 // e
6789 // f
6790 }
6791 }
6792
6793 fn g() {
6794 // h
6795 }
6796 "#
6797 .unindent();
6798
6799 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6800 cx.set_state(&text);
6801 cx.set_head_text(&base_text);
6802 cx.update_editor(|editor, window, cx| {
6803 editor.expand_all_diff_hunks(&Default::default(), window, cx);
6804 });
6805
6806 cx.assert_state_with_diff(
6807 "
6808 ˇimpl A {
6809 - // this is an uncommitted comment
6810
6811 fn b() {
6812 c();
6813 }
6814
6815 - // this is another uncommitted comment
6816 -
6817 fn d() {
6818 // e
6819 // f
6820 }
6821 }
6822
6823 fn g() {
6824 // h
6825 }
6826 "
6827 .unindent(),
6828 );
6829
6830 let expected_display_text = "
6831 impl A {
6832 // this is an uncommitted comment
6833
6834 fn b() {
6835 ⋯
6836 }
6837
6838 // this is another uncommitted comment
6839
6840 fn d() {
6841 ⋯
6842 }
6843 }
6844
6845 fn g() {
6846 ⋯
6847 }
6848 "
6849 .unindent();
6850
6851 cx.update_editor(|editor, window, cx| {
6852 editor.fold_function_bodies(&FoldFunctionBodies, window, cx);
6853 assert_eq!(editor.display_text(cx), expected_display_text);
6854 });
6855}
6856
6857#[gpui::test]
6858async fn test_autoindent(cx: &mut TestAppContext) {
6859 init_test(cx, |_| {});
6860
6861 let language = Arc::new(
6862 Language::new(
6863 LanguageConfig {
6864 brackets: BracketPairConfig {
6865 pairs: vec![
6866 BracketPair {
6867 start: "{".to_string(),
6868 end: "}".to_string(),
6869 close: false,
6870 surround: false,
6871 newline: true,
6872 },
6873 BracketPair {
6874 start: "(".to_string(),
6875 end: ")".to_string(),
6876 close: false,
6877 surround: false,
6878 newline: true,
6879 },
6880 ],
6881 ..Default::default()
6882 },
6883 ..Default::default()
6884 },
6885 Some(tree_sitter_rust::LANGUAGE.into()),
6886 )
6887 .with_indents_query(
6888 r#"
6889 (_ "(" ")" @end) @indent
6890 (_ "{" "}" @end) @indent
6891 "#,
6892 )
6893 .unwrap(),
6894 );
6895
6896 let text = "fn a() {}";
6897
6898 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
6899 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
6900 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
6901 editor
6902 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
6903 .await;
6904
6905 editor.update_in(cx, |editor, window, cx| {
6906 editor.change_selections(None, window, cx, |s| s.select_ranges([5..5, 8..8, 9..9]));
6907 editor.newline(&Newline, window, cx);
6908 assert_eq!(editor.text(cx), "fn a(\n \n) {\n \n}\n");
6909 assert_eq!(
6910 editor.selections.ranges(cx),
6911 &[
6912 Point::new(1, 4)..Point::new(1, 4),
6913 Point::new(3, 4)..Point::new(3, 4),
6914 Point::new(5, 0)..Point::new(5, 0)
6915 ]
6916 );
6917 });
6918}
6919
6920#[gpui::test]
6921async fn test_autoindent_selections(cx: &mut TestAppContext) {
6922 init_test(cx, |_| {});
6923
6924 {
6925 let mut cx = EditorLspTestContext::new_rust(Default::default(), cx).await;
6926 cx.set_state(indoc! {"
6927 impl A {
6928
6929 fn b() {}
6930
6931 «fn c() {
6932
6933 }ˇ»
6934 }
6935 "});
6936
6937 cx.update_editor(|editor, window, cx| {
6938 editor.autoindent(&Default::default(), window, cx);
6939 });
6940
6941 cx.assert_editor_state(indoc! {"
6942 impl A {
6943
6944 fn b() {}
6945
6946 «fn c() {
6947
6948 }ˇ»
6949 }
6950 "});
6951 }
6952
6953 {
6954 let mut cx = EditorTestContext::new_multibuffer(
6955 cx,
6956 [indoc! { "
6957 impl A {
6958 «
6959 // a
6960 fn b(){}
6961 »
6962 «
6963 }
6964 fn c(){}
6965 »
6966 "}],
6967 );
6968
6969 let buffer = cx.update_editor(|editor, _, cx| {
6970 let buffer = editor.buffer().update(cx, |buffer, _| {
6971 buffer.all_buffers().iter().next().unwrap().clone()
6972 });
6973 buffer.update(cx, |buffer, cx| buffer.set_language(Some(rust_lang()), cx));
6974 buffer
6975 });
6976
6977 cx.run_until_parked();
6978 cx.update_editor(|editor, window, cx| {
6979 editor.select_all(&Default::default(), window, cx);
6980 editor.autoindent(&Default::default(), window, cx)
6981 });
6982 cx.run_until_parked();
6983
6984 cx.update(|_, cx| {
6985 assert_eq!(
6986 buffer.read(cx).text(),
6987 indoc! { "
6988 impl A {
6989
6990 // a
6991 fn b(){}
6992
6993
6994 }
6995 fn c(){}
6996
6997 " }
6998 )
6999 });
7000 }
7001}
7002
7003#[gpui::test]
7004async fn test_autoclose_and_auto_surround_pairs(cx: &mut TestAppContext) {
7005 init_test(cx, |_| {});
7006
7007 let mut cx = EditorTestContext::new(cx).await;
7008
7009 let language = Arc::new(Language::new(
7010 LanguageConfig {
7011 brackets: BracketPairConfig {
7012 pairs: vec![
7013 BracketPair {
7014 start: "{".to_string(),
7015 end: "}".to_string(),
7016 close: true,
7017 surround: true,
7018 newline: true,
7019 },
7020 BracketPair {
7021 start: "(".to_string(),
7022 end: ")".to_string(),
7023 close: true,
7024 surround: true,
7025 newline: true,
7026 },
7027 BracketPair {
7028 start: "/*".to_string(),
7029 end: " */".to_string(),
7030 close: true,
7031 surround: true,
7032 newline: true,
7033 },
7034 BracketPair {
7035 start: "[".to_string(),
7036 end: "]".to_string(),
7037 close: false,
7038 surround: false,
7039 newline: true,
7040 },
7041 BracketPair {
7042 start: "\"".to_string(),
7043 end: "\"".to_string(),
7044 close: true,
7045 surround: true,
7046 newline: false,
7047 },
7048 BracketPair {
7049 start: "<".to_string(),
7050 end: ">".to_string(),
7051 close: false,
7052 surround: true,
7053 newline: true,
7054 },
7055 ],
7056 ..Default::default()
7057 },
7058 autoclose_before: "})]".to_string(),
7059 ..Default::default()
7060 },
7061 Some(tree_sitter_rust::LANGUAGE.into()),
7062 ));
7063
7064 cx.language_registry().add(language.clone());
7065 cx.update_buffer(|buffer, cx| {
7066 buffer.set_language(Some(language), cx);
7067 });
7068
7069 cx.set_state(
7070 &r#"
7071 🏀ˇ
7072 εˇ
7073 ❤️ˇ
7074 "#
7075 .unindent(),
7076 );
7077
7078 // autoclose multiple nested brackets at multiple cursors
7079 cx.update_editor(|editor, window, cx| {
7080 editor.handle_input("{", window, cx);
7081 editor.handle_input("{", window, cx);
7082 editor.handle_input("{", window, cx);
7083 });
7084 cx.assert_editor_state(
7085 &"
7086 🏀{{{ˇ}}}
7087 ε{{{ˇ}}}
7088 ❤️{{{ˇ}}}
7089 "
7090 .unindent(),
7091 );
7092
7093 // insert a different closing bracket
7094 cx.update_editor(|editor, window, cx| {
7095 editor.handle_input(")", window, cx);
7096 });
7097 cx.assert_editor_state(
7098 &"
7099 🏀{{{)ˇ}}}
7100 ε{{{)ˇ}}}
7101 ❤️{{{)ˇ}}}
7102 "
7103 .unindent(),
7104 );
7105
7106 // skip over the auto-closed brackets when typing a closing bracket
7107 cx.update_editor(|editor, window, cx| {
7108 editor.move_right(&MoveRight, window, cx);
7109 editor.handle_input("}", window, cx);
7110 editor.handle_input("}", window, cx);
7111 editor.handle_input("}", window, cx);
7112 });
7113 cx.assert_editor_state(
7114 &"
7115 🏀{{{)}}}}ˇ
7116 ε{{{)}}}}ˇ
7117 ❤️{{{)}}}}ˇ
7118 "
7119 .unindent(),
7120 );
7121
7122 // autoclose multi-character pairs
7123 cx.set_state(
7124 &"
7125 ˇ
7126 ˇ
7127 "
7128 .unindent(),
7129 );
7130 cx.update_editor(|editor, window, cx| {
7131 editor.handle_input("/", window, cx);
7132 editor.handle_input("*", window, cx);
7133 });
7134 cx.assert_editor_state(
7135 &"
7136 /*ˇ */
7137 /*ˇ */
7138 "
7139 .unindent(),
7140 );
7141
7142 // one cursor autocloses a multi-character pair, one cursor
7143 // does not autoclose.
7144 cx.set_state(
7145 &"
7146 /ˇ
7147 ˇ
7148 "
7149 .unindent(),
7150 );
7151 cx.update_editor(|editor, window, cx| editor.handle_input("*", window, cx));
7152 cx.assert_editor_state(
7153 &"
7154 /*ˇ */
7155 *ˇ
7156 "
7157 .unindent(),
7158 );
7159
7160 // Don't autoclose if the next character isn't whitespace and isn't
7161 // listed in the language's "autoclose_before" section.
7162 cx.set_state("ˇa b");
7163 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7164 cx.assert_editor_state("{ˇa b");
7165
7166 // Don't autoclose if `close` is false for the bracket pair
7167 cx.set_state("ˇ");
7168 cx.update_editor(|editor, window, cx| editor.handle_input("[", window, cx));
7169 cx.assert_editor_state("[ˇ");
7170
7171 // Surround with brackets if text is selected
7172 cx.set_state("«aˇ» b");
7173 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7174 cx.assert_editor_state("{«aˇ»} b");
7175
7176 // Autoclose when not immediately after a word character
7177 cx.set_state("a ˇ");
7178 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7179 cx.assert_editor_state("a \"ˇ\"");
7180
7181 // Autoclose pair where the start and end characters are the same
7182 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7183 cx.assert_editor_state("a \"\"ˇ");
7184
7185 // Don't autoclose when immediately after a word character
7186 cx.set_state("aˇ");
7187 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7188 cx.assert_editor_state("a\"ˇ");
7189
7190 // Do autoclose when after a non-word character
7191 cx.set_state("{ˇ");
7192 cx.update_editor(|editor, window, cx| editor.handle_input("\"", window, cx));
7193 cx.assert_editor_state("{\"ˇ\"");
7194
7195 // Non identical pairs autoclose regardless of preceding character
7196 cx.set_state("aˇ");
7197 cx.update_editor(|editor, window, cx| editor.handle_input("{", window, cx));
7198 cx.assert_editor_state("a{ˇ}");
7199
7200 // Don't autoclose pair if autoclose is disabled
7201 cx.set_state("ˇ");
7202 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7203 cx.assert_editor_state("<ˇ");
7204
7205 // Surround with brackets if text is selected and auto_surround is enabled, even if autoclose is disabled
7206 cx.set_state("«aˇ» b");
7207 cx.update_editor(|editor, window, cx| editor.handle_input("<", window, cx));
7208 cx.assert_editor_state("<«aˇ»> b");
7209}
7210
7211#[gpui::test]
7212async fn test_always_treat_brackets_as_autoclosed_skip_over(cx: &mut TestAppContext) {
7213 init_test(cx, |settings| {
7214 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7215 });
7216
7217 let mut cx = EditorTestContext::new(cx).await;
7218
7219 let language = Arc::new(Language::new(
7220 LanguageConfig {
7221 brackets: BracketPairConfig {
7222 pairs: vec![
7223 BracketPair {
7224 start: "{".to_string(),
7225 end: "}".to_string(),
7226 close: true,
7227 surround: true,
7228 newline: true,
7229 },
7230 BracketPair {
7231 start: "(".to_string(),
7232 end: ")".to_string(),
7233 close: true,
7234 surround: true,
7235 newline: true,
7236 },
7237 BracketPair {
7238 start: "[".to_string(),
7239 end: "]".to_string(),
7240 close: false,
7241 surround: false,
7242 newline: true,
7243 },
7244 ],
7245 ..Default::default()
7246 },
7247 autoclose_before: "})]".to_string(),
7248 ..Default::default()
7249 },
7250 Some(tree_sitter_rust::LANGUAGE.into()),
7251 ));
7252
7253 cx.language_registry().add(language.clone());
7254 cx.update_buffer(|buffer, cx| {
7255 buffer.set_language(Some(language), cx);
7256 });
7257
7258 cx.set_state(
7259 &"
7260 ˇ
7261 ˇ
7262 ˇ
7263 "
7264 .unindent(),
7265 );
7266
7267 // ensure only matching closing brackets are skipped over
7268 cx.update_editor(|editor, window, cx| {
7269 editor.handle_input("}", window, cx);
7270 editor.move_left(&MoveLeft, window, cx);
7271 editor.handle_input(")", window, cx);
7272 editor.move_left(&MoveLeft, window, cx);
7273 });
7274 cx.assert_editor_state(
7275 &"
7276 ˇ)}
7277 ˇ)}
7278 ˇ)}
7279 "
7280 .unindent(),
7281 );
7282
7283 // skip-over closing brackets at multiple cursors
7284 cx.update_editor(|editor, window, cx| {
7285 editor.handle_input(")", window, cx);
7286 editor.handle_input("}", window, cx);
7287 });
7288 cx.assert_editor_state(
7289 &"
7290 )}ˇ
7291 )}ˇ
7292 )}ˇ
7293 "
7294 .unindent(),
7295 );
7296
7297 // ignore non-close brackets
7298 cx.update_editor(|editor, window, cx| {
7299 editor.handle_input("]", window, cx);
7300 editor.move_left(&MoveLeft, window, cx);
7301 editor.handle_input("]", window, cx);
7302 });
7303 cx.assert_editor_state(
7304 &"
7305 )}]ˇ]
7306 )}]ˇ]
7307 )}]ˇ]
7308 "
7309 .unindent(),
7310 );
7311}
7312
7313#[gpui::test]
7314async fn test_autoclose_with_embedded_language(cx: &mut TestAppContext) {
7315 init_test(cx, |_| {});
7316
7317 let mut cx = EditorTestContext::new(cx).await;
7318
7319 let html_language = Arc::new(
7320 Language::new(
7321 LanguageConfig {
7322 name: "HTML".into(),
7323 brackets: BracketPairConfig {
7324 pairs: vec![
7325 BracketPair {
7326 start: "<".into(),
7327 end: ">".into(),
7328 close: true,
7329 ..Default::default()
7330 },
7331 BracketPair {
7332 start: "{".into(),
7333 end: "}".into(),
7334 close: true,
7335 ..Default::default()
7336 },
7337 BracketPair {
7338 start: "(".into(),
7339 end: ")".into(),
7340 close: true,
7341 ..Default::default()
7342 },
7343 ],
7344 ..Default::default()
7345 },
7346 autoclose_before: "})]>".into(),
7347 ..Default::default()
7348 },
7349 Some(tree_sitter_html::LANGUAGE.into()),
7350 )
7351 .with_injection_query(
7352 r#"
7353 (script_element
7354 (raw_text) @injection.content
7355 (#set! injection.language "javascript"))
7356 "#,
7357 )
7358 .unwrap(),
7359 );
7360
7361 let javascript_language = Arc::new(Language::new(
7362 LanguageConfig {
7363 name: "JavaScript".into(),
7364 brackets: BracketPairConfig {
7365 pairs: vec![
7366 BracketPair {
7367 start: "/*".into(),
7368 end: " */".into(),
7369 close: true,
7370 ..Default::default()
7371 },
7372 BracketPair {
7373 start: "{".into(),
7374 end: "}".into(),
7375 close: true,
7376 ..Default::default()
7377 },
7378 BracketPair {
7379 start: "(".into(),
7380 end: ")".into(),
7381 close: true,
7382 ..Default::default()
7383 },
7384 ],
7385 ..Default::default()
7386 },
7387 autoclose_before: "})]>".into(),
7388 ..Default::default()
7389 },
7390 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
7391 ));
7392
7393 cx.language_registry().add(html_language.clone());
7394 cx.language_registry().add(javascript_language.clone());
7395
7396 cx.update_buffer(|buffer, cx| {
7397 buffer.set_language(Some(html_language), cx);
7398 });
7399
7400 cx.set_state(
7401 &r#"
7402 <body>ˇ
7403 <script>
7404 var x = 1;ˇ
7405 </script>
7406 </body>ˇ
7407 "#
7408 .unindent(),
7409 );
7410
7411 // Precondition: different languages are active at different locations.
7412 cx.update_editor(|editor, window, cx| {
7413 let snapshot = editor.snapshot(window, cx);
7414 let cursors = editor.selections.ranges::<usize>(cx);
7415 let languages = cursors
7416 .iter()
7417 .map(|c| snapshot.language_at(c.start).unwrap().name())
7418 .collect::<Vec<_>>();
7419 assert_eq!(
7420 languages,
7421 &["HTML".into(), "JavaScript".into(), "HTML".into()]
7422 );
7423 });
7424
7425 // Angle brackets autoclose in HTML, but not JavaScript.
7426 cx.update_editor(|editor, window, cx| {
7427 editor.handle_input("<", window, cx);
7428 editor.handle_input("a", window, cx);
7429 });
7430 cx.assert_editor_state(
7431 &r#"
7432 <body><aˇ>
7433 <script>
7434 var x = 1;<aˇ
7435 </script>
7436 </body><aˇ>
7437 "#
7438 .unindent(),
7439 );
7440
7441 // Curly braces and parens autoclose in both HTML and JavaScript.
7442 cx.update_editor(|editor, window, cx| {
7443 editor.handle_input(" b=", window, cx);
7444 editor.handle_input("{", window, cx);
7445 editor.handle_input("c", window, cx);
7446 editor.handle_input("(", window, cx);
7447 });
7448 cx.assert_editor_state(
7449 &r#"
7450 <body><a b={c(ˇ)}>
7451 <script>
7452 var x = 1;<a b={c(ˇ)}
7453 </script>
7454 </body><a b={c(ˇ)}>
7455 "#
7456 .unindent(),
7457 );
7458
7459 // Brackets that were already autoclosed are skipped.
7460 cx.update_editor(|editor, window, cx| {
7461 editor.handle_input(")", window, cx);
7462 editor.handle_input("d", window, cx);
7463 editor.handle_input("}", window, cx);
7464 });
7465 cx.assert_editor_state(
7466 &r#"
7467 <body><a b={c()d}ˇ>
7468 <script>
7469 var x = 1;<a b={c()d}ˇ
7470 </script>
7471 </body><a b={c()d}ˇ>
7472 "#
7473 .unindent(),
7474 );
7475 cx.update_editor(|editor, window, cx| {
7476 editor.handle_input(">", window, cx);
7477 });
7478 cx.assert_editor_state(
7479 &r#"
7480 <body><a b={c()d}>ˇ
7481 <script>
7482 var x = 1;<a b={c()d}>ˇ
7483 </script>
7484 </body><a b={c()d}>ˇ
7485 "#
7486 .unindent(),
7487 );
7488
7489 // Reset
7490 cx.set_state(
7491 &r#"
7492 <body>ˇ
7493 <script>
7494 var x = 1;ˇ
7495 </script>
7496 </body>ˇ
7497 "#
7498 .unindent(),
7499 );
7500
7501 cx.update_editor(|editor, window, cx| {
7502 editor.handle_input("<", window, cx);
7503 });
7504 cx.assert_editor_state(
7505 &r#"
7506 <body><ˇ>
7507 <script>
7508 var x = 1;<ˇ
7509 </script>
7510 </body><ˇ>
7511 "#
7512 .unindent(),
7513 );
7514
7515 // When backspacing, the closing angle brackets are removed.
7516 cx.update_editor(|editor, window, cx| {
7517 editor.backspace(&Backspace, window, cx);
7518 });
7519 cx.assert_editor_state(
7520 &r#"
7521 <body>ˇ
7522 <script>
7523 var x = 1;ˇ
7524 </script>
7525 </body>ˇ
7526 "#
7527 .unindent(),
7528 );
7529
7530 // Block comments autoclose in JavaScript, but not HTML.
7531 cx.update_editor(|editor, window, cx| {
7532 editor.handle_input("/", window, cx);
7533 editor.handle_input("*", window, cx);
7534 });
7535 cx.assert_editor_state(
7536 &r#"
7537 <body>/*ˇ
7538 <script>
7539 var x = 1;/*ˇ */
7540 </script>
7541 </body>/*ˇ
7542 "#
7543 .unindent(),
7544 );
7545}
7546
7547#[gpui::test]
7548async fn test_autoclose_with_overrides(cx: &mut TestAppContext) {
7549 init_test(cx, |_| {});
7550
7551 let mut cx = EditorTestContext::new(cx).await;
7552
7553 let rust_language = Arc::new(
7554 Language::new(
7555 LanguageConfig {
7556 name: "Rust".into(),
7557 brackets: serde_json::from_value(json!([
7558 { "start": "{", "end": "}", "close": true, "newline": true },
7559 { "start": "\"", "end": "\"", "close": true, "newline": false, "not_in": ["string"] },
7560 ]))
7561 .unwrap(),
7562 autoclose_before: "})]>".into(),
7563 ..Default::default()
7564 },
7565 Some(tree_sitter_rust::LANGUAGE.into()),
7566 )
7567 .with_override_query("(string_literal) @string")
7568 .unwrap(),
7569 );
7570
7571 cx.language_registry().add(rust_language.clone());
7572 cx.update_buffer(|buffer, cx| {
7573 buffer.set_language(Some(rust_language), cx);
7574 });
7575
7576 cx.set_state(
7577 &r#"
7578 let x = ˇ
7579 "#
7580 .unindent(),
7581 );
7582
7583 // Inserting a quotation mark. A closing quotation mark is automatically inserted.
7584 cx.update_editor(|editor, window, cx| {
7585 editor.handle_input("\"", window, cx);
7586 });
7587 cx.assert_editor_state(
7588 &r#"
7589 let x = "ˇ"
7590 "#
7591 .unindent(),
7592 );
7593
7594 // Inserting another quotation mark. The cursor moves across the existing
7595 // automatically-inserted quotation mark.
7596 cx.update_editor(|editor, window, cx| {
7597 editor.handle_input("\"", window, cx);
7598 });
7599 cx.assert_editor_state(
7600 &r#"
7601 let x = ""ˇ
7602 "#
7603 .unindent(),
7604 );
7605
7606 // Reset
7607 cx.set_state(
7608 &r#"
7609 let x = ˇ
7610 "#
7611 .unindent(),
7612 );
7613
7614 // Inserting a quotation mark inside of a string. A second quotation mark is not inserted.
7615 cx.update_editor(|editor, window, cx| {
7616 editor.handle_input("\"", window, cx);
7617 editor.handle_input(" ", window, cx);
7618 editor.move_left(&Default::default(), window, cx);
7619 editor.handle_input("\\", window, cx);
7620 editor.handle_input("\"", window, cx);
7621 });
7622 cx.assert_editor_state(
7623 &r#"
7624 let x = "\"ˇ "
7625 "#
7626 .unindent(),
7627 );
7628
7629 // Inserting a closing quotation mark at the position of an automatically-inserted quotation
7630 // mark. Nothing is inserted.
7631 cx.update_editor(|editor, window, cx| {
7632 editor.move_right(&Default::default(), window, cx);
7633 editor.handle_input("\"", window, cx);
7634 });
7635 cx.assert_editor_state(
7636 &r#"
7637 let x = "\" "ˇ
7638 "#
7639 .unindent(),
7640 );
7641}
7642
7643#[gpui::test]
7644async fn test_surround_with_pair(cx: &mut TestAppContext) {
7645 init_test(cx, |_| {});
7646
7647 let language = Arc::new(Language::new(
7648 LanguageConfig {
7649 brackets: BracketPairConfig {
7650 pairs: vec![
7651 BracketPair {
7652 start: "{".to_string(),
7653 end: "}".to_string(),
7654 close: true,
7655 surround: true,
7656 newline: true,
7657 },
7658 BracketPair {
7659 start: "/* ".to_string(),
7660 end: "*/".to_string(),
7661 close: true,
7662 surround: true,
7663 ..Default::default()
7664 },
7665 ],
7666 ..Default::default()
7667 },
7668 ..Default::default()
7669 },
7670 Some(tree_sitter_rust::LANGUAGE.into()),
7671 ));
7672
7673 let text = r#"
7674 a
7675 b
7676 c
7677 "#
7678 .unindent();
7679
7680 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7681 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7682 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7683 editor
7684 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7685 .await;
7686
7687 editor.update_in(cx, |editor, window, cx| {
7688 editor.change_selections(None, window, cx, |s| {
7689 s.select_display_ranges([
7690 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7691 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7692 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1),
7693 ])
7694 });
7695
7696 editor.handle_input("{", window, cx);
7697 editor.handle_input("{", window, cx);
7698 editor.handle_input("{", window, cx);
7699 assert_eq!(
7700 editor.text(cx),
7701 "
7702 {{{a}}}
7703 {{{b}}}
7704 {{{c}}}
7705 "
7706 .unindent()
7707 );
7708 assert_eq!(
7709 editor.selections.display_ranges(cx),
7710 [
7711 DisplayPoint::new(DisplayRow(0), 3)..DisplayPoint::new(DisplayRow(0), 4),
7712 DisplayPoint::new(DisplayRow(1), 3)..DisplayPoint::new(DisplayRow(1), 4),
7713 DisplayPoint::new(DisplayRow(2), 3)..DisplayPoint::new(DisplayRow(2), 4)
7714 ]
7715 );
7716
7717 editor.undo(&Undo, window, cx);
7718 editor.undo(&Undo, window, cx);
7719 editor.undo(&Undo, window, cx);
7720 assert_eq!(
7721 editor.text(cx),
7722 "
7723 a
7724 b
7725 c
7726 "
7727 .unindent()
7728 );
7729 assert_eq!(
7730 editor.selections.display_ranges(cx),
7731 [
7732 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7733 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7734 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7735 ]
7736 );
7737
7738 // Ensure inserting the first character of a multi-byte bracket pair
7739 // doesn't surround the selections with the bracket.
7740 editor.handle_input("/", window, cx);
7741 assert_eq!(
7742 editor.text(cx),
7743 "
7744 /
7745 /
7746 /
7747 "
7748 .unindent()
7749 );
7750 assert_eq!(
7751 editor.selections.display_ranges(cx),
7752 [
7753 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7754 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7755 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7756 ]
7757 );
7758
7759 editor.undo(&Undo, window, cx);
7760 assert_eq!(
7761 editor.text(cx),
7762 "
7763 a
7764 b
7765 c
7766 "
7767 .unindent()
7768 );
7769 assert_eq!(
7770 editor.selections.display_ranges(cx),
7771 [
7772 DisplayPoint::new(DisplayRow(0), 0)..DisplayPoint::new(DisplayRow(0), 1),
7773 DisplayPoint::new(DisplayRow(1), 0)..DisplayPoint::new(DisplayRow(1), 1),
7774 DisplayPoint::new(DisplayRow(2), 0)..DisplayPoint::new(DisplayRow(2), 1)
7775 ]
7776 );
7777
7778 // Ensure inserting the last character of a multi-byte bracket pair
7779 // doesn't surround the selections with the bracket.
7780 editor.handle_input("*", window, cx);
7781 assert_eq!(
7782 editor.text(cx),
7783 "
7784 *
7785 *
7786 *
7787 "
7788 .unindent()
7789 );
7790 assert_eq!(
7791 editor.selections.display_ranges(cx),
7792 [
7793 DisplayPoint::new(DisplayRow(0), 1)..DisplayPoint::new(DisplayRow(0), 1),
7794 DisplayPoint::new(DisplayRow(1), 1)..DisplayPoint::new(DisplayRow(1), 1),
7795 DisplayPoint::new(DisplayRow(2), 1)..DisplayPoint::new(DisplayRow(2), 1)
7796 ]
7797 );
7798 });
7799}
7800
7801#[gpui::test]
7802async fn test_delete_autoclose_pair(cx: &mut TestAppContext) {
7803 init_test(cx, |_| {});
7804
7805 let language = Arc::new(Language::new(
7806 LanguageConfig {
7807 brackets: BracketPairConfig {
7808 pairs: vec![BracketPair {
7809 start: "{".to_string(),
7810 end: "}".to_string(),
7811 close: true,
7812 surround: true,
7813 newline: true,
7814 }],
7815 ..Default::default()
7816 },
7817 autoclose_before: "}".to_string(),
7818 ..Default::default()
7819 },
7820 Some(tree_sitter_rust::LANGUAGE.into()),
7821 ));
7822
7823 let text = r#"
7824 a
7825 b
7826 c
7827 "#
7828 .unindent();
7829
7830 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
7831 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
7832 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
7833 editor
7834 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
7835 .await;
7836
7837 editor.update_in(cx, |editor, window, cx| {
7838 editor.change_selections(None, window, cx, |s| {
7839 s.select_ranges([
7840 Point::new(0, 1)..Point::new(0, 1),
7841 Point::new(1, 1)..Point::new(1, 1),
7842 Point::new(2, 1)..Point::new(2, 1),
7843 ])
7844 });
7845
7846 editor.handle_input("{", window, cx);
7847 editor.handle_input("{", window, cx);
7848 editor.handle_input("_", window, cx);
7849 assert_eq!(
7850 editor.text(cx),
7851 "
7852 a{{_}}
7853 b{{_}}
7854 c{{_}}
7855 "
7856 .unindent()
7857 );
7858 assert_eq!(
7859 editor.selections.ranges::<Point>(cx),
7860 [
7861 Point::new(0, 4)..Point::new(0, 4),
7862 Point::new(1, 4)..Point::new(1, 4),
7863 Point::new(2, 4)..Point::new(2, 4)
7864 ]
7865 );
7866
7867 editor.backspace(&Default::default(), window, cx);
7868 editor.backspace(&Default::default(), window, cx);
7869 assert_eq!(
7870 editor.text(cx),
7871 "
7872 a{}
7873 b{}
7874 c{}
7875 "
7876 .unindent()
7877 );
7878 assert_eq!(
7879 editor.selections.ranges::<Point>(cx),
7880 [
7881 Point::new(0, 2)..Point::new(0, 2),
7882 Point::new(1, 2)..Point::new(1, 2),
7883 Point::new(2, 2)..Point::new(2, 2)
7884 ]
7885 );
7886
7887 editor.delete_to_previous_word_start(&Default::default(), window, cx);
7888 assert_eq!(
7889 editor.text(cx),
7890 "
7891 a
7892 b
7893 c
7894 "
7895 .unindent()
7896 );
7897 assert_eq!(
7898 editor.selections.ranges::<Point>(cx),
7899 [
7900 Point::new(0, 1)..Point::new(0, 1),
7901 Point::new(1, 1)..Point::new(1, 1),
7902 Point::new(2, 1)..Point::new(2, 1)
7903 ]
7904 );
7905 });
7906}
7907
7908#[gpui::test]
7909async fn test_always_treat_brackets_as_autoclosed_delete(cx: &mut TestAppContext) {
7910 init_test(cx, |settings| {
7911 settings.defaults.always_treat_brackets_as_autoclosed = Some(true);
7912 });
7913
7914 let mut cx = EditorTestContext::new(cx).await;
7915
7916 let language = Arc::new(Language::new(
7917 LanguageConfig {
7918 brackets: BracketPairConfig {
7919 pairs: vec![
7920 BracketPair {
7921 start: "{".to_string(),
7922 end: "}".to_string(),
7923 close: true,
7924 surround: true,
7925 newline: true,
7926 },
7927 BracketPair {
7928 start: "(".to_string(),
7929 end: ")".to_string(),
7930 close: true,
7931 surround: true,
7932 newline: true,
7933 },
7934 BracketPair {
7935 start: "[".to_string(),
7936 end: "]".to_string(),
7937 close: false,
7938 surround: true,
7939 newline: true,
7940 },
7941 ],
7942 ..Default::default()
7943 },
7944 autoclose_before: "})]".to_string(),
7945 ..Default::default()
7946 },
7947 Some(tree_sitter_rust::LANGUAGE.into()),
7948 ));
7949
7950 cx.language_registry().add(language.clone());
7951 cx.update_buffer(|buffer, cx| {
7952 buffer.set_language(Some(language), cx);
7953 });
7954
7955 cx.set_state(
7956 &"
7957 {(ˇ)}
7958 [[ˇ]]
7959 {(ˇ)}
7960 "
7961 .unindent(),
7962 );
7963
7964 cx.update_editor(|editor, window, cx| {
7965 editor.backspace(&Default::default(), window, cx);
7966 editor.backspace(&Default::default(), window, cx);
7967 });
7968
7969 cx.assert_editor_state(
7970 &"
7971 ˇ
7972 ˇ]]
7973 ˇ
7974 "
7975 .unindent(),
7976 );
7977
7978 cx.update_editor(|editor, window, cx| {
7979 editor.handle_input("{", window, cx);
7980 editor.handle_input("{", window, cx);
7981 editor.move_right(&MoveRight, window, cx);
7982 editor.move_right(&MoveRight, window, cx);
7983 editor.move_left(&MoveLeft, window, cx);
7984 editor.move_left(&MoveLeft, window, cx);
7985 editor.backspace(&Default::default(), window, cx);
7986 });
7987
7988 cx.assert_editor_state(
7989 &"
7990 {ˇ}
7991 {ˇ}]]
7992 {ˇ}
7993 "
7994 .unindent(),
7995 );
7996
7997 cx.update_editor(|editor, window, cx| {
7998 editor.backspace(&Default::default(), window, cx);
7999 });
8000
8001 cx.assert_editor_state(
8002 &"
8003 ˇ
8004 ˇ]]
8005 ˇ
8006 "
8007 .unindent(),
8008 );
8009}
8010
8011#[gpui::test]
8012async fn test_auto_replace_emoji_shortcode(cx: &mut TestAppContext) {
8013 init_test(cx, |_| {});
8014
8015 let language = Arc::new(Language::new(
8016 LanguageConfig::default(),
8017 Some(tree_sitter_rust::LANGUAGE.into()),
8018 ));
8019
8020 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(language, cx));
8021 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8022 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8023 editor
8024 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
8025 .await;
8026
8027 editor.update_in(cx, |editor, window, cx| {
8028 editor.set_auto_replace_emoji_shortcode(true);
8029
8030 editor.handle_input("Hello ", window, cx);
8031 editor.handle_input(":wave", window, cx);
8032 assert_eq!(editor.text(cx), "Hello :wave".unindent());
8033
8034 editor.handle_input(":", window, cx);
8035 assert_eq!(editor.text(cx), "Hello 👋".unindent());
8036
8037 editor.handle_input(" :smile", window, cx);
8038 assert_eq!(editor.text(cx), "Hello 👋 :smile".unindent());
8039
8040 editor.handle_input(":", window, cx);
8041 assert_eq!(editor.text(cx), "Hello 👋 😄".unindent());
8042
8043 // Ensure shortcode gets replaced when it is part of a word that only consists of emojis
8044 editor.handle_input(":wave", window, cx);
8045 assert_eq!(editor.text(cx), "Hello 👋 😄:wave".unindent());
8046
8047 editor.handle_input(":", window, cx);
8048 assert_eq!(editor.text(cx), "Hello 👋 😄👋".unindent());
8049
8050 editor.handle_input(":1", window, cx);
8051 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1".unindent());
8052
8053 editor.handle_input(":", window, cx);
8054 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1:".unindent());
8055
8056 // Ensure shortcode does not get replaced when it is part of a word
8057 editor.handle_input(" Test:wave", window, cx);
8058 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave".unindent());
8059
8060 editor.handle_input(":", window, cx);
8061 assert_eq!(editor.text(cx), "Hello 👋 😄👋:1: Test:wave:".unindent());
8062
8063 editor.set_auto_replace_emoji_shortcode(false);
8064
8065 // Ensure shortcode does not get replaced when auto replace is off
8066 editor.handle_input(" :wave", window, cx);
8067 assert_eq!(
8068 editor.text(cx),
8069 "Hello 👋 😄👋:1: Test:wave: :wave".unindent()
8070 );
8071
8072 editor.handle_input(":", window, cx);
8073 assert_eq!(
8074 editor.text(cx),
8075 "Hello 👋 😄👋:1: Test:wave: :wave:".unindent()
8076 );
8077 });
8078}
8079
8080#[gpui::test]
8081async fn test_snippet_placeholder_choices(cx: &mut TestAppContext) {
8082 init_test(cx, |_| {});
8083
8084 let (text, insertion_ranges) = marked_text_ranges(
8085 indoc! {"
8086 ˇ
8087 "},
8088 false,
8089 );
8090
8091 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8092 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8093
8094 _ = editor.update_in(cx, |editor, window, cx| {
8095 let snippet = Snippet::parse("type ${1|,i32,u32|} = $2").unwrap();
8096
8097 editor
8098 .insert_snippet(&insertion_ranges, snippet, window, cx)
8099 .unwrap();
8100
8101 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8102 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8103 assert_eq!(editor.text(cx), expected_text);
8104 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8105 }
8106
8107 assert(
8108 editor,
8109 cx,
8110 indoc! {"
8111 type «» =•
8112 "},
8113 );
8114
8115 assert!(editor.context_menu_visible(), "There should be a matches");
8116 });
8117}
8118
8119#[gpui::test]
8120async fn test_snippets(cx: &mut TestAppContext) {
8121 init_test(cx, |_| {});
8122
8123 let (text, insertion_ranges) = marked_text_ranges(
8124 indoc! {"
8125 a.ˇ b
8126 a.ˇ b
8127 a.ˇ b
8128 "},
8129 false,
8130 );
8131
8132 let buffer = cx.update(|cx| MultiBuffer::build_simple(&text, cx));
8133 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
8134
8135 editor.update_in(cx, |editor, window, cx| {
8136 let snippet = Snippet::parse("f(${1:one}, ${2:two}, ${1:three})$0").unwrap();
8137
8138 editor
8139 .insert_snippet(&insertion_ranges, snippet, window, cx)
8140 .unwrap();
8141
8142 fn assert(editor: &mut Editor, cx: &mut Context<Editor>, marked_text: &str) {
8143 let (expected_text, selection_ranges) = marked_text_ranges(marked_text, false);
8144 assert_eq!(editor.text(cx), expected_text);
8145 assert_eq!(editor.selections.ranges::<usize>(cx), selection_ranges);
8146 }
8147
8148 assert(
8149 editor,
8150 cx,
8151 indoc! {"
8152 a.f(«one», two, «three») b
8153 a.f(«one», two, «three») b
8154 a.f(«one», two, «three») b
8155 "},
8156 );
8157
8158 // Can't move earlier than the first tab stop
8159 assert!(!editor.move_to_prev_snippet_tabstop(window, cx));
8160 assert(
8161 editor,
8162 cx,
8163 indoc! {"
8164 a.f(«one», two, «three») b
8165 a.f(«one», two, «three») b
8166 a.f(«one», two, «three») b
8167 "},
8168 );
8169
8170 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8171 assert(
8172 editor,
8173 cx,
8174 indoc! {"
8175 a.f(one, «two», three) b
8176 a.f(one, «two», three) b
8177 a.f(one, «two», three) b
8178 "},
8179 );
8180
8181 editor.move_to_prev_snippet_tabstop(window, cx);
8182 assert(
8183 editor,
8184 cx,
8185 indoc! {"
8186 a.f(«one», two, «three») b
8187 a.f(«one», two, «three») b
8188 a.f(«one», two, «three») b
8189 "},
8190 );
8191
8192 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8193 assert(
8194 editor,
8195 cx,
8196 indoc! {"
8197 a.f(one, «two», three) b
8198 a.f(one, «two», three) b
8199 a.f(one, «two», three) b
8200 "},
8201 );
8202 assert!(editor.move_to_next_snippet_tabstop(window, cx));
8203 assert(
8204 editor,
8205 cx,
8206 indoc! {"
8207 a.f(one, two, three)ˇ b
8208 a.f(one, two, three)ˇ b
8209 a.f(one, two, three)ˇ b
8210 "},
8211 );
8212
8213 // As soon as the last tab stop is reached, snippet state is gone
8214 editor.move_to_prev_snippet_tabstop(window, cx);
8215 assert(
8216 editor,
8217 cx,
8218 indoc! {"
8219 a.f(one, two, three)ˇ b
8220 a.f(one, two, three)ˇ b
8221 a.f(one, two, three)ˇ b
8222 "},
8223 );
8224 });
8225}
8226
8227#[gpui::test]
8228async fn test_document_format_during_save(cx: &mut TestAppContext) {
8229 init_test(cx, |_| {});
8230
8231 let fs = FakeFs::new(cx.executor());
8232 fs.insert_file(path!("/file.rs"), Default::default()).await;
8233
8234 let project = Project::test(fs, [path!("/file.rs").as_ref()], cx).await;
8235
8236 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8237 language_registry.add(rust_lang());
8238 let mut fake_servers = language_registry.register_fake_lsp(
8239 "Rust",
8240 FakeLspAdapter {
8241 capabilities: lsp::ServerCapabilities {
8242 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8243 ..Default::default()
8244 },
8245 ..Default::default()
8246 },
8247 );
8248
8249 let buffer = project
8250 .update(cx, |project, cx| {
8251 project.open_local_buffer(path!("/file.rs"), cx)
8252 })
8253 .await
8254 .unwrap();
8255
8256 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8257 let (editor, cx) = cx.add_window_view(|window, cx| {
8258 build_editor_with_project(project.clone(), buffer, window, cx)
8259 });
8260 editor.update_in(cx, |editor, window, cx| {
8261 editor.set_text("one\ntwo\nthree\n", window, cx)
8262 });
8263 assert!(cx.read(|cx| editor.is_dirty(cx)));
8264
8265 cx.executor().start_waiting();
8266 let fake_server = fake_servers.next().await.unwrap();
8267
8268 {
8269 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8270 move |params, _| async move {
8271 assert_eq!(
8272 params.text_document.uri,
8273 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8274 );
8275 assert_eq!(params.options.tab_size, 4);
8276 Ok(Some(vec![lsp::TextEdit::new(
8277 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8278 ", ".to_string(),
8279 )]))
8280 },
8281 );
8282 let save = editor
8283 .update_in(cx, |editor, window, cx| {
8284 editor.save(true, project.clone(), window, cx)
8285 })
8286 .unwrap();
8287 cx.executor().start_waiting();
8288 save.await;
8289
8290 assert_eq!(
8291 editor.update(cx, |editor, cx| editor.text(cx)),
8292 "one, two\nthree\n"
8293 );
8294 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8295 }
8296
8297 {
8298 editor.update_in(cx, |editor, window, cx| {
8299 editor.set_text("one\ntwo\nthree\n", window, cx)
8300 });
8301 assert!(cx.read(|cx| editor.is_dirty(cx)));
8302
8303 // Ensure we can still save even if formatting hangs.
8304 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8305 move |params, _| async move {
8306 assert_eq!(
8307 params.text_document.uri,
8308 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8309 );
8310 futures::future::pending::<()>().await;
8311 unreachable!()
8312 },
8313 );
8314 let save = editor
8315 .update_in(cx, |editor, window, cx| {
8316 editor.save(true, project.clone(), window, cx)
8317 })
8318 .unwrap();
8319 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8320 cx.executor().start_waiting();
8321 save.await;
8322 assert_eq!(
8323 editor.update(cx, |editor, cx| editor.text(cx)),
8324 "one\ntwo\nthree\n"
8325 );
8326 }
8327
8328 // For non-dirty buffer, no formatting request should be sent
8329 {
8330 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8331
8332 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(move |_, _| async move {
8333 panic!("Should not be invoked on non-dirty buffer");
8334 });
8335 let save = editor
8336 .update_in(cx, |editor, window, cx| {
8337 editor.save(true, project.clone(), window, cx)
8338 })
8339 .unwrap();
8340 cx.executor().start_waiting();
8341 save.await;
8342 }
8343
8344 // Set rust language override and assert overridden tabsize is sent to language server
8345 update_test_language_settings(cx, |settings| {
8346 settings.languages.insert(
8347 "Rust".into(),
8348 LanguageSettingsContent {
8349 tab_size: NonZeroU32::new(8),
8350 ..Default::default()
8351 },
8352 );
8353 });
8354
8355 {
8356 editor.update_in(cx, |editor, window, cx| {
8357 editor.set_text("somehting_new\n", window, cx)
8358 });
8359 assert!(cx.read(|cx| editor.is_dirty(cx)));
8360 let _formatting_request_signal = fake_server
8361 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8362 assert_eq!(
8363 params.text_document.uri,
8364 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8365 );
8366 assert_eq!(params.options.tab_size, 8);
8367 Ok(Some(vec![]))
8368 });
8369 let save = editor
8370 .update_in(cx, |editor, window, cx| {
8371 editor.save(true, project.clone(), window, cx)
8372 })
8373 .unwrap();
8374 cx.executor().start_waiting();
8375 save.await;
8376 }
8377}
8378
8379#[gpui::test]
8380async fn test_multibuffer_format_during_save(cx: &mut TestAppContext) {
8381 init_test(cx, |_| {});
8382
8383 let cols = 4;
8384 let rows = 10;
8385 let sample_text_1 = sample_text(rows, cols, 'a');
8386 assert_eq!(
8387 sample_text_1,
8388 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
8389 );
8390 let sample_text_2 = sample_text(rows, cols, 'l');
8391 assert_eq!(
8392 sample_text_2,
8393 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
8394 );
8395 let sample_text_3 = sample_text(rows, cols, 'v');
8396 assert_eq!(
8397 sample_text_3,
8398 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
8399 );
8400
8401 let fs = FakeFs::new(cx.executor());
8402 fs.insert_tree(
8403 path!("/a"),
8404 json!({
8405 "main.rs": sample_text_1,
8406 "other.rs": sample_text_2,
8407 "lib.rs": sample_text_3,
8408 }),
8409 )
8410 .await;
8411
8412 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
8413 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
8414 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
8415
8416 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8417 language_registry.add(rust_lang());
8418 let mut fake_servers = language_registry.register_fake_lsp(
8419 "Rust",
8420 FakeLspAdapter {
8421 capabilities: lsp::ServerCapabilities {
8422 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8423 ..Default::default()
8424 },
8425 ..Default::default()
8426 },
8427 );
8428
8429 let worktree = project.update(cx, |project, cx| {
8430 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
8431 assert_eq!(worktrees.len(), 1);
8432 worktrees.pop().unwrap()
8433 });
8434 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
8435
8436 let buffer_1 = project
8437 .update(cx, |project, cx| {
8438 project.open_buffer((worktree_id, "main.rs"), cx)
8439 })
8440 .await
8441 .unwrap();
8442 let buffer_2 = project
8443 .update(cx, |project, cx| {
8444 project.open_buffer((worktree_id, "other.rs"), cx)
8445 })
8446 .await
8447 .unwrap();
8448 let buffer_3 = project
8449 .update(cx, |project, cx| {
8450 project.open_buffer((worktree_id, "lib.rs"), cx)
8451 })
8452 .await
8453 .unwrap();
8454
8455 let multi_buffer = cx.new(|cx| {
8456 let mut multi_buffer = MultiBuffer::new(ReadWrite);
8457 multi_buffer.push_excerpts(
8458 buffer_1.clone(),
8459 [
8460 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8461 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8462 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8463 ],
8464 cx,
8465 );
8466 multi_buffer.push_excerpts(
8467 buffer_2.clone(),
8468 [
8469 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8470 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8471 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8472 ],
8473 cx,
8474 );
8475 multi_buffer.push_excerpts(
8476 buffer_3.clone(),
8477 [
8478 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
8479 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
8480 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
8481 ],
8482 cx,
8483 );
8484 multi_buffer
8485 });
8486 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
8487 Editor::new(
8488 EditorMode::full(),
8489 multi_buffer,
8490 Some(project.clone()),
8491 window,
8492 cx,
8493 )
8494 });
8495
8496 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8497 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8498 s.select_ranges(Some(1..2))
8499 });
8500 editor.insert("|one|two|three|", window, cx);
8501 });
8502 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8503 multi_buffer_editor.update_in(cx, |editor, window, cx| {
8504 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
8505 s.select_ranges(Some(60..70))
8506 });
8507 editor.insert("|four|five|six|", window, cx);
8508 });
8509 assert!(cx.read(|cx| multi_buffer_editor.is_dirty(cx)));
8510
8511 // First two buffers should be edited, but not the third one.
8512 assert_eq!(
8513 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8514 "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}",
8515 );
8516 buffer_1.update(cx, |buffer, _| {
8517 assert!(buffer.is_dirty());
8518 assert_eq!(
8519 buffer.text(),
8520 "a|one|two|three|aa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj",
8521 )
8522 });
8523 buffer_2.update(cx, |buffer, _| {
8524 assert!(buffer.is_dirty());
8525 assert_eq!(
8526 buffer.text(),
8527 "llll\nmmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu",
8528 )
8529 });
8530 buffer_3.update(cx, |buffer, _| {
8531 assert!(!buffer.is_dirty());
8532 assert_eq!(buffer.text(), sample_text_3,)
8533 });
8534 cx.executor().run_until_parked();
8535
8536 cx.executor().start_waiting();
8537 let save = multi_buffer_editor
8538 .update_in(cx, |editor, window, cx| {
8539 editor.save(true, project.clone(), window, cx)
8540 })
8541 .unwrap();
8542
8543 let fake_server = fake_servers.next().await.unwrap();
8544 fake_server
8545 .server
8546 .on_request::<lsp::request::Formatting, _, _>(move |params, _| async move {
8547 Ok(Some(vec![lsp::TextEdit::new(
8548 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8549 format!("[{} formatted]", params.text_document.uri),
8550 )]))
8551 })
8552 .detach();
8553 save.await;
8554
8555 // After multibuffer saving, only first two buffers should be reformatted, but not the third one (as it was not dirty).
8556 assert!(cx.read(|cx| !multi_buffer_editor.is_dirty(cx)));
8557 assert_eq!(
8558 multi_buffer_editor.update(cx, |editor, cx| editor.text(cx)),
8559 uri!(
8560 "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}"
8561 ),
8562 );
8563 buffer_1.update(cx, |buffer, _| {
8564 assert!(!buffer.is_dirty());
8565 assert_eq!(
8566 buffer.text(),
8567 uri!("a|o[file:///a/main.rs formatted]bbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj\n"),
8568 )
8569 });
8570 buffer_2.update(cx, |buffer, _| {
8571 assert!(!buffer.is_dirty());
8572 assert_eq!(
8573 buffer.text(),
8574 uri!("lll[file:///a/other.rs formatted]mmmm\nnnnn|four|five|six|oooo\npppp\nr\nssss\ntttt\nuuuu\n"),
8575 )
8576 });
8577 buffer_3.update(cx, |buffer, _| {
8578 assert!(!buffer.is_dirty());
8579 assert_eq!(buffer.text(), sample_text_3,)
8580 });
8581}
8582
8583#[gpui::test]
8584async fn test_range_format_during_save(cx: &mut TestAppContext) {
8585 init_test(cx, |_| {});
8586
8587 let fs = FakeFs::new(cx.executor());
8588 fs.insert_file(path!("/file.rs"), Default::default()).await;
8589
8590 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8591
8592 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8593 language_registry.add(rust_lang());
8594 let mut fake_servers = language_registry.register_fake_lsp(
8595 "Rust",
8596 FakeLspAdapter {
8597 capabilities: lsp::ServerCapabilities {
8598 document_range_formatting_provider: Some(lsp::OneOf::Left(true)),
8599 ..Default::default()
8600 },
8601 ..Default::default()
8602 },
8603 );
8604
8605 let buffer = project
8606 .update(cx, |project, cx| {
8607 project.open_local_buffer(path!("/file.rs"), cx)
8608 })
8609 .await
8610 .unwrap();
8611
8612 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8613 let (editor, cx) = cx.add_window_view(|window, cx| {
8614 build_editor_with_project(project.clone(), buffer, window, cx)
8615 });
8616 editor.update_in(cx, |editor, window, cx| {
8617 editor.set_text("one\ntwo\nthree\n", window, cx)
8618 });
8619 assert!(cx.read(|cx| editor.is_dirty(cx)));
8620
8621 cx.executor().start_waiting();
8622 let fake_server = fake_servers.next().await.unwrap();
8623
8624 let save = editor
8625 .update_in(cx, |editor, window, cx| {
8626 editor.save(true, project.clone(), window, cx)
8627 })
8628 .unwrap();
8629 fake_server
8630 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8631 assert_eq!(
8632 params.text_document.uri,
8633 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8634 );
8635 assert_eq!(params.options.tab_size, 4);
8636 Ok(Some(vec![lsp::TextEdit::new(
8637 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8638 ", ".to_string(),
8639 )]))
8640 })
8641 .next()
8642 .await;
8643 cx.executor().start_waiting();
8644 save.await;
8645 assert_eq!(
8646 editor.update(cx, |editor, cx| editor.text(cx)),
8647 "one, two\nthree\n"
8648 );
8649 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8650
8651 editor.update_in(cx, |editor, window, cx| {
8652 editor.set_text("one\ntwo\nthree\n", window, cx)
8653 });
8654 assert!(cx.read(|cx| editor.is_dirty(cx)));
8655
8656 // Ensure we can still save even if formatting hangs.
8657 fake_server.set_request_handler::<lsp::request::RangeFormatting, _, _>(
8658 move |params, _| async move {
8659 assert_eq!(
8660 params.text_document.uri,
8661 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8662 );
8663 futures::future::pending::<()>().await;
8664 unreachable!()
8665 },
8666 );
8667 let save = editor
8668 .update_in(cx, |editor, window, cx| {
8669 editor.save(true, project.clone(), window, cx)
8670 })
8671 .unwrap();
8672 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8673 cx.executor().start_waiting();
8674 save.await;
8675 assert_eq!(
8676 editor.update(cx, |editor, cx| editor.text(cx)),
8677 "one\ntwo\nthree\n"
8678 );
8679 assert!(!cx.read(|cx| editor.is_dirty(cx)));
8680
8681 // For non-dirty buffer, no formatting request should be sent
8682 let save = editor
8683 .update_in(cx, |editor, window, cx| {
8684 editor.save(true, project.clone(), window, cx)
8685 })
8686 .unwrap();
8687 let _pending_format_request = fake_server
8688 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |_, _| async move {
8689 panic!("Should not be invoked on non-dirty buffer");
8690 })
8691 .next();
8692 cx.executor().start_waiting();
8693 save.await;
8694
8695 // Set Rust language override and assert overridden tabsize is sent to language server
8696 update_test_language_settings(cx, |settings| {
8697 settings.languages.insert(
8698 "Rust".into(),
8699 LanguageSettingsContent {
8700 tab_size: NonZeroU32::new(8),
8701 ..Default::default()
8702 },
8703 );
8704 });
8705
8706 editor.update_in(cx, |editor, window, cx| {
8707 editor.set_text("somehting_new\n", window, cx)
8708 });
8709 assert!(cx.read(|cx| editor.is_dirty(cx)));
8710 let save = editor
8711 .update_in(cx, |editor, window, cx| {
8712 editor.save(true, project.clone(), window, cx)
8713 })
8714 .unwrap();
8715 fake_server
8716 .set_request_handler::<lsp::request::RangeFormatting, _, _>(move |params, _| async move {
8717 assert_eq!(
8718 params.text_document.uri,
8719 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8720 );
8721 assert_eq!(params.options.tab_size, 8);
8722 Ok(Some(vec![]))
8723 })
8724 .next()
8725 .await;
8726 cx.executor().start_waiting();
8727 save.await;
8728}
8729
8730#[gpui::test]
8731async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
8732 init_test(cx, |settings| {
8733 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
8734 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
8735 ))
8736 });
8737
8738 let fs = FakeFs::new(cx.executor());
8739 fs.insert_file(path!("/file.rs"), Default::default()).await;
8740
8741 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8742
8743 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8744 language_registry.add(Arc::new(Language::new(
8745 LanguageConfig {
8746 name: "Rust".into(),
8747 matcher: LanguageMatcher {
8748 path_suffixes: vec!["rs".to_string()],
8749 ..Default::default()
8750 },
8751 ..LanguageConfig::default()
8752 },
8753 Some(tree_sitter_rust::LANGUAGE.into()),
8754 )));
8755 update_test_language_settings(cx, |settings| {
8756 // Enable Prettier formatting for the same buffer, and ensure
8757 // LSP is called instead of Prettier.
8758 settings.defaults.prettier = Some(PrettierSettings {
8759 allowed: true,
8760 ..PrettierSettings::default()
8761 });
8762 });
8763 let mut fake_servers = language_registry.register_fake_lsp(
8764 "Rust",
8765 FakeLspAdapter {
8766 capabilities: lsp::ServerCapabilities {
8767 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8768 ..Default::default()
8769 },
8770 ..Default::default()
8771 },
8772 );
8773
8774 let buffer = project
8775 .update(cx, |project, cx| {
8776 project.open_local_buffer(path!("/file.rs"), cx)
8777 })
8778 .await
8779 .unwrap();
8780
8781 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8782 let (editor, cx) = cx.add_window_view(|window, cx| {
8783 build_editor_with_project(project.clone(), buffer, window, cx)
8784 });
8785 editor.update_in(cx, |editor, window, cx| {
8786 editor.set_text("one\ntwo\nthree\n", window, cx)
8787 });
8788
8789 cx.executor().start_waiting();
8790 let fake_server = fake_servers.next().await.unwrap();
8791
8792 let format = editor
8793 .update_in(cx, |editor, window, cx| {
8794 editor.perform_format(
8795 project.clone(),
8796 FormatTrigger::Manual,
8797 FormatTarget::Buffers,
8798 window,
8799 cx,
8800 )
8801 })
8802 .unwrap();
8803 fake_server
8804 .set_request_handler::<lsp::request::Formatting, _, _>(move |params, _| async move {
8805 assert_eq!(
8806 params.text_document.uri,
8807 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8808 );
8809 assert_eq!(params.options.tab_size, 4);
8810 Ok(Some(vec![lsp::TextEdit::new(
8811 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(1, 0)),
8812 ", ".to_string(),
8813 )]))
8814 })
8815 .next()
8816 .await;
8817 cx.executor().start_waiting();
8818 format.await;
8819 assert_eq!(
8820 editor.update(cx, |editor, cx| editor.text(cx)),
8821 "one, two\nthree\n"
8822 );
8823
8824 editor.update_in(cx, |editor, window, cx| {
8825 editor.set_text("one\ntwo\nthree\n", window, cx)
8826 });
8827 // Ensure we don't lock if formatting hangs.
8828 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8829 move |params, _| async move {
8830 assert_eq!(
8831 params.text_document.uri,
8832 lsp::Url::from_file_path(path!("/file.rs")).unwrap()
8833 );
8834 futures::future::pending::<()>().await;
8835 unreachable!()
8836 },
8837 );
8838 let format = editor
8839 .update_in(cx, |editor, window, cx| {
8840 editor.perform_format(
8841 project,
8842 FormatTrigger::Manual,
8843 FormatTarget::Buffers,
8844 window,
8845 cx,
8846 )
8847 })
8848 .unwrap();
8849 cx.executor().advance_clock(super::FORMAT_TIMEOUT);
8850 cx.executor().start_waiting();
8851 format.await;
8852 assert_eq!(
8853 editor.update(cx, |editor, cx| editor.text(cx)),
8854 "one\ntwo\nthree\n"
8855 );
8856}
8857
8858#[gpui::test]
8859async fn test_multiple_formatters(cx: &mut TestAppContext) {
8860 init_test(cx, |settings| {
8861 settings.defaults.remove_trailing_whitespace_on_save = Some(true);
8862 settings.defaults.formatter =
8863 Some(language_settings::SelectedFormatter::List(FormatterList(
8864 vec![
8865 Formatter::LanguageServer { name: None },
8866 Formatter::CodeActions(
8867 [
8868 ("code-action-1".into(), true),
8869 ("code-action-2".into(), true),
8870 ]
8871 .into_iter()
8872 .collect(),
8873 ),
8874 ]
8875 .into(),
8876 )))
8877 });
8878
8879 let fs = FakeFs::new(cx.executor());
8880 fs.insert_file(path!("/file.rs"), "one \ntwo \nthree".into())
8881 .await;
8882
8883 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
8884 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
8885 language_registry.add(rust_lang());
8886
8887 let mut fake_servers = language_registry.register_fake_lsp(
8888 "Rust",
8889 FakeLspAdapter {
8890 capabilities: lsp::ServerCapabilities {
8891 document_formatting_provider: Some(lsp::OneOf::Left(true)),
8892 execute_command_provider: Some(lsp::ExecuteCommandOptions {
8893 commands: vec!["the-command-for-code-action-1".into()],
8894 ..Default::default()
8895 }),
8896 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
8897 ..Default::default()
8898 },
8899 ..Default::default()
8900 },
8901 );
8902
8903 let buffer = project
8904 .update(cx, |project, cx| {
8905 project.open_local_buffer(path!("/file.rs"), cx)
8906 })
8907 .await
8908 .unwrap();
8909
8910 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
8911 let (editor, cx) = cx.add_window_view(|window, cx| {
8912 build_editor_with_project(project.clone(), buffer, window, cx)
8913 });
8914
8915 cx.executor().start_waiting();
8916
8917 let fake_server = fake_servers.next().await.unwrap();
8918 fake_server.set_request_handler::<lsp::request::Formatting, _, _>(
8919 move |_params, _| async move {
8920 Ok(Some(vec![lsp::TextEdit::new(
8921 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8922 "applied-formatting\n".to_string(),
8923 )]))
8924 },
8925 );
8926 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
8927 move |params, _| async move {
8928 assert_eq!(
8929 params.context.only,
8930 Some(vec!["code-action-1".into(), "code-action-2".into()])
8931 );
8932 let uri = lsp::Url::from_file_path(path!("/file.rs")).unwrap();
8933 Ok(Some(vec![
8934 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8935 kind: Some("code-action-1".into()),
8936 edit: Some(lsp::WorkspaceEdit::new(
8937 [(
8938 uri.clone(),
8939 vec![lsp::TextEdit::new(
8940 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8941 "applied-code-action-1-edit\n".to_string(),
8942 )],
8943 )]
8944 .into_iter()
8945 .collect(),
8946 )),
8947 command: Some(lsp::Command {
8948 command: "the-command-for-code-action-1".into(),
8949 ..Default::default()
8950 }),
8951 ..Default::default()
8952 }),
8953 lsp::CodeActionOrCommand::CodeAction(lsp::CodeAction {
8954 kind: Some("code-action-2".into()),
8955 edit: Some(lsp::WorkspaceEdit::new(
8956 [(
8957 uri.clone(),
8958 vec![lsp::TextEdit::new(
8959 lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
8960 "applied-code-action-2-edit\n".to_string(),
8961 )],
8962 )]
8963 .into_iter()
8964 .collect(),
8965 )),
8966 ..Default::default()
8967 }),
8968 ]))
8969 },
8970 );
8971
8972 fake_server.set_request_handler::<lsp::request::CodeActionResolveRequest, _, _>({
8973 move |params, _| async move { Ok(params) }
8974 });
8975
8976 let command_lock = Arc::new(futures::lock::Mutex::new(()));
8977 fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
8978 let fake = fake_server.clone();
8979 let lock = command_lock.clone();
8980 move |params, _| {
8981 assert_eq!(params.command, "the-command-for-code-action-1");
8982 let fake = fake.clone();
8983 let lock = lock.clone();
8984 async move {
8985 lock.lock().await;
8986 fake.server
8987 .request::<lsp::request::ApplyWorkspaceEdit>(lsp::ApplyWorkspaceEditParams {
8988 label: None,
8989 edit: lsp::WorkspaceEdit {
8990 changes: Some(
8991 [(
8992 lsp::Url::from_file_path(path!("/file.rs")).unwrap(),
8993 vec![lsp::TextEdit {
8994 range: lsp::Range::new(
8995 lsp::Position::new(0, 0),
8996 lsp::Position::new(0, 0),
8997 ),
8998 new_text: "applied-code-action-1-command\n".into(),
8999 }],
9000 )]
9001 .into_iter()
9002 .collect(),
9003 ),
9004 ..Default::default()
9005 },
9006 })
9007 .await
9008 .into_response()
9009 .unwrap();
9010 Ok(Some(json!(null)))
9011 }
9012 }
9013 });
9014
9015 cx.executor().start_waiting();
9016 editor
9017 .update_in(cx, |editor, window, cx| {
9018 editor.perform_format(
9019 project.clone(),
9020 FormatTrigger::Manual,
9021 FormatTarget::Buffers,
9022 window,
9023 cx,
9024 )
9025 })
9026 .unwrap()
9027 .await;
9028 editor.update(cx, |editor, cx| {
9029 assert_eq!(
9030 editor.text(cx),
9031 r#"
9032 applied-code-action-2-edit
9033 applied-code-action-1-command
9034 applied-code-action-1-edit
9035 applied-formatting
9036 one
9037 two
9038 three
9039 "#
9040 .unindent()
9041 );
9042 });
9043
9044 editor.update_in(cx, |editor, window, cx| {
9045 editor.undo(&Default::default(), window, cx);
9046 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9047 });
9048
9049 // Perform a manual edit while waiting for an LSP command
9050 // that's being run as part of a formatting code action.
9051 let lock_guard = command_lock.lock().await;
9052 let format = editor
9053 .update_in(cx, |editor, window, cx| {
9054 editor.perform_format(
9055 project.clone(),
9056 FormatTrigger::Manual,
9057 FormatTarget::Buffers,
9058 window,
9059 cx,
9060 )
9061 })
9062 .unwrap();
9063 cx.run_until_parked();
9064 editor.update(cx, |editor, cx| {
9065 assert_eq!(
9066 editor.text(cx),
9067 r#"
9068 applied-code-action-1-edit
9069 applied-formatting
9070 one
9071 two
9072 three
9073 "#
9074 .unindent()
9075 );
9076
9077 editor.buffer.update(cx, |buffer, cx| {
9078 let ix = buffer.len(cx);
9079 buffer.edit([(ix..ix, "edited\n")], None, cx);
9080 });
9081 });
9082
9083 // Allow the LSP command to proceed. Because the buffer was edited,
9084 // the second code action will not be run.
9085 drop(lock_guard);
9086 format.await;
9087 editor.update_in(cx, |editor, window, cx| {
9088 assert_eq!(
9089 editor.text(cx),
9090 r#"
9091 applied-code-action-1-command
9092 applied-code-action-1-edit
9093 applied-formatting
9094 one
9095 two
9096 three
9097 edited
9098 "#
9099 .unindent()
9100 );
9101
9102 // The manual edit is undone first, because it is the last thing the user did
9103 // (even though the command completed afterwards).
9104 editor.undo(&Default::default(), window, cx);
9105 assert_eq!(
9106 editor.text(cx),
9107 r#"
9108 applied-code-action-1-command
9109 applied-code-action-1-edit
9110 applied-formatting
9111 one
9112 two
9113 three
9114 "#
9115 .unindent()
9116 );
9117
9118 // All the formatting (including the command, which completed after the manual edit)
9119 // is undone together.
9120 editor.undo(&Default::default(), window, cx);
9121 assert_eq!(editor.text(cx), "one \ntwo \nthree");
9122 });
9123}
9124
9125#[gpui::test]
9126async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
9127 init_test(cx, |settings| {
9128 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
9129 FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
9130 ))
9131 });
9132
9133 let fs = FakeFs::new(cx.executor());
9134 fs.insert_file(path!("/file.ts"), Default::default()).await;
9135
9136 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
9137
9138 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
9139 language_registry.add(Arc::new(Language::new(
9140 LanguageConfig {
9141 name: "TypeScript".into(),
9142 matcher: LanguageMatcher {
9143 path_suffixes: vec!["ts".to_string()],
9144 ..Default::default()
9145 },
9146 ..LanguageConfig::default()
9147 },
9148 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
9149 )));
9150 update_test_language_settings(cx, |settings| {
9151 settings.defaults.prettier = Some(PrettierSettings {
9152 allowed: true,
9153 ..PrettierSettings::default()
9154 });
9155 });
9156 let mut fake_servers = language_registry.register_fake_lsp(
9157 "TypeScript",
9158 FakeLspAdapter {
9159 capabilities: lsp::ServerCapabilities {
9160 code_action_provider: Some(lsp::CodeActionProviderCapability::Simple(true)),
9161 ..Default::default()
9162 },
9163 ..Default::default()
9164 },
9165 );
9166
9167 let buffer = project
9168 .update(cx, |project, cx| {
9169 project.open_local_buffer(path!("/file.ts"), cx)
9170 })
9171 .await
9172 .unwrap();
9173
9174 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
9175 let (editor, cx) = cx.add_window_view(|window, cx| {
9176 build_editor_with_project(project.clone(), buffer, window, cx)
9177 });
9178 editor.update_in(cx, |editor, window, cx| {
9179 editor.set_text(
9180 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9181 window,
9182 cx,
9183 )
9184 });
9185
9186 cx.executor().start_waiting();
9187 let fake_server = fake_servers.next().await.unwrap();
9188
9189 let format = editor
9190 .update_in(cx, |editor, window, cx| {
9191 editor.perform_code_action_kind(
9192 project.clone(),
9193 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9194 window,
9195 cx,
9196 )
9197 })
9198 .unwrap();
9199 fake_server
9200 .set_request_handler::<lsp::request::CodeActionRequest, _, _>(move |params, _| async move {
9201 assert_eq!(
9202 params.text_document.uri,
9203 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9204 );
9205 Ok(Some(vec![lsp::CodeActionOrCommand::CodeAction(
9206 lsp::CodeAction {
9207 title: "Organize Imports".to_string(),
9208 kind: Some(lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS),
9209 edit: Some(lsp::WorkspaceEdit {
9210 changes: Some(
9211 [(
9212 params.text_document.uri.clone(),
9213 vec![lsp::TextEdit::new(
9214 lsp::Range::new(
9215 lsp::Position::new(1, 0),
9216 lsp::Position::new(2, 0),
9217 ),
9218 "".to_string(),
9219 )],
9220 )]
9221 .into_iter()
9222 .collect(),
9223 ),
9224 ..Default::default()
9225 }),
9226 ..Default::default()
9227 },
9228 )]))
9229 })
9230 .next()
9231 .await;
9232 cx.executor().start_waiting();
9233 format.await;
9234 assert_eq!(
9235 editor.update(cx, |editor, cx| editor.text(cx)),
9236 "import { a } from 'module';\n\nconst x = a;\n"
9237 );
9238
9239 editor.update_in(cx, |editor, window, cx| {
9240 editor.set_text(
9241 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n",
9242 window,
9243 cx,
9244 )
9245 });
9246 // Ensure we don't lock if code action hangs.
9247 fake_server.set_request_handler::<lsp::request::CodeActionRequest, _, _>(
9248 move |params, _| async move {
9249 assert_eq!(
9250 params.text_document.uri,
9251 lsp::Url::from_file_path(path!("/file.ts")).unwrap()
9252 );
9253 futures::future::pending::<()>().await;
9254 unreachable!()
9255 },
9256 );
9257 let format = editor
9258 .update_in(cx, |editor, window, cx| {
9259 editor.perform_code_action_kind(
9260 project,
9261 CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
9262 window,
9263 cx,
9264 )
9265 })
9266 .unwrap();
9267 cx.executor().advance_clock(super::CODE_ACTION_TIMEOUT);
9268 cx.executor().start_waiting();
9269 format.await;
9270 assert_eq!(
9271 editor.update(cx, |editor, cx| editor.text(cx)),
9272 "import { a } from 'module';\nimport { b } from 'module';\n\nconst x = a;\n"
9273 );
9274}
9275
9276#[gpui::test]
9277async fn test_concurrent_format_requests(cx: &mut TestAppContext) {
9278 init_test(cx, |_| {});
9279
9280 let mut cx = EditorLspTestContext::new_rust(
9281 lsp::ServerCapabilities {
9282 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9283 ..Default::default()
9284 },
9285 cx,
9286 )
9287 .await;
9288
9289 cx.set_state(indoc! {"
9290 one.twoˇ
9291 "});
9292
9293 // The format request takes a long time. When it completes, it inserts
9294 // a newline and an indent before the `.`
9295 cx.lsp
9296 .set_request_handler::<lsp::request::Formatting, _, _>(move |_, cx| {
9297 let executor = cx.background_executor().clone();
9298 async move {
9299 executor.timer(Duration::from_millis(100)).await;
9300 Ok(Some(vec![lsp::TextEdit {
9301 range: lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 3)),
9302 new_text: "\n ".into(),
9303 }]))
9304 }
9305 });
9306
9307 // Submit a format request.
9308 let format_1 = cx
9309 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9310 .unwrap();
9311 cx.executor().run_until_parked();
9312
9313 // Submit a second format request.
9314 let format_2 = cx
9315 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9316 .unwrap();
9317 cx.executor().run_until_parked();
9318
9319 // Wait for both format requests to complete
9320 cx.executor().advance_clock(Duration::from_millis(200));
9321 cx.executor().start_waiting();
9322 format_1.await.unwrap();
9323 cx.executor().start_waiting();
9324 format_2.await.unwrap();
9325
9326 // The formatting edits only happens once.
9327 cx.assert_editor_state(indoc! {"
9328 one
9329 .twoˇ
9330 "});
9331}
9332
9333#[gpui::test]
9334async fn test_strip_whitespace_and_format_via_lsp(cx: &mut TestAppContext) {
9335 init_test(cx, |settings| {
9336 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
9337 });
9338
9339 let mut cx = EditorLspTestContext::new_rust(
9340 lsp::ServerCapabilities {
9341 document_formatting_provider: Some(lsp::OneOf::Left(true)),
9342 ..Default::default()
9343 },
9344 cx,
9345 )
9346 .await;
9347
9348 // Set up a buffer white some trailing whitespace and no trailing newline.
9349 cx.set_state(
9350 &[
9351 "one ", //
9352 "twoˇ", //
9353 "three ", //
9354 "four", //
9355 ]
9356 .join("\n"),
9357 );
9358
9359 // Submit a format request.
9360 let format = cx
9361 .update_editor(|editor, window, cx| editor.format(&Format, window, cx))
9362 .unwrap();
9363
9364 // Record which buffer changes have been sent to the language server
9365 let buffer_changes = Arc::new(Mutex::new(Vec::new()));
9366 cx.lsp
9367 .handle_notification::<lsp::notification::DidChangeTextDocument, _>({
9368 let buffer_changes = buffer_changes.clone();
9369 move |params, _| {
9370 buffer_changes.lock().extend(
9371 params
9372 .content_changes
9373 .into_iter()
9374 .map(|e| (e.range.unwrap(), e.text)),
9375 );
9376 }
9377 });
9378
9379 // Handle formatting requests to the language server.
9380 cx.lsp
9381 .set_request_handler::<lsp::request::Formatting, _, _>({
9382 let buffer_changes = buffer_changes.clone();
9383 move |_, _| {
9384 // When formatting is requested, trailing whitespace has already been stripped,
9385 // and the trailing newline has already been added.
9386 assert_eq!(
9387 &buffer_changes.lock()[1..],
9388 &[
9389 (
9390 lsp::Range::new(lsp::Position::new(0, 3), lsp::Position::new(0, 4)),
9391 "".into()
9392 ),
9393 (
9394 lsp::Range::new(lsp::Position::new(2, 5), lsp::Position::new(2, 6)),
9395 "".into()
9396 ),
9397 (
9398 lsp::Range::new(lsp::Position::new(3, 4), lsp::Position::new(3, 4)),
9399 "\n".into()
9400 ),
9401 ]
9402 );
9403
9404 // Insert blank lines between each line of the buffer.
9405 async move {
9406 Ok(Some(vec![
9407 lsp::TextEdit {
9408 range: lsp::Range::new(
9409 lsp::Position::new(1, 0),
9410 lsp::Position::new(1, 0),
9411 ),
9412 new_text: "\n".into(),
9413 },
9414 lsp::TextEdit {
9415 range: lsp::Range::new(
9416 lsp::Position::new(2, 0),
9417 lsp::Position::new(2, 0),
9418 ),
9419 new_text: "\n".into(),
9420 },
9421 ]))
9422 }
9423 }
9424 });
9425
9426 // After formatting the buffer, the trailing whitespace is stripped,
9427 // a newline is appended, and the edits provided by the language server
9428 // have been applied.
9429 format.await.unwrap();
9430 cx.assert_editor_state(
9431 &[
9432 "one", //
9433 "", //
9434 "twoˇ", //
9435 "", //
9436 "three", //
9437 "four", //
9438 "", //
9439 ]
9440 .join("\n"),
9441 );
9442
9443 // Undoing the formatting undoes the trailing whitespace removal, the
9444 // trailing newline, and the LSP edits.
9445 cx.update_buffer(|buffer, cx| buffer.undo(cx));
9446 cx.assert_editor_state(
9447 &[
9448 "one ", //
9449 "twoˇ", //
9450 "three ", //
9451 "four", //
9452 ]
9453 .join("\n"),
9454 );
9455}
9456
9457#[gpui::test]
9458async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
9459 cx: &mut TestAppContext,
9460) {
9461 init_test(cx, |_| {});
9462
9463 cx.update(|cx| {
9464 cx.update_global::<SettingsStore, _>(|settings, cx| {
9465 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9466 settings.auto_signature_help = Some(true);
9467 });
9468 });
9469 });
9470
9471 let mut cx = EditorLspTestContext::new_rust(
9472 lsp::ServerCapabilities {
9473 signature_help_provider: Some(lsp::SignatureHelpOptions {
9474 ..Default::default()
9475 }),
9476 ..Default::default()
9477 },
9478 cx,
9479 )
9480 .await;
9481
9482 let language = Language::new(
9483 LanguageConfig {
9484 name: "Rust".into(),
9485 brackets: BracketPairConfig {
9486 pairs: vec![
9487 BracketPair {
9488 start: "{".to_string(),
9489 end: "}".to_string(),
9490 close: true,
9491 surround: true,
9492 newline: true,
9493 },
9494 BracketPair {
9495 start: "(".to_string(),
9496 end: ")".to_string(),
9497 close: true,
9498 surround: true,
9499 newline: true,
9500 },
9501 BracketPair {
9502 start: "/*".to_string(),
9503 end: " */".to_string(),
9504 close: true,
9505 surround: true,
9506 newline: true,
9507 },
9508 BracketPair {
9509 start: "[".to_string(),
9510 end: "]".to_string(),
9511 close: false,
9512 surround: false,
9513 newline: true,
9514 },
9515 BracketPair {
9516 start: "\"".to_string(),
9517 end: "\"".to_string(),
9518 close: true,
9519 surround: true,
9520 newline: false,
9521 },
9522 BracketPair {
9523 start: "<".to_string(),
9524 end: ">".to_string(),
9525 close: false,
9526 surround: true,
9527 newline: true,
9528 },
9529 ],
9530 ..Default::default()
9531 },
9532 autoclose_before: "})]".to_string(),
9533 ..Default::default()
9534 },
9535 Some(tree_sitter_rust::LANGUAGE.into()),
9536 );
9537 let language = Arc::new(language);
9538
9539 cx.language_registry().add(language.clone());
9540 cx.update_buffer(|buffer, cx| {
9541 buffer.set_language(Some(language), cx);
9542 });
9543
9544 cx.set_state(
9545 &r#"
9546 fn main() {
9547 sampleˇ
9548 }
9549 "#
9550 .unindent(),
9551 );
9552
9553 cx.update_editor(|editor, window, cx| {
9554 editor.handle_input("(", window, cx);
9555 });
9556 cx.assert_editor_state(
9557 &"
9558 fn main() {
9559 sample(ˇ)
9560 }
9561 "
9562 .unindent(),
9563 );
9564
9565 let mocked_response = lsp::SignatureHelp {
9566 signatures: vec![lsp::SignatureInformation {
9567 label: "fn sample(param1: u8, param2: u8)".to_string(),
9568 documentation: None,
9569 parameters: Some(vec![
9570 lsp::ParameterInformation {
9571 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9572 documentation: None,
9573 },
9574 lsp::ParameterInformation {
9575 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9576 documentation: None,
9577 },
9578 ]),
9579 active_parameter: None,
9580 }],
9581 active_signature: Some(0),
9582 active_parameter: Some(0),
9583 };
9584 handle_signature_help_request(&mut cx, mocked_response).await;
9585
9586 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9587 .await;
9588
9589 cx.editor(|editor, _, _| {
9590 let signature_help_state = editor.signature_help_state.popover().cloned();
9591 assert_eq!(
9592 signature_help_state.unwrap().label,
9593 "param1: u8, param2: u8"
9594 );
9595 });
9596}
9597
9598#[gpui::test]
9599async fn test_handle_input_with_different_show_signature_settings(cx: &mut TestAppContext) {
9600 init_test(cx, |_| {});
9601
9602 cx.update(|cx| {
9603 cx.update_global::<SettingsStore, _>(|settings, cx| {
9604 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9605 settings.auto_signature_help = Some(false);
9606 settings.show_signature_help_after_edits = Some(false);
9607 });
9608 });
9609 });
9610
9611 let mut cx = EditorLspTestContext::new_rust(
9612 lsp::ServerCapabilities {
9613 signature_help_provider: Some(lsp::SignatureHelpOptions {
9614 ..Default::default()
9615 }),
9616 ..Default::default()
9617 },
9618 cx,
9619 )
9620 .await;
9621
9622 let language = Language::new(
9623 LanguageConfig {
9624 name: "Rust".into(),
9625 brackets: BracketPairConfig {
9626 pairs: vec![
9627 BracketPair {
9628 start: "{".to_string(),
9629 end: "}".to_string(),
9630 close: true,
9631 surround: true,
9632 newline: true,
9633 },
9634 BracketPair {
9635 start: "(".to_string(),
9636 end: ")".to_string(),
9637 close: true,
9638 surround: true,
9639 newline: true,
9640 },
9641 BracketPair {
9642 start: "/*".to_string(),
9643 end: " */".to_string(),
9644 close: true,
9645 surround: true,
9646 newline: true,
9647 },
9648 BracketPair {
9649 start: "[".to_string(),
9650 end: "]".to_string(),
9651 close: false,
9652 surround: false,
9653 newline: true,
9654 },
9655 BracketPair {
9656 start: "\"".to_string(),
9657 end: "\"".to_string(),
9658 close: true,
9659 surround: true,
9660 newline: false,
9661 },
9662 BracketPair {
9663 start: "<".to_string(),
9664 end: ">".to_string(),
9665 close: false,
9666 surround: true,
9667 newline: true,
9668 },
9669 ],
9670 ..Default::default()
9671 },
9672 autoclose_before: "})]".to_string(),
9673 ..Default::default()
9674 },
9675 Some(tree_sitter_rust::LANGUAGE.into()),
9676 );
9677 let language = Arc::new(language);
9678
9679 cx.language_registry().add(language.clone());
9680 cx.update_buffer(|buffer, cx| {
9681 buffer.set_language(Some(language), cx);
9682 });
9683
9684 // Ensure that signature_help is not called when no signature help is enabled.
9685 cx.set_state(
9686 &r#"
9687 fn main() {
9688 sampleˇ
9689 }
9690 "#
9691 .unindent(),
9692 );
9693 cx.update_editor(|editor, window, cx| {
9694 editor.handle_input("(", window, cx);
9695 });
9696 cx.assert_editor_state(
9697 &"
9698 fn main() {
9699 sample(ˇ)
9700 }
9701 "
9702 .unindent(),
9703 );
9704 cx.editor(|editor, _, _| {
9705 assert!(editor.signature_help_state.task().is_none());
9706 });
9707
9708 let mocked_response = lsp::SignatureHelp {
9709 signatures: vec![lsp::SignatureInformation {
9710 label: "fn sample(param1: u8, param2: u8)".to_string(),
9711 documentation: None,
9712 parameters: Some(vec![
9713 lsp::ParameterInformation {
9714 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9715 documentation: None,
9716 },
9717 lsp::ParameterInformation {
9718 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9719 documentation: None,
9720 },
9721 ]),
9722 active_parameter: None,
9723 }],
9724 active_signature: Some(0),
9725 active_parameter: Some(0),
9726 };
9727
9728 // Ensure that signature_help is called when enabled afte edits
9729 cx.update(|_, cx| {
9730 cx.update_global::<SettingsStore, _>(|settings, cx| {
9731 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9732 settings.auto_signature_help = Some(false);
9733 settings.show_signature_help_after_edits = Some(true);
9734 });
9735 });
9736 });
9737 cx.set_state(
9738 &r#"
9739 fn main() {
9740 sampleˇ
9741 }
9742 "#
9743 .unindent(),
9744 );
9745 cx.update_editor(|editor, window, cx| {
9746 editor.handle_input("(", window, cx);
9747 });
9748 cx.assert_editor_state(
9749 &"
9750 fn main() {
9751 sample(ˇ)
9752 }
9753 "
9754 .unindent(),
9755 );
9756 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9757 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9758 .await;
9759 cx.update_editor(|editor, _, _| {
9760 let signature_help_state = editor.signature_help_state.popover().cloned();
9761 assert!(signature_help_state.is_some());
9762 assert_eq!(
9763 signature_help_state.unwrap().label,
9764 "param1: u8, param2: u8"
9765 );
9766 editor.signature_help_state = SignatureHelpState::default();
9767 });
9768
9769 // Ensure that signature_help is called when auto signature help override is enabled
9770 cx.update(|_, cx| {
9771 cx.update_global::<SettingsStore, _>(|settings, cx| {
9772 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9773 settings.auto_signature_help = Some(true);
9774 settings.show_signature_help_after_edits = Some(false);
9775 });
9776 });
9777 });
9778 cx.set_state(
9779 &r#"
9780 fn main() {
9781 sampleˇ
9782 }
9783 "#
9784 .unindent(),
9785 );
9786 cx.update_editor(|editor, window, cx| {
9787 editor.handle_input("(", window, cx);
9788 });
9789 cx.assert_editor_state(
9790 &"
9791 fn main() {
9792 sample(ˇ)
9793 }
9794 "
9795 .unindent(),
9796 );
9797 handle_signature_help_request(&mut cx, mocked_response).await;
9798 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9799 .await;
9800 cx.editor(|editor, _, _| {
9801 let signature_help_state = editor.signature_help_state.popover().cloned();
9802 assert!(signature_help_state.is_some());
9803 assert_eq!(
9804 signature_help_state.unwrap().label,
9805 "param1: u8, param2: u8"
9806 );
9807 });
9808}
9809
9810#[gpui::test]
9811async fn test_signature_help(cx: &mut TestAppContext) {
9812 init_test(cx, |_| {});
9813 cx.update(|cx| {
9814 cx.update_global::<SettingsStore, _>(|settings, cx| {
9815 settings.update_user_settings::<EditorSettings>(cx, |settings| {
9816 settings.auto_signature_help = Some(true);
9817 });
9818 });
9819 });
9820
9821 let mut cx = EditorLspTestContext::new_rust(
9822 lsp::ServerCapabilities {
9823 signature_help_provider: Some(lsp::SignatureHelpOptions {
9824 ..Default::default()
9825 }),
9826 ..Default::default()
9827 },
9828 cx,
9829 )
9830 .await;
9831
9832 // A test that directly calls `show_signature_help`
9833 cx.update_editor(|editor, window, cx| {
9834 editor.show_signature_help(&ShowSignatureHelp, window, cx);
9835 });
9836
9837 let mocked_response = lsp::SignatureHelp {
9838 signatures: vec![lsp::SignatureInformation {
9839 label: "fn sample(param1: u8, param2: u8)".to_string(),
9840 documentation: None,
9841 parameters: Some(vec![
9842 lsp::ParameterInformation {
9843 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9844 documentation: None,
9845 },
9846 lsp::ParameterInformation {
9847 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9848 documentation: None,
9849 },
9850 ]),
9851 active_parameter: None,
9852 }],
9853 active_signature: Some(0),
9854 active_parameter: Some(0),
9855 };
9856 handle_signature_help_request(&mut cx, mocked_response).await;
9857
9858 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9859 .await;
9860
9861 cx.editor(|editor, _, _| {
9862 let signature_help_state = editor.signature_help_state.popover().cloned();
9863 assert!(signature_help_state.is_some());
9864 assert_eq!(
9865 signature_help_state.unwrap().label,
9866 "param1: u8, param2: u8"
9867 );
9868 });
9869
9870 // When exiting outside from inside the brackets, `signature_help` is closed.
9871 cx.set_state(indoc! {"
9872 fn main() {
9873 sample(ˇ);
9874 }
9875
9876 fn sample(param1: u8, param2: u8) {}
9877 "});
9878
9879 cx.update_editor(|editor, window, cx| {
9880 editor.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
9881 });
9882
9883 let mocked_response = lsp::SignatureHelp {
9884 signatures: Vec::new(),
9885 active_signature: None,
9886 active_parameter: None,
9887 };
9888 handle_signature_help_request(&mut cx, mocked_response).await;
9889
9890 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
9891 .await;
9892
9893 cx.editor(|editor, _, _| {
9894 assert!(!editor.signature_help_state.is_shown());
9895 });
9896
9897 // When entering inside the brackets from outside, `show_signature_help` is automatically called.
9898 cx.set_state(indoc! {"
9899 fn main() {
9900 sample(ˇ);
9901 }
9902
9903 fn sample(param1: u8, param2: u8) {}
9904 "});
9905
9906 let mocked_response = lsp::SignatureHelp {
9907 signatures: vec![lsp::SignatureInformation {
9908 label: "fn sample(param1: u8, param2: u8)".to_string(),
9909 documentation: None,
9910 parameters: Some(vec![
9911 lsp::ParameterInformation {
9912 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9913 documentation: None,
9914 },
9915 lsp::ParameterInformation {
9916 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9917 documentation: None,
9918 },
9919 ]),
9920 active_parameter: None,
9921 }],
9922 active_signature: Some(0),
9923 active_parameter: Some(0),
9924 };
9925 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9926 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9927 .await;
9928 cx.editor(|editor, _, _| {
9929 assert!(editor.signature_help_state.is_shown());
9930 });
9931
9932 // Restore the popover with more parameter input
9933 cx.set_state(indoc! {"
9934 fn main() {
9935 sample(param1, param2ˇ);
9936 }
9937
9938 fn sample(param1: u8, param2: u8) {}
9939 "});
9940
9941 let mocked_response = lsp::SignatureHelp {
9942 signatures: vec![lsp::SignatureInformation {
9943 label: "fn sample(param1: u8, param2: u8)".to_string(),
9944 documentation: None,
9945 parameters: Some(vec![
9946 lsp::ParameterInformation {
9947 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
9948 documentation: None,
9949 },
9950 lsp::ParameterInformation {
9951 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
9952 documentation: None,
9953 },
9954 ]),
9955 active_parameter: None,
9956 }],
9957 active_signature: Some(0),
9958 active_parameter: Some(1),
9959 };
9960 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
9961 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9962 .await;
9963
9964 // When selecting a range, the popover is gone.
9965 // Avoid using `cx.set_state` to not actually edit the document, just change its selections.
9966 cx.update_editor(|editor, window, cx| {
9967 editor.change_selections(None, window, cx, |s| {
9968 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
9969 })
9970 });
9971 cx.assert_editor_state(indoc! {"
9972 fn main() {
9973 sample(param1, «ˇparam2»);
9974 }
9975
9976 fn sample(param1: u8, param2: u8) {}
9977 "});
9978 cx.editor(|editor, _, _| {
9979 assert!(!editor.signature_help_state.is_shown());
9980 });
9981
9982 // When unselecting again, the popover is back if within the brackets.
9983 cx.update_editor(|editor, window, cx| {
9984 editor.change_selections(None, window, cx, |s| {
9985 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
9986 })
9987 });
9988 cx.assert_editor_state(indoc! {"
9989 fn main() {
9990 sample(param1, ˇparam2);
9991 }
9992
9993 fn sample(param1: u8, param2: u8) {}
9994 "});
9995 handle_signature_help_request(&mut cx, mocked_response).await;
9996 cx.condition(|editor, _| editor.signature_help_state.is_shown())
9997 .await;
9998 cx.editor(|editor, _, _| {
9999 assert!(editor.signature_help_state.is_shown());
10000 });
10001
10002 // Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
10003 cx.update_editor(|editor, window, cx| {
10004 editor.change_selections(None, window, cx, |s| {
10005 s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
10006 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10007 })
10008 });
10009 cx.assert_editor_state(indoc! {"
10010 fn main() {
10011 sample(param1, ˇparam2);
10012 }
10013
10014 fn sample(param1: u8, param2: u8) {}
10015 "});
10016
10017 let mocked_response = lsp::SignatureHelp {
10018 signatures: vec![lsp::SignatureInformation {
10019 label: "fn sample(param1: u8, param2: u8)".to_string(),
10020 documentation: None,
10021 parameters: Some(vec![
10022 lsp::ParameterInformation {
10023 label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
10024 documentation: None,
10025 },
10026 lsp::ParameterInformation {
10027 label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
10028 documentation: None,
10029 },
10030 ]),
10031 active_parameter: None,
10032 }],
10033 active_signature: Some(0),
10034 active_parameter: Some(1),
10035 };
10036 handle_signature_help_request(&mut cx, mocked_response.clone()).await;
10037 cx.condition(|editor, _| editor.signature_help_state.is_shown())
10038 .await;
10039 cx.update_editor(|editor, _, cx| {
10040 editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
10041 });
10042 cx.condition(|editor, _| !editor.signature_help_state.is_shown())
10043 .await;
10044 cx.update_editor(|editor, window, cx| {
10045 editor.change_selections(None, window, cx, |s| {
10046 s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
10047 })
10048 });
10049 cx.assert_editor_state(indoc! {"
10050 fn main() {
10051 sample(param1, «ˇparam2»);
10052 }
10053
10054 fn sample(param1: u8, param2: u8) {}
10055 "});
10056 cx.update_editor(|editor, window, cx| {
10057 editor.change_selections(None, window, cx, |s| {
10058 s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
10059 })
10060 });
10061 cx.assert_editor_state(indoc! {"
10062 fn main() {
10063 sample(param1, ˇparam2);
10064 }
10065
10066 fn sample(param1: u8, param2: u8) {}
10067 "});
10068 cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
10069 .await;
10070}
10071
10072#[gpui::test]
10073async fn test_completion_mode(cx: &mut TestAppContext) {
10074 init_test(cx, |_| {});
10075 let mut cx = EditorLspTestContext::new_rust(
10076 lsp::ServerCapabilities {
10077 completion_provider: Some(lsp::CompletionOptions {
10078 resolve_provider: Some(true),
10079 ..Default::default()
10080 }),
10081 ..Default::default()
10082 },
10083 cx,
10084 )
10085 .await;
10086
10087 struct Run {
10088 run_description: &'static str,
10089 initial_state: String,
10090 buffer_marked_text: String,
10091 completion_text: &'static str,
10092 expected_with_insert_mode: String,
10093 expected_with_replace_mode: String,
10094 expected_with_replace_subsequence_mode: String,
10095 expected_with_replace_suffix_mode: String,
10096 }
10097
10098 let runs = [
10099 Run {
10100 run_description: "Start of word matches completion text",
10101 initial_state: "before ediˇ after".into(),
10102 buffer_marked_text: "before <edi|> after".into(),
10103 completion_text: "editor",
10104 expected_with_insert_mode: "before editorˇ after".into(),
10105 expected_with_replace_mode: "before editorˇ after".into(),
10106 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10107 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10108 },
10109 Run {
10110 run_description: "Accept same text at the middle of the word",
10111 initial_state: "before ediˇtor after".into(),
10112 buffer_marked_text: "before <edi|tor> after".into(),
10113 completion_text: "editor",
10114 expected_with_insert_mode: "before editorˇtor after".into(),
10115 expected_with_replace_mode: "before editorˇ after".into(),
10116 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10117 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10118 },
10119 Run {
10120 run_description: "End of word matches completion text -- cursor at end",
10121 initial_state: "before torˇ after".into(),
10122 buffer_marked_text: "before <tor|> after".into(),
10123 completion_text: "editor",
10124 expected_with_insert_mode: "before editorˇ after".into(),
10125 expected_with_replace_mode: "before editorˇ after".into(),
10126 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10127 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10128 },
10129 Run {
10130 run_description: "End of word matches completion text -- cursor at start",
10131 initial_state: "before ˇtor after".into(),
10132 buffer_marked_text: "before <|tor> after".into(),
10133 completion_text: "editor",
10134 expected_with_insert_mode: "before editorˇtor after".into(),
10135 expected_with_replace_mode: "before editorˇ after".into(),
10136 expected_with_replace_subsequence_mode: "before editorˇ after".into(),
10137 expected_with_replace_suffix_mode: "before editorˇ after".into(),
10138 },
10139 Run {
10140 run_description: "Prepend text containing whitespace",
10141 initial_state: "pˇfield: bool".into(),
10142 buffer_marked_text: "<p|field>: bool".into(),
10143 completion_text: "pub ",
10144 expected_with_insert_mode: "pub ˇfield: bool".into(),
10145 expected_with_replace_mode: "pub ˇ: bool".into(),
10146 expected_with_replace_subsequence_mode: "pub ˇfield: bool".into(),
10147 expected_with_replace_suffix_mode: "pub ˇfield: bool".into(),
10148 },
10149 Run {
10150 run_description: "Add element to start of list",
10151 initial_state: "[element_ˇelement_2]".into(),
10152 buffer_marked_text: "[<element_|element_2>]".into(),
10153 completion_text: "element_1",
10154 expected_with_insert_mode: "[element_1ˇelement_2]".into(),
10155 expected_with_replace_mode: "[element_1ˇ]".into(),
10156 expected_with_replace_subsequence_mode: "[element_1ˇelement_2]".into(),
10157 expected_with_replace_suffix_mode: "[element_1ˇelement_2]".into(),
10158 },
10159 Run {
10160 run_description: "Add element to start of list -- first and second elements are equal",
10161 initial_state: "[elˇelement]".into(),
10162 buffer_marked_text: "[<el|element>]".into(),
10163 completion_text: "element",
10164 expected_with_insert_mode: "[elementˇelement]".into(),
10165 expected_with_replace_mode: "[elementˇ]".into(),
10166 expected_with_replace_subsequence_mode: "[elementˇelement]".into(),
10167 expected_with_replace_suffix_mode: "[elementˇ]".into(),
10168 },
10169 Run {
10170 run_description: "Ends with matching suffix",
10171 initial_state: "SubˇError".into(),
10172 buffer_marked_text: "<Sub|Error>".into(),
10173 completion_text: "SubscriptionError",
10174 expected_with_insert_mode: "SubscriptionErrorˇError".into(),
10175 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10176 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10177 expected_with_replace_suffix_mode: "SubscriptionErrorˇ".into(),
10178 },
10179 Run {
10180 run_description: "Suffix is a subsequence -- contiguous",
10181 initial_state: "SubˇErr".into(),
10182 buffer_marked_text: "<Sub|Err>".into(),
10183 completion_text: "SubscriptionError",
10184 expected_with_insert_mode: "SubscriptionErrorˇErr".into(),
10185 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10186 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10187 expected_with_replace_suffix_mode: "SubscriptionErrorˇErr".into(),
10188 },
10189 Run {
10190 run_description: "Suffix is a subsequence -- non-contiguous -- replace intended",
10191 initial_state: "Suˇscrirr".into(),
10192 buffer_marked_text: "<Su|scrirr>".into(),
10193 completion_text: "SubscriptionError",
10194 expected_with_insert_mode: "SubscriptionErrorˇscrirr".into(),
10195 expected_with_replace_mode: "SubscriptionErrorˇ".into(),
10196 expected_with_replace_subsequence_mode: "SubscriptionErrorˇ".into(),
10197 expected_with_replace_suffix_mode: "SubscriptionErrorˇscrirr".into(),
10198 },
10199 Run {
10200 run_description: "Suffix is a subsequence -- non-contiguous -- replace unintended",
10201 initial_state: "foo(indˇix)".into(),
10202 buffer_marked_text: "foo(<ind|ix>)".into(),
10203 completion_text: "node_index",
10204 expected_with_insert_mode: "foo(node_indexˇix)".into(),
10205 expected_with_replace_mode: "foo(node_indexˇ)".into(),
10206 expected_with_replace_subsequence_mode: "foo(node_indexˇix)".into(),
10207 expected_with_replace_suffix_mode: "foo(node_indexˇix)".into(),
10208 },
10209 ];
10210
10211 for run in runs {
10212 let run_variations = [
10213 (LspInsertMode::Insert, run.expected_with_insert_mode),
10214 (LspInsertMode::Replace, run.expected_with_replace_mode),
10215 (
10216 LspInsertMode::ReplaceSubsequence,
10217 run.expected_with_replace_subsequence_mode,
10218 ),
10219 (
10220 LspInsertMode::ReplaceSuffix,
10221 run.expected_with_replace_suffix_mode,
10222 ),
10223 ];
10224
10225 for (lsp_insert_mode, expected_text) in run_variations {
10226 eprintln!(
10227 "run = {:?}, mode = {lsp_insert_mode:.?}",
10228 run.run_description,
10229 );
10230
10231 update_test_language_settings(&mut cx, |settings| {
10232 settings.defaults.completions = Some(CompletionSettings {
10233 lsp_insert_mode,
10234 words: WordsCompletionMode::Disabled,
10235 lsp: true,
10236 lsp_fetch_timeout_ms: 0,
10237 });
10238 });
10239
10240 cx.set_state(&run.initial_state);
10241 cx.update_editor(|editor, window, cx| {
10242 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10243 });
10244
10245 let counter = Arc::new(AtomicUsize::new(0));
10246 handle_completion_request_with_insert_and_replace(
10247 &mut cx,
10248 &run.buffer_marked_text,
10249 vec![run.completion_text],
10250 counter.clone(),
10251 )
10252 .await;
10253 cx.condition(|editor, _| editor.context_menu_visible())
10254 .await;
10255 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10256
10257 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10258 editor
10259 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10260 .unwrap()
10261 });
10262 cx.assert_editor_state(&expected_text);
10263 handle_resolve_completion_request(&mut cx, None).await;
10264 apply_additional_edits.await.unwrap();
10265 }
10266 }
10267}
10268
10269#[gpui::test]
10270async fn test_completion_with_mode_specified_by_action(cx: &mut TestAppContext) {
10271 init_test(cx, |_| {});
10272 let mut cx = EditorLspTestContext::new_rust(
10273 lsp::ServerCapabilities {
10274 completion_provider: Some(lsp::CompletionOptions {
10275 resolve_provider: Some(true),
10276 ..Default::default()
10277 }),
10278 ..Default::default()
10279 },
10280 cx,
10281 )
10282 .await;
10283
10284 let initial_state = "SubˇError";
10285 let buffer_marked_text = "<Sub|Error>";
10286 let completion_text = "SubscriptionError";
10287 let expected_with_insert_mode = "SubscriptionErrorˇError";
10288 let expected_with_replace_mode = "SubscriptionErrorˇ";
10289
10290 update_test_language_settings(&mut cx, |settings| {
10291 settings.defaults.completions = Some(CompletionSettings {
10292 words: WordsCompletionMode::Disabled,
10293 // set the opposite here to ensure that the action is overriding the default behavior
10294 lsp_insert_mode: LspInsertMode::Insert,
10295 lsp: true,
10296 lsp_fetch_timeout_ms: 0,
10297 });
10298 });
10299
10300 cx.set_state(initial_state);
10301 cx.update_editor(|editor, window, cx| {
10302 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10303 });
10304
10305 let counter = Arc::new(AtomicUsize::new(0));
10306 handle_completion_request_with_insert_and_replace(
10307 &mut cx,
10308 &buffer_marked_text,
10309 vec![completion_text],
10310 counter.clone(),
10311 )
10312 .await;
10313 cx.condition(|editor, _| editor.context_menu_visible())
10314 .await;
10315 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10316
10317 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10318 editor
10319 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10320 .unwrap()
10321 });
10322 cx.assert_editor_state(&expected_with_replace_mode);
10323 handle_resolve_completion_request(&mut cx, None).await;
10324 apply_additional_edits.await.unwrap();
10325
10326 update_test_language_settings(&mut cx, |settings| {
10327 settings.defaults.completions = Some(CompletionSettings {
10328 words: WordsCompletionMode::Disabled,
10329 // set the opposite here to ensure that the action is overriding the default behavior
10330 lsp_insert_mode: LspInsertMode::Replace,
10331 lsp: true,
10332 lsp_fetch_timeout_ms: 0,
10333 });
10334 });
10335
10336 cx.set_state(initial_state);
10337 cx.update_editor(|editor, window, cx| {
10338 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10339 });
10340 handle_completion_request_with_insert_and_replace(
10341 &mut cx,
10342 &buffer_marked_text,
10343 vec![completion_text],
10344 counter.clone(),
10345 )
10346 .await;
10347 cx.condition(|editor, _| editor.context_menu_visible())
10348 .await;
10349 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10350
10351 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10352 editor
10353 .confirm_completion_insert(&ConfirmCompletionInsert, window, cx)
10354 .unwrap()
10355 });
10356 cx.assert_editor_state(&expected_with_insert_mode);
10357 handle_resolve_completion_request(&mut cx, None).await;
10358 apply_additional_edits.await.unwrap();
10359}
10360
10361#[gpui::test]
10362async fn test_completion_replacing_surrounding_text_with_multicursors(cx: &mut TestAppContext) {
10363 init_test(cx, |_| {});
10364 let mut cx = EditorLspTestContext::new_rust(
10365 lsp::ServerCapabilities {
10366 completion_provider: Some(lsp::CompletionOptions {
10367 resolve_provider: Some(true),
10368 ..Default::default()
10369 }),
10370 ..Default::default()
10371 },
10372 cx,
10373 )
10374 .await;
10375
10376 // scenario: surrounding text matches completion text
10377 let completion_text = "to_offset";
10378 let initial_state = indoc! {"
10379 1. buf.to_offˇsuffix
10380 2. buf.to_offˇsuf
10381 3. buf.to_offˇfix
10382 4. buf.to_offˇ
10383 5. into_offˇensive
10384 6. ˇsuffix
10385 7. let ˇ //
10386 8. aaˇzz
10387 9. buf.to_off«zzzzzˇ»suffix
10388 10. buf.«ˇzzzzz»suffix
10389 11. to_off«ˇzzzzz»
10390
10391 buf.to_offˇsuffix // newest cursor
10392 "};
10393 let completion_marked_buffer = indoc! {"
10394 1. buf.to_offsuffix
10395 2. buf.to_offsuf
10396 3. buf.to_offfix
10397 4. buf.to_off
10398 5. into_offensive
10399 6. suffix
10400 7. let //
10401 8. aazz
10402 9. buf.to_offzzzzzsuffix
10403 10. buf.zzzzzsuffix
10404 11. to_offzzzzz
10405
10406 buf.<to_off|suffix> // newest cursor
10407 "};
10408 let expected = indoc! {"
10409 1. buf.to_offsetˇ
10410 2. buf.to_offsetˇsuf
10411 3. buf.to_offsetˇfix
10412 4. buf.to_offsetˇ
10413 5. into_offsetˇensive
10414 6. to_offsetˇsuffix
10415 7. let to_offsetˇ //
10416 8. aato_offsetˇzz
10417 9. buf.to_offsetˇ
10418 10. buf.to_offsetˇsuffix
10419 11. to_offsetˇ
10420
10421 buf.to_offsetˇ // newest cursor
10422 "};
10423 cx.set_state(initial_state);
10424 cx.update_editor(|editor, window, cx| {
10425 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10426 });
10427 handle_completion_request_with_insert_and_replace(
10428 &mut cx,
10429 completion_marked_buffer,
10430 vec![completion_text],
10431 Arc::new(AtomicUsize::new(0)),
10432 )
10433 .await;
10434 cx.condition(|editor, _| editor.context_menu_visible())
10435 .await;
10436 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10437 editor
10438 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10439 .unwrap()
10440 });
10441 cx.assert_editor_state(expected);
10442 handle_resolve_completion_request(&mut cx, None).await;
10443 apply_additional_edits.await.unwrap();
10444
10445 // scenario: surrounding text matches surroundings of newest cursor, inserting at the end
10446 let completion_text = "foo_and_bar";
10447 let initial_state = indoc! {"
10448 1. ooanbˇ
10449 2. zooanbˇ
10450 3. ooanbˇz
10451 4. zooanbˇz
10452 5. ooanˇ
10453 6. oanbˇ
10454
10455 ooanbˇ
10456 "};
10457 let completion_marked_buffer = indoc! {"
10458 1. ooanb
10459 2. zooanb
10460 3. ooanbz
10461 4. zooanbz
10462 5. ooan
10463 6. oanb
10464
10465 <ooanb|>
10466 "};
10467 let expected = indoc! {"
10468 1. foo_and_barˇ
10469 2. zfoo_and_barˇ
10470 3. foo_and_barˇz
10471 4. zfoo_and_barˇz
10472 5. ooanfoo_and_barˇ
10473 6. oanbfoo_and_barˇ
10474
10475 foo_and_barˇ
10476 "};
10477 cx.set_state(initial_state);
10478 cx.update_editor(|editor, window, cx| {
10479 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10480 });
10481 handle_completion_request_with_insert_and_replace(
10482 &mut cx,
10483 completion_marked_buffer,
10484 vec![completion_text],
10485 Arc::new(AtomicUsize::new(0)),
10486 )
10487 .await;
10488 cx.condition(|editor, _| editor.context_menu_visible())
10489 .await;
10490 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10491 editor
10492 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10493 .unwrap()
10494 });
10495 cx.assert_editor_state(expected);
10496 handle_resolve_completion_request(&mut cx, None).await;
10497 apply_additional_edits.await.unwrap();
10498
10499 // scenario: surrounding text matches surroundings of newest cursor, inserted at the middle
10500 // (expects the same as if it was inserted at the end)
10501 let completion_text = "foo_and_bar";
10502 let initial_state = indoc! {"
10503 1. ooˇanb
10504 2. zooˇanb
10505 3. ooˇanbz
10506 4. zooˇanbz
10507
10508 ooˇanb
10509 "};
10510 let completion_marked_buffer = indoc! {"
10511 1. ooanb
10512 2. zooanb
10513 3. ooanbz
10514 4. zooanbz
10515
10516 <oo|anb>
10517 "};
10518 let expected = indoc! {"
10519 1. foo_and_barˇ
10520 2. zfoo_and_barˇ
10521 3. foo_and_barˇz
10522 4. zfoo_and_barˇz
10523
10524 foo_and_barˇ
10525 "};
10526 cx.set_state(initial_state);
10527 cx.update_editor(|editor, window, cx| {
10528 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10529 });
10530 handle_completion_request_with_insert_and_replace(
10531 &mut cx,
10532 completion_marked_buffer,
10533 vec![completion_text],
10534 Arc::new(AtomicUsize::new(0)),
10535 )
10536 .await;
10537 cx.condition(|editor, _| editor.context_menu_visible())
10538 .await;
10539 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10540 editor
10541 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10542 .unwrap()
10543 });
10544 cx.assert_editor_state(expected);
10545 handle_resolve_completion_request(&mut cx, None).await;
10546 apply_additional_edits.await.unwrap();
10547}
10548
10549// This used to crash
10550#[gpui::test]
10551async fn test_completion_in_multibuffer_with_replace_range(cx: &mut TestAppContext) {
10552 init_test(cx, |_| {});
10553
10554 let buffer_text = indoc! {"
10555 fn main() {
10556 10.satu;
10557
10558 //
10559 // separate cursors so they open in different excerpts (manually reproducible)
10560 //
10561
10562 10.satu20;
10563 }
10564 "};
10565 let multibuffer_text_with_selections = indoc! {"
10566 fn main() {
10567 10.satuˇ;
10568
10569 //
10570
10571 //
10572
10573 10.satuˇ20;
10574 }
10575 "};
10576 let expected_multibuffer = indoc! {"
10577 fn main() {
10578 10.saturating_sub()ˇ;
10579
10580 //
10581
10582 //
10583
10584 10.saturating_sub()ˇ;
10585 }
10586 "};
10587
10588 let first_excerpt_end = buffer_text.find("//").unwrap() + 3;
10589 let second_excerpt_end = buffer_text.rfind("//").unwrap() - 4;
10590
10591 let fs = FakeFs::new(cx.executor());
10592 fs.insert_tree(
10593 path!("/a"),
10594 json!({
10595 "main.rs": buffer_text,
10596 }),
10597 )
10598 .await;
10599
10600 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
10601 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
10602 language_registry.add(rust_lang());
10603 let mut fake_servers = language_registry.register_fake_lsp(
10604 "Rust",
10605 FakeLspAdapter {
10606 capabilities: lsp::ServerCapabilities {
10607 completion_provider: Some(lsp::CompletionOptions {
10608 resolve_provider: None,
10609 ..lsp::CompletionOptions::default()
10610 }),
10611 ..lsp::ServerCapabilities::default()
10612 },
10613 ..FakeLspAdapter::default()
10614 },
10615 );
10616 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
10617 let cx = &mut VisualTestContext::from_window(*workspace, cx);
10618 let buffer = project
10619 .update(cx, |project, cx| {
10620 project.open_local_buffer(path!("/a/main.rs"), cx)
10621 })
10622 .await
10623 .unwrap();
10624
10625 let multi_buffer = cx.new(|cx| {
10626 let mut multi_buffer = MultiBuffer::new(Capability::ReadWrite);
10627 multi_buffer.push_excerpts(
10628 buffer.clone(),
10629 [ExcerptRange::new(0..first_excerpt_end)],
10630 cx,
10631 );
10632 multi_buffer.push_excerpts(
10633 buffer.clone(),
10634 [ExcerptRange::new(second_excerpt_end..buffer_text.len())],
10635 cx,
10636 );
10637 multi_buffer
10638 });
10639
10640 let editor = workspace
10641 .update(cx, |_, window, cx| {
10642 cx.new(|cx| {
10643 Editor::new(
10644 EditorMode::Full {
10645 scale_ui_elements_with_buffer_font_size: false,
10646 show_active_line_background: false,
10647 sized_by_content: false,
10648 },
10649 multi_buffer.clone(),
10650 Some(project.clone()),
10651 window,
10652 cx,
10653 )
10654 })
10655 })
10656 .unwrap();
10657
10658 let pane = workspace
10659 .update(cx, |workspace, _, _| workspace.active_pane().clone())
10660 .unwrap();
10661 pane.update_in(cx, |pane, window, cx| {
10662 pane.add_item(Box::new(editor.clone()), true, true, None, window, cx);
10663 });
10664
10665 let fake_server = fake_servers.next().await.unwrap();
10666
10667 editor.update_in(cx, |editor, window, cx| {
10668 editor.change_selections(None, window, cx, |s| {
10669 s.select_ranges([
10670 Point::new(1, 11)..Point::new(1, 11),
10671 Point::new(7, 11)..Point::new(7, 11),
10672 ])
10673 });
10674
10675 assert_text_with_selections(editor, multibuffer_text_with_selections, cx);
10676 });
10677
10678 editor.update_in(cx, |editor, window, cx| {
10679 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10680 });
10681
10682 fake_server
10683 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
10684 let completion_item = lsp::CompletionItem {
10685 label: "saturating_sub()".into(),
10686 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
10687 lsp::InsertReplaceEdit {
10688 new_text: "saturating_sub()".to_owned(),
10689 insert: lsp::Range::new(
10690 lsp::Position::new(7, 7),
10691 lsp::Position::new(7, 11),
10692 ),
10693 replace: lsp::Range::new(
10694 lsp::Position::new(7, 7),
10695 lsp::Position::new(7, 13),
10696 ),
10697 },
10698 )),
10699 ..lsp::CompletionItem::default()
10700 };
10701
10702 Ok(Some(lsp::CompletionResponse::Array(vec![completion_item])))
10703 })
10704 .next()
10705 .await
10706 .unwrap();
10707
10708 cx.condition(&editor, |editor, _| editor.context_menu_visible())
10709 .await;
10710
10711 editor
10712 .update_in(cx, |editor, window, cx| {
10713 editor
10714 .confirm_completion_replace(&ConfirmCompletionReplace, window, cx)
10715 .unwrap()
10716 })
10717 .await
10718 .unwrap();
10719
10720 editor.update(cx, |editor, cx| {
10721 assert_text_with_selections(editor, expected_multibuffer, cx);
10722 })
10723}
10724
10725#[gpui::test]
10726async fn test_completion(cx: &mut TestAppContext) {
10727 init_test(cx, |_| {});
10728
10729 let mut cx = EditorLspTestContext::new_rust(
10730 lsp::ServerCapabilities {
10731 completion_provider: Some(lsp::CompletionOptions {
10732 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10733 resolve_provider: Some(true),
10734 ..Default::default()
10735 }),
10736 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10737 ..Default::default()
10738 },
10739 cx,
10740 )
10741 .await;
10742 let counter = Arc::new(AtomicUsize::new(0));
10743
10744 cx.set_state(indoc! {"
10745 oneˇ
10746 two
10747 three
10748 "});
10749 cx.simulate_keystroke(".");
10750 handle_completion_request(
10751 &mut cx,
10752 indoc! {"
10753 one.|<>
10754 two
10755 three
10756 "},
10757 vec!["first_completion", "second_completion"],
10758 counter.clone(),
10759 )
10760 .await;
10761 cx.condition(|editor, _| editor.context_menu_visible())
10762 .await;
10763 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
10764
10765 let _handler = handle_signature_help_request(
10766 &mut cx,
10767 lsp::SignatureHelp {
10768 signatures: vec![lsp::SignatureInformation {
10769 label: "test signature".to_string(),
10770 documentation: None,
10771 parameters: Some(vec![lsp::ParameterInformation {
10772 label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
10773 documentation: None,
10774 }]),
10775 active_parameter: None,
10776 }],
10777 active_signature: None,
10778 active_parameter: None,
10779 },
10780 );
10781 cx.update_editor(|editor, window, cx| {
10782 assert!(
10783 !editor.signature_help_state.is_shown(),
10784 "No signature help was called for"
10785 );
10786 editor.show_signature_help(&ShowSignatureHelp, window, cx);
10787 });
10788 cx.run_until_parked();
10789 cx.update_editor(|editor, _, _| {
10790 assert!(
10791 !editor.signature_help_state.is_shown(),
10792 "No signature help should be shown when completions menu is open"
10793 );
10794 });
10795
10796 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10797 editor.context_menu_next(&Default::default(), window, cx);
10798 editor
10799 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10800 .unwrap()
10801 });
10802 cx.assert_editor_state(indoc! {"
10803 one.second_completionˇ
10804 two
10805 three
10806 "});
10807
10808 handle_resolve_completion_request(
10809 &mut cx,
10810 Some(vec![
10811 (
10812 //This overlaps with the primary completion edit which is
10813 //misbehavior from the LSP spec, test that we filter it out
10814 indoc! {"
10815 one.second_ˇcompletion
10816 two
10817 threeˇ
10818 "},
10819 "overlapping additional edit",
10820 ),
10821 (
10822 indoc! {"
10823 one.second_completion
10824 two
10825 threeˇ
10826 "},
10827 "\nadditional edit",
10828 ),
10829 ]),
10830 )
10831 .await;
10832 apply_additional_edits.await.unwrap();
10833 cx.assert_editor_state(indoc! {"
10834 one.second_completionˇ
10835 two
10836 three
10837 additional edit
10838 "});
10839
10840 cx.set_state(indoc! {"
10841 one.second_completion
10842 twoˇ
10843 threeˇ
10844 additional edit
10845 "});
10846 cx.simulate_keystroke(" ");
10847 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10848 cx.simulate_keystroke("s");
10849 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10850
10851 cx.assert_editor_state(indoc! {"
10852 one.second_completion
10853 two sˇ
10854 three sˇ
10855 additional edit
10856 "});
10857 handle_completion_request(
10858 &mut cx,
10859 indoc! {"
10860 one.second_completion
10861 two s
10862 three <s|>
10863 additional edit
10864 "},
10865 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10866 counter.clone(),
10867 )
10868 .await;
10869 cx.condition(|editor, _| editor.context_menu_visible())
10870 .await;
10871 assert_eq!(counter.load(atomic::Ordering::Acquire), 2);
10872
10873 cx.simulate_keystroke("i");
10874
10875 handle_completion_request(
10876 &mut cx,
10877 indoc! {"
10878 one.second_completion
10879 two si
10880 three <si|>
10881 additional edit
10882 "},
10883 vec!["fourth_completion", "fifth_completion", "sixth_completion"],
10884 counter.clone(),
10885 )
10886 .await;
10887 cx.condition(|editor, _| editor.context_menu_visible())
10888 .await;
10889 assert_eq!(counter.load(atomic::Ordering::Acquire), 3);
10890
10891 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10892 editor
10893 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10894 .unwrap()
10895 });
10896 cx.assert_editor_state(indoc! {"
10897 one.second_completion
10898 two sixth_completionˇ
10899 three sixth_completionˇ
10900 additional edit
10901 "});
10902
10903 apply_additional_edits.await.unwrap();
10904
10905 update_test_language_settings(&mut cx, |settings| {
10906 settings.defaults.show_completions_on_input = Some(false);
10907 });
10908 cx.set_state("editorˇ");
10909 cx.simulate_keystroke(".");
10910 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10911 cx.simulate_keystrokes("c l o");
10912 cx.assert_editor_state("editor.cloˇ");
10913 assert!(cx.editor(|e, _, _| e.context_menu.borrow_mut().is_none()));
10914 cx.update_editor(|editor, window, cx| {
10915 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
10916 });
10917 handle_completion_request(
10918 &mut cx,
10919 "editor.<clo|>",
10920 vec!["close", "clobber"],
10921 counter.clone(),
10922 )
10923 .await;
10924 cx.condition(|editor, _| editor.context_menu_visible())
10925 .await;
10926 assert_eq!(counter.load(atomic::Ordering::Acquire), 4);
10927
10928 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
10929 editor
10930 .confirm_completion(&ConfirmCompletion::default(), window, cx)
10931 .unwrap()
10932 });
10933 cx.assert_editor_state("editor.closeˇ");
10934 handle_resolve_completion_request(&mut cx, None).await;
10935 apply_additional_edits.await.unwrap();
10936}
10937
10938#[gpui::test]
10939async fn test_word_completion(cx: &mut TestAppContext) {
10940 let lsp_fetch_timeout_ms = 10;
10941 init_test(cx, |language_settings| {
10942 language_settings.defaults.completions = Some(CompletionSettings {
10943 words: WordsCompletionMode::Fallback,
10944 lsp: true,
10945 lsp_fetch_timeout_ms: 10,
10946 lsp_insert_mode: LspInsertMode::Insert,
10947 });
10948 });
10949
10950 let mut cx = EditorLspTestContext::new_rust(
10951 lsp::ServerCapabilities {
10952 completion_provider: Some(lsp::CompletionOptions {
10953 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
10954 ..lsp::CompletionOptions::default()
10955 }),
10956 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
10957 ..lsp::ServerCapabilities::default()
10958 },
10959 cx,
10960 )
10961 .await;
10962
10963 let throttle_completions = Arc::new(AtomicBool::new(false));
10964
10965 let lsp_throttle_completions = throttle_completions.clone();
10966 let _completion_requests_handler =
10967 cx.lsp
10968 .server
10969 .on_request::<lsp::request::Completion, _, _>(move |_, cx| {
10970 let lsp_throttle_completions = lsp_throttle_completions.clone();
10971 let cx = cx.clone();
10972 async move {
10973 if lsp_throttle_completions.load(atomic::Ordering::Acquire) {
10974 cx.background_executor()
10975 .timer(Duration::from_millis(lsp_fetch_timeout_ms * 10))
10976 .await;
10977 }
10978 Ok(Some(lsp::CompletionResponse::Array(vec![
10979 lsp::CompletionItem {
10980 label: "first".into(),
10981 ..lsp::CompletionItem::default()
10982 },
10983 lsp::CompletionItem {
10984 label: "last".into(),
10985 ..lsp::CompletionItem::default()
10986 },
10987 ])))
10988 }
10989 });
10990
10991 cx.set_state(indoc! {"
10992 oneˇ
10993 two
10994 three
10995 "});
10996 cx.simulate_keystroke(".");
10997 cx.executor().run_until_parked();
10998 cx.condition(|editor, _| editor.context_menu_visible())
10999 .await;
11000 cx.update_editor(|editor, window, cx| {
11001 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11002 {
11003 assert_eq!(
11004 completion_menu_entries(&menu),
11005 &["first", "last"],
11006 "When LSP server is fast to reply, no fallback word completions are used"
11007 );
11008 } else {
11009 panic!("expected completion menu to be open");
11010 }
11011 editor.cancel(&Cancel, window, cx);
11012 });
11013 cx.executor().run_until_parked();
11014 cx.condition(|editor, _| !editor.context_menu_visible())
11015 .await;
11016
11017 throttle_completions.store(true, atomic::Ordering::Release);
11018 cx.simulate_keystroke(".");
11019 cx.executor()
11020 .advance_clock(Duration::from_millis(lsp_fetch_timeout_ms * 2));
11021 cx.executor().run_until_parked();
11022 cx.condition(|editor, _| editor.context_menu_visible())
11023 .await;
11024 cx.update_editor(|editor, _, _| {
11025 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11026 {
11027 assert_eq!(completion_menu_entries(&menu), &["one", "three", "two"],
11028 "When LSP server is slow, document words can be shown instead, if configured accordingly");
11029 } else {
11030 panic!("expected completion menu to be open");
11031 }
11032 });
11033}
11034
11035#[gpui::test]
11036async fn test_word_completions_do_not_duplicate_lsp_ones(cx: &mut TestAppContext) {
11037 init_test(cx, |language_settings| {
11038 language_settings.defaults.completions = Some(CompletionSettings {
11039 words: WordsCompletionMode::Enabled,
11040 lsp: true,
11041 lsp_fetch_timeout_ms: 0,
11042 lsp_insert_mode: LspInsertMode::Insert,
11043 });
11044 });
11045
11046 let mut cx = EditorLspTestContext::new_rust(
11047 lsp::ServerCapabilities {
11048 completion_provider: Some(lsp::CompletionOptions {
11049 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11050 ..lsp::CompletionOptions::default()
11051 }),
11052 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11053 ..lsp::ServerCapabilities::default()
11054 },
11055 cx,
11056 )
11057 .await;
11058
11059 let _completion_requests_handler =
11060 cx.lsp
11061 .server
11062 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11063 Ok(Some(lsp::CompletionResponse::Array(vec![
11064 lsp::CompletionItem {
11065 label: "first".into(),
11066 ..lsp::CompletionItem::default()
11067 },
11068 lsp::CompletionItem {
11069 label: "last".into(),
11070 ..lsp::CompletionItem::default()
11071 },
11072 ])))
11073 });
11074
11075 cx.set_state(indoc! {"ˇ
11076 first
11077 last
11078 second
11079 "});
11080 cx.simulate_keystroke(".");
11081 cx.executor().run_until_parked();
11082 cx.condition(|editor, _| editor.context_menu_visible())
11083 .await;
11084 cx.update_editor(|editor, _, _| {
11085 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11086 {
11087 assert_eq!(
11088 completion_menu_entries(&menu),
11089 &["first", "last", "second"],
11090 "Word completions that has the same edit as the any of the LSP ones, should not be proposed"
11091 );
11092 } else {
11093 panic!("expected completion menu to be open");
11094 }
11095 });
11096}
11097
11098#[gpui::test]
11099async fn test_word_completions_continue_on_typing(cx: &mut TestAppContext) {
11100 init_test(cx, |language_settings| {
11101 language_settings.defaults.completions = Some(CompletionSettings {
11102 words: WordsCompletionMode::Disabled,
11103 lsp: true,
11104 lsp_fetch_timeout_ms: 0,
11105 lsp_insert_mode: LspInsertMode::Insert,
11106 });
11107 });
11108
11109 let mut cx = EditorLspTestContext::new_rust(
11110 lsp::ServerCapabilities {
11111 completion_provider: Some(lsp::CompletionOptions {
11112 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11113 ..lsp::CompletionOptions::default()
11114 }),
11115 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11116 ..lsp::ServerCapabilities::default()
11117 },
11118 cx,
11119 )
11120 .await;
11121
11122 let _completion_requests_handler =
11123 cx.lsp
11124 .server
11125 .on_request::<lsp::request::Completion, _, _>(move |_, _| async move {
11126 panic!("LSP completions should not be queried when dealing with word completions")
11127 });
11128
11129 cx.set_state(indoc! {"ˇ
11130 first
11131 last
11132 second
11133 "});
11134 cx.update_editor(|editor, window, cx| {
11135 editor.show_word_completions(&ShowWordCompletions, window, cx);
11136 });
11137 cx.executor().run_until_parked();
11138 cx.condition(|editor, _| editor.context_menu_visible())
11139 .await;
11140 cx.update_editor(|editor, _, _| {
11141 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11142 {
11143 assert_eq!(
11144 completion_menu_entries(&menu),
11145 &["first", "last", "second"],
11146 "`ShowWordCompletions` action should show word completions"
11147 );
11148 } else {
11149 panic!("expected completion menu to be open");
11150 }
11151 });
11152
11153 cx.simulate_keystroke("l");
11154 cx.executor().run_until_parked();
11155 cx.condition(|editor, _| editor.context_menu_visible())
11156 .await;
11157 cx.update_editor(|editor, _, _| {
11158 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11159 {
11160 assert_eq!(
11161 completion_menu_entries(&menu),
11162 &["last"],
11163 "After showing word completions, further editing should filter them and not query the LSP"
11164 );
11165 } else {
11166 panic!("expected completion menu to be open");
11167 }
11168 });
11169}
11170
11171#[gpui::test]
11172async fn test_word_completions_usually_skip_digits(cx: &mut TestAppContext) {
11173 init_test(cx, |language_settings| {
11174 language_settings.defaults.completions = Some(CompletionSettings {
11175 words: WordsCompletionMode::Fallback,
11176 lsp: false,
11177 lsp_fetch_timeout_ms: 0,
11178 lsp_insert_mode: LspInsertMode::Insert,
11179 });
11180 });
11181
11182 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
11183
11184 cx.set_state(indoc! {"ˇ
11185 0_usize
11186 let
11187 33
11188 4.5f32
11189 "});
11190 cx.update_editor(|editor, window, cx| {
11191 editor.show_completions(&ShowCompletions::default(), window, cx);
11192 });
11193 cx.executor().run_until_parked();
11194 cx.condition(|editor, _| editor.context_menu_visible())
11195 .await;
11196 cx.update_editor(|editor, window, cx| {
11197 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11198 {
11199 assert_eq!(
11200 completion_menu_entries(&menu),
11201 &["let"],
11202 "With no digits in the completion query, no digits should be in the word completions"
11203 );
11204 } else {
11205 panic!("expected completion menu to be open");
11206 }
11207 editor.cancel(&Cancel, window, cx);
11208 });
11209
11210 cx.set_state(indoc! {"3ˇ
11211 0_usize
11212 let
11213 3
11214 33.35f32
11215 "});
11216 cx.update_editor(|editor, window, cx| {
11217 editor.show_completions(&ShowCompletions::default(), window, cx);
11218 });
11219 cx.executor().run_until_parked();
11220 cx.condition(|editor, _| editor.context_menu_visible())
11221 .await;
11222 cx.update_editor(|editor, _, _| {
11223 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11224 {
11225 assert_eq!(completion_menu_entries(&menu), &["33", "35f32"], "The digit is in the completion query, \
11226 return matching words with digits (`33`, `35f32`) but exclude query duplicates (`3`)");
11227 } else {
11228 panic!("expected completion menu to be open");
11229 }
11230 });
11231}
11232
11233fn gen_text_edit(params: &CompletionParams, text: &str) -> Option<lsp::CompletionTextEdit> {
11234 let position = || lsp::Position {
11235 line: params.text_document_position.position.line,
11236 character: params.text_document_position.position.character,
11237 };
11238 Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11239 range: lsp::Range {
11240 start: position(),
11241 end: position(),
11242 },
11243 new_text: text.to_string(),
11244 }))
11245}
11246
11247#[gpui::test]
11248async fn test_multiline_completion(cx: &mut TestAppContext) {
11249 init_test(cx, |_| {});
11250
11251 let fs = FakeFs::new(cx.executor());
11252 fs.insert_tree(
11253 path!("/a"),
11254 json!({
11255 "main.ts": "a",
11256 }),
11257 )
11258 .await;
11259
11260 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
11261 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
11262 let typescript_language = Arc::new(Language::new(
11263 LanguageConfig {
11264 name: "TypeScript".into(),
11265 matcher: LanguageMatcher {
11266 path_suffixes: vec!["ts".to_string()],
11267 ..LanguageMatcher::default()
11268 },
11269 line_comments: vec!["// ".into()],
11270 ..LanguageConfig::default()
11271 },
11272 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
11273 ));
11274 language_registry.add(typescript_language.clone());
11275 let mut fake_servers = language_registry.register_fake_lsp(
11276 "TypeScript",
11277 FakeLspAdapter {
11278 capabilities: lsp::ServerCapabilities {
11279 completion_provider: Some(lsp::CompletionOptions {
11280 trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
11281 ..lsp::CompletionOptions::default()
11282 }),
11283 signature_help_provider: Some(lsp::SignatureHelpOptions::default()),
11284 ..lsp::ServerCapabilities::default()
11285 },
11286 // Emulate vtsls label generation
11287 label_for_completion: Some(Box::new(|item, _| {
11288 let text = if let Some(description) = item
11289 .label_details
11290 .as_ref()
11291 .and_then(|label_details| label_details.description.as_ref())
11292 {
11293 format!("{} {}", item.label, description)
11294 } else if let Some(detail) = &item.detail {
11295 format!("{} {}", item.label, detail)
11296 } else {
11297 item.label.clone()
11298 };
11299 let len = text.len();
11300 Some(language::CodeLabel {
11301 text,
11302 runs: Vec::new(),
11303 filter_range: 0..len,
11304 })
11305 })),
11306 ..FakeLspAdapter::default()
11307 },
11308 );
11309 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
11310 let cx = &mut VisualTestContext::from_window(*workspace, cx);
11311 let worktree_id = workspace
11312 .update(cx, |workspace, _window, cx| {
11313 workspace.project().update(cx, |project, cx| {
11314 project.worktrees(cx).next().unwrap().read(cx).id()
11315 })
11316 })
11317 .unwrap();
11318 let _buffer = project
11319 .update(cx, |project, cx| {
11320 project.open_local_buffer_with_lsp(path!("/a/main.ts"), cx)
11321 })
11322 .await
11323 .unwrap();
11324 let editor = workspace
11325 .update(cx, |workspace, window, cx| {
11326 workspace.open_path((worktree_id, "main.ts"), None, true, window, cx)
11327 })
11328 .unwrap()
11329 .await
11330 .unwrap()
11331 .downcast::<Editor>()
11332 .unwrap();
11333 let fake_server = fake_servers.next().await.unwrap();
11334
11335 let multiline_label = "StickyHeaderExcerpt {\n excerpt,\n next_excerpt_controls_present,\n next_buffer_row,\n }: StickyHeaderExcerpt<'_>,";
11336 let multiline_label_2 = "a\nb\nc\n";
11337 let multiline_detail = "[]struct {\n\tSignerId\tstruct {\n\t\tIssuer\t\t\tstring\t`json:\"issuer\"`\n\t\tSubjectSerialNumber\"`\n}}";
11338 let multiline_description = "d\ne\nf\n";
11339 let multiline_detail_2 = "g\nh\ni\n";
11340
11341 let mut completion_handle = fake_server.set_request_handler::<lsp::request::Completion, _, _>(
11342 move |params, _| async move {
11343 Ok(Some(lsp::CompletionResponse::Array(vec![
11344 lsp::CompletionItem {
11345 label: multiline_label.to_string(),
11346 text_edit: gen_text_edit(¶ms, "new_text_1"),
11347 ..lsp::CompletionItem::default()
11348 },
11349 lsp::CompletionItem {
11350 label: "single line label 1".to_string(),
11351 detail: Some(multiline_detail.to_string()),
11352 text_edit: gen_text_edit(¶ms, "new_text_2"),
11353 ..lsp::CompletionItem::default()
11354 },
11355 lsp::CompletionItem {
11356 label: "single line label 2".to_string(),
11357 label_details: Some(lsp::CompletionItemLabelDetails {
11358 description: Some(multiline_description.to_string()),
11359 detail: None,
11360 }),
11361 text_edit: gen_text_edit(¶ms, "new_text_2"),
11362 ..lsp::CompletionItem::default()
11363 },
11364 lsp::CompletionItem {
11365 label: multiline_label_2.to_string(),
11366 detail: Some(multiline_detail_2.to_string()),
11367 text_edit: gen_text_edit(¶ms, "new_text_3"),
11368 ..lsp::CompletionItem::default()
11369 },
11370 lsp::CompletionItem {
11371 label: "Label with many spaces and \t but without newlines".to_string(),
11372 detail: Some(
11373 "Details with many spaces and \t but without newlines".to_string(),
11374 ),
11375 text_edit: gen_text_edit(¶ms, "new_text_4"),
11376 ..lsp::CompletionItem::default()
11377 },
11378 ])))
11379 },
11380 );
11381
11382 editor.update_in(cx, |editor, window, cx| {
11383 cx.focus_self(window);
11384 editor.move_to_end(&MoveToEnd, window, cx);
11385 editor.handle_input(".", window, cx);
11386 });
11387 cx.run_until_parked();
11388 completion_handle.next().await.unwrap();
11389
11390 editor.update(cx, |editor, _| {
11391 assert!(editor.context_menu_visible());
11392 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11393 {
11394 let completion_labels = menu
11395 .completions
11396 .borrow()
11397 .iter()
11398 .map(|c| c.label.text.clone())
11399 .collect::<Vec<_>>();
11400 assert_eq!(
11401 completion_labels,
11402 &[
11403 "StickyHeaderExcerpt { excerpt, next_excerpt_controls_present, next_buffer_row, }: StickyHeaderExcerpt<'_>,",
11404 "single line label 1 []struct { SignerId struct { Issuer string `json:\"issuer\"` SubjectSerialNumber\"` }}",
11405 "single line label 2 d e f ",
11406 "a b c g h i ",
11407 "Label with many spaces and \t but without newlines Details with many spaces and \t but without newlines",
11408 ],
11409 "Completion items should have their labels without newlines, also replacing excessive whitespaces. Completion items without newlines should not be altered.",
11410 );
11411
11412 for completion in menu
11413 .completions
11414 .borrow()
11415 .iter() {
11416 assert_eq!(
11417 completion.label.filter_range,
11418 0..completion.label.text.len(),
11419 "Adjusted completion items should still keep their filter ranges for the entire label. Item: {completion:?}"
11420 );
11421 }
11422 } else {
11423 panic!("expected completion menu to be open");
11424 }
11425 });
11426}
11427
11428#[gpui::test]
11429async fn test_completion_page_up_down_keys(cx: &mut TestAppContext) {
11430 init_test(cx, |_| {});
11431 let mut cx = EditorLspTestContext::new_rust(
11432 lsp::ServerCapabilities {
11433 completion_provider: Some(lsp::CompletionOptions {
11434 trigger_characters: Some(vec![".".to_string()]),
11435 ..Default::default()
11436 }),
11437 ..Default::default()
11438 },
11439 cx,
11440 )
11441 .await;
11442 cx.lsp
11443 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11444 Ok(Some(lsp::CompletionResponse::Array(vec![
11445 lsp::CompletionItem {
11446 label: "first".into(),
11447 ..Default::default()
11448 },
11449 lsp::CompletionItem {
11450 label: "last".into(),
11451 ..Default::default()
11452 },
11453 ])))
11454 });
11455 cx.set_state("variableˇ");
11456 cx.simulate_keystroke(".");
11457 cx.executor().run_until_parked();
11458
11459 cx.update_editor(|editor, _, _| {
11460 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11461 {
11462 assert_eq!(completion_menu_entries(&menu), &["first", "last"]);
11463 } else {
11464 panic!("expected completion menu to be open");
11465 }
11466 });
11467
11468 cx.update_editor(|editor, window, cx| {
11469 editor.move_page_down(&MovePageDown::default(), window, cx);
11470 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11471 {
11472 assert!(
11473 menu.selected_item == 1,
11474 "expected PageDown to select the last item from the context menu"
11475 );
11476 } else {
11477 panic!("expected completion menu to stay open after PageDown");
11478 }
11479 });
11480
11481 cx.update_editor(|editor, window, cx| {
11482 editor.move_page_up(&MovePageUp::default(), window, cx);
11483 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
11484 {
11485 assert!(
11486 menu.selected_item == 0,
11487 "expected PageUp to select the first item from the context menu"
11488 );
11489 } else {
11490 panic!("expected completion menu to stay open after PageUp");
11491 }
11492 });
11493}
11494
11495#[gpui::test]
11496async fn test_as_is_completions(cx: &mut TestAppContext) {
11497 init_test(cx, |_| {});
11498 let mut cx = EditorLspTestContext::new_rust(
11499 lsp::ServerCapabilities {
11500 completion_provider: Some(lsp::CompletionOptions {
11501 ..Default::default()
11502 }),
11503 ..Default::default()
11504 },
11505 cx,
11506 )
11507 .await;
11508 cx.lsp
11509 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
11510 Ok(Some(lsp::CompletionResponse::Array(vec![
11511 lsp::CompletionItem {
11512 label: "unsafe".into(),
11513 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11514 range: lsp::Range {
11515 start: lsp::Position {
11516 line: 1,
11517 character: 2,
11518 },
11519 end: lsp::Position {
11520 line: 1,
11521 character: 3,
11522 },
11523 },
11524 new_text: "unsafe".to_string(),
11525 })),
11526 insert_text_mode: Some(lsp::InsertTextMode::AS_IS),
11527 ..Default::default()
11528 },
11529 ])))
11530 });
11531 cx.set_state("fn a() {}\n nˇ");
11532 cx.executor().run_until_parked();
11533 cx.update_editor(|editor, window, cx| {
11534 editor.show_completions(
11535 &ShowCompletions {
11536 trigger: Some("\n".into()),
11537 },
11538 window,
11539 cx,
11540 );
11541 });
11542 cx.executor().run_until_parked();
11543
11544 cx.update_editor(|editor, window, cx| {
11545 editor.confirm_completion(&Default::default(), window, cx)
11546 });
11547 cx.executor().run_until_parked();
11548 cx.assert_editor_state("fn a() {}\n unsafeˇ");
11549}
11550
11551#[gpui::test]
11552async fn test_no_duplicated_completion_requests(cx: &mut TestAppContext) {
11553 init_test(cx, |_| {});
11554
11555 let mut cx = EditorLspTestContext::new_rust(
11556 lsp::ServerCapabilities {
11557 completion_provider: Some(lsp::CompletionOptions {
11558 trigger_characters: Some(vec![".".to_string()]),
11559 resolve_provider: Some(true),
11560 ..Default::default()
11561 }),
11562 ..Default::default()
11563 },
11564 cx,
11565 )
11566 .await;
11567
11568 cx.set_state("fn main() { let a = 2ˇ; }");
11569 cx.simulate_keystroke(".");
11570 let completion_item = lsp::CompletionItem {
11571 label: "Some".into(),
11572 kind: Some(lsp::CompletionItemKind::SNIPPET),
11573 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
11574 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
11575 kind: lsp::MarkupKind::Markdown,
11576 value: "```rust\nSome(2)\n```".to_string(),
11577 })),
11578 deprecated: Some(false),
11579 sort_text: Some("Some".to_string()),
11580 filter_text: Some("Some".to_string()),
11581 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
11582 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
11583 range: lsp::Range {
11584 start: lsp::Position {
11585 line: 0,
11586 character: 22,
11587 },
11588 end: lsp::Position {
11589 line: 0,
11590 character: 22,
11591 },
11592 },
11593 new_text: "Some(2)".to_string(),
11594 })),
11595 additional_text_edits: Some(vec![lsp::TextEdit {
11596 range: lsp::Range {
11597 start: lsp::Position {
11598 line: 0,
11599 character: 20,
11600 },
11601 end: lsp::Position {
11602 line: 0,
11603 character: 22,
11604 },
11605 },
11606 new_text: "".to_string(),
11607 }]),
11608 ..Default::default()
11609 };
11610
11611 let closure_completion_item = completion_item.clone();
11612 let counter = Arc::new(AtomicUsize::new(0));
11613 let counter_clone = counter.clone();
11614 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
11615 let task_completion_item = closure_completion_item.clone();
11616 counter_clone.fetch_add(1, atomic::Ordering::Release);
11617 async move {
11618 Ok(Some(lsp::CompletionResponse::Array(vec![
11619 task_completion_item,
11620 ])))
11621 }
11622 });
11623
11624 cx.condition(|editor, _| editor.context_menu_visible())
11625 .await;
11626 cx.assert_editor_state("fn main() { let a = 2.ˇ; }");
11627 assert!(request.next().await.is_some());
11628 assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
11629
11630 cx.simulate_keystrokes("S o m");
11631 cx.condition(|editor, _| editor.context_menu_visible())
11632 .await;
11633 cx.assert_editor_state("fn main() { let a = 2.Somˇ; }");
11634 assert!(request.next().await.is_some());
11635 assert!(request.next().await.is_some());
11636 assert!(request.next().await.is_some());
11637 request.close();
11638 assert!(request.next().await.is_none());
11639 assert_eq!(
11640 counter.load(atomic::Ordering::Acquire),
11641 4,
11642 "With the completions menu open, only one LSP request should happen per input"
11643 );
11644}
11645
11646#[gpui::test]
11647async fn test_toggle_comment(cx: &mut TestAppContext) {
11648 init_test(cx, |_| {});
11649 let mut cx = EditorTestContext::new(cx).await;
11650 let language = Arc::new(Language::new(
11651 LanguageConfig {
11652 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11653 ..Default::default()
11654 },
11655 Some(tree_sitter_rust::LANGUAGE.into()),
11656 ));
11657 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11658
11659 // If multiple selections intersect a line, the line is only toggled once.
11660 cx.set_state(indoc! {"
11661 fn a() {
11662 «//b();
11663 ˇ»// «c();
11664 //ˇ» d();
11665 }
11666 "});
11667
11668 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11669
11670 cx.assert_editor_state(indoc! {"
11671 fn a() {
11672 «b();
11673 c();
11674 ˇ» d();
11675 }
11676 "});
11677
11678 // The comment prefix is inserted at the same column for every line in a
11679 // selection.
11680 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11681
11682 cx.assert_editor_state(indoc! {"
11683 fn a() {
11684 // «b();
11685 // c();
11686 ˇ»// d();
11687 }
11688 "});
11689
11690 // If a selection ends at the beginning of a line, that line is not toggled.
11691 cx.set_selections_state(indoc! {"
11692 fn a() {
11693 // b();
11694 «// c();
11695 ˇ» // d();
11696 }
11697 "});
11698
11699 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11700
11701 cx.assert_editor_state(indoc! {"
11702 fn a() {
11703 // b();
11704 «c();
11705 ˇ» // d();
11706 }
11707 "});
11708
11709 // If a selection span a single line and is empty, the line is toggled.
11710 cx.set_state(indoc! {"
11711 fn a() {
11712 a();
11713 b();
11714 ˇ
11715 }
11716 "});
11717
11718 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11719
11720 cx.assert_editor_state(indoc! {"
11721 fn a() {
11722 a();
11723 b();
11724 //•ˇ
11725 }
11726 "});
11727
11728 // If a selection span multiple lines, empty lines are not toggled.
11729 cx.set_state(indoc! {"
11730 fn a() {
11731 «a();
11732
11733 c();ˇ»
11734 }
11735 "});
11736
11737 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11738
11739 cx.assert_editor_state(indoc! {"
11740 fn a() {
11741 // «a();
11742
11743 // c();ˇ»
11744 }
11745 "});
11746
11747 // If a selection includes multiple comment prefixes, all lines are uncommented.
11748 cx.set_state(indoc! {"
11749 fn a() {
11750 «// a();
11751 /// b();
11752 //! c();ˇ»
11753 }
11754 "});
11755
11756 cx.update_editor(|e, window, cx| e.toggle_comments(&ToggleComments::default(), window, cx));
11757
11758 cx.assert_editor_state(indoc! {"
11759 fn a() {
11760 «a();
11761 b();
11762 c();ˇ»
11763 }
11764 "});
11765}
11766
11767#[gpui::test]
11768async fn test_toggle_comment_ignore_indent(cx: &mut TestAppContext) {
11769 init_test(cx, |_| {});
11770 let mut cx = EditorTestContext::new(cx).await;
11771 let language = Arc::new(Language::new(
11772 LanguageConfig {
11773 line_comments: vec!["// ".into(), "//! ".into(), "/// ".into()],
11774 ..Default::default()
11775 },
11776 Some(tree_sitter_rust::LANGUAGE.into()),
11777 ));
11778 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
11779
11780 let toggle_comments = &ToggleComments {
11781 advance_downwards: false,
11782 ignore_indent: true,
11783 };
11784
11785 // If multiple selections intersect a line, the line is only toggled once.
11786 cx.set_state(indoc! {"
11787 fn a() {
11788 // «b();
11789 // c();
11790 // ˇ» d();
11791 }
11792 "});
11793
11794 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11795
11796 cx.assert_editor_state(indoc! {"
11797 fn a() {
11798 «b();
11799 c();
11800 ˇ» d();
11801 }
11802 "});
11803
11804 // The comment prefix is inserted at the beginning of each line
11805 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11806
11807 cx.assert_editor_state(indoc! {"
11808 fn a() {
11809 // «b();
11810 // c();
11811 // ˇ» d();
11812 }
11813 "});
11814
11815 // If a selection ends at the beginning of a line, that line is not toggled.
11816 cx.set_selections_state(indoc! {"
11817 fn a() {
11818 // b();
11819 // «c();
11820 ˇ»// d();
11821 }
11822 "});
11823
11824 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11825
11826 cx.assert_editor_state(indoc! {"
11827 fn a() {
11828 // b();
11829 «c();
11830 ˇ»// d();
11831 }
11832 "});
11833
11834 // If a selection span a single line and is empty, the line is toggled.
11835 cx.set_state(indoc! {"
11836 fn a() {
11837 a();
11838 b();
11839 ˇ
11840 }
11841 "});
11842
11843 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11844
11845 cx.assert_editor_state(indoc! {"
11846 fn a() {
11847 a();
11848 b();
11849 //ˇ
11850 }
11851 "});
11852
11853 // If a selection span multiple lines, empty lines are not toggled.
11854 cx.set_state(indoc! {"
11855 fn a() {
11856 «a();
11857
11858 c();ˇ»
11859 }
11860 "});
11861
11862 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11863
11864 cx.assert_editor_state(indoc! {"
11865 fn a() {
11866 // «a();
11867
11868 // c();ˇ»
11869 }
11870 "});
11871
11872 // If a selection includes multiple comment prefixes, all lines are uncommented.
11873 cx.set_state(indoc! {"
11874 fn a() {
11875 // «a();
11876 /// b();
11877 //! c();ˇ»
11878 }
11879 "});
11880
11881 cx.update_editor(|e, window, cx| e.toggle_comments(toggle_comments, window, cx));
11882
11883 cx.assert_editor_state(indoc! {"
11884 fn a() {
11885 «a();
11886 b();
11887 c();ˇ»
11888 }
11889 "});
11890}
11891
11892#[gpui::test]
11893async fn test_advance_downward_on_toggle_comment(cx: &mut TestAppContext) {
11894 init_test(cx, |_| {});
11895
11896 let language = Arc::new(Language::new(
11897 LanguageConfig {
11898 line_comments: vec!["// ".into()],
11899 ..Default::default()
11900 },
11901 Some(tree_sitter_rust::LANGUAGE.into()),
11902 ));
11903
11904 let mut cx = EditorTestContext::new(cx).await;
11905
11906 cx.language_registry().add(language.clone());
11907 cx.update_buffer(|buffer, cx| {
11908 buffer.set_language(Some(language), cx);
11909 });
11910
11911 let toggle_comments = &ToggleComments {
11912 advance_downwards: true,
11913 ignore_indent: false,
11914 };
11915
11916 // Single cursor on one line -> advance
11917 // Cursor moves horizontally 3 characters as well on non-blank line
11918 cx.set_state(indoc!(
11919 "fn a() {
11920 ˇdog();
11921 cat();
11922 }"
11923 ));
11924 cx.update_editor(|editor, window, cx| {
11925 editor.toggle_comments(toggle_comments, window, cx);
11926 });
11927 cx.assert_editor_state(indoc!(
11928 "fn a() {
11929 // dog();
11930 catˇ();
11931 }"
11932 ));
11933
11934 // Single selection on one line -> don't advance
11935 cx.set_state(indoc!(
11936 "fn a() {
11937 «dog()ˇ»;
11938 cat();
11939 }"
11940 ));
11941 cx.update_editor(|editor, window, cx| {
11942 editor.toggle_comments(toggle_comments, window, cx);
11943 });
11944 cx.assert_editor_state(indoc!(
11945 "fn a() {
11946 // «dog()ˇ»;
11947 cat();
11948 }"
11949 ));
11950
11951 // Multiple cursors on one line -> advance
11952 cx.set_state(indoc!(
11953 "fn a() {
11954 ˇdˇog();
11955 cat();
11956 }"
11957 ));
11958 cx.update_editor(|editor, window, cx| {
11959 editor.toggle_comments(toggle_comments, window, cx);
11960 });
11961 cx.assert_editor_state(indoc!(
11962 "fn a() {
11963 // dog();
11964 catˇ(ˇ);
11965 }"
11966 ));
11967
11968 // Multiple cursors on one line, with selection -> don't advance
11969 cx.set_state(indoc!(
11970 "fn a() {
11971 ˇdˇog«()ˇ»;
11972 cat();
11973 }"
11974 ));
11975 cx.update_editor(|editor, window, cx| {
11976 editor.toggle_comments(toggle_comments, window, cx);
11977 });
11978 cx.assert_editor_state(indoc!(
11979 "fn a() {
11980 // ˇdˇog«()ˇ»;
11981 cat();
11982 }"
11983 ));
11984
11985 // Single cursor on one line -> advance
11986 // Cursor moves to column 0 on blank line
11987 cx.set_state(indoc!(
11988 "fn a() {
11989 ˇdog();
11990
11991 cat();
11992 }"
11993 ));
11994 cx.update_editor(|editor, window, cx| {
11995 editor.toggle_comments(toggle_comments, window, cx);
11996 });
11997 cx.assert_editor_state(indoc!(
11998 "fn a() {
11999 // dog();
12000 ˇ
12001 cat();
12002 }"
12003 ));
12004
12005 // Single cursor on one line -> advance
12006 // Cursor starts and ends at column 0
12007 cx.set_state(indoc!(
12008 "fn a() {
12009 ˇ dog();
12010 cat();
12011 }"
12012 ));
12013 cx.update_editor(|editor, window, cx| {
12014 editor.toggle_comments(toggle_comments, window, cx);
12015 });
12016 cx.assert_editor_state(indoc!(
12017 "fn a() {
12018 // dog();
12019 ˇ cat();
12020 }"
12021 ));
12022}
12023
12024#[gpui::test]
12025async fn test_toggle_block_comment(cx: &mut TestAppContext) {
12026 init_test(cx, |_| {});
12027
12028 let mut cx = EditorTestContext::new(cx).await;
12029
12030 let html_language = Arc::new(
12031 Language::new(
12032 LanguageConfig {
12033 name: "HTML".into(),
12034 block_comment: Some(("<!-- ".into(), " -->".into())),
12035 ..Default::default()
12036 },
12037 Some(tree_sitter_html::LANGUAGE.into()),
12038 )
12039 .with_injection_query(
12040 r#"
12041 (script_element
12042 (raw_text) @injection.content
12043 (#set! injection.language "javascript"))
12044 "#,
12045 )
12046 .unwrap(),
12047 );
12048
12049 let javascript_language = Arc::new(Language::new(
12050 LanguageConfig {
12051 name: "JavaScript".into(),
12052 line_comments: vec!["// ".into()],
12053 ..Default::default()
12054 },
12055 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
12056 ));
12057
12058 cx.language_registry().add(html_language.clone());
12059 cx.language_registry().add(javascript_language.clone());
12060 cx.update_buffer(|buffer, cx| {
12061 buffer.set_language(Some(html_language), cx);
12062 });
12063
12064 // Toggle comments for empty selections
12065 cx.set_state(
12066 &r#"
12067 <p>A</p>ˇ
12068 <p>B</p>ˇ
12069 <p>C</p>ˇ
12070 "#
12071 .unindent(),
12072 );
12073 cx.update_editor(|editor, window, cx| {
12074 editor.toggle_comments(&ToggleComments::default(), window, cx)
12075 });
12076 cx.assert_editor_state(
12077 &r#"
12078 <!-- <p>A</p>ˇ -->
12079 <!-- <p>B</p>ˇ -->
12080 <!-- <p>C</p>ˇ -->
12081 "#
12082 .unindent(),
12083 );
12084 cx.update_editor(|editor, window, cx| {
12085 editor.toggle_comments(&ToggleComments::default(), window, cx)
12086 });
12087 cx.assert_editor_state(
12088 &r#"
12089 <p>A</p>ˇ
12090 <p>B</p>ˇ
12091 <p>C</p>ˇ
12092 "#
12093 .unindent(),
12094 );
12095
12096 // Toggle comments for mixture of empty and non-empty selections, where
12097 // multiple selections occupy a given line.
12098 cx.set_state(
12099 &r#"
12100 <p>A«</p>
12101 <p>ˇ»B</p>ˇ
12102 <p>C«</p>
12103 <p>ˇ»D</p>ˇ
12104 "#
12105 .unindent(),
12106 );
12107
12108 cx.update_editor(|editor, window, cx| {
12109 editor.toggle_comments(&ToggleComments::default(), window, cx)
12110 });
12111 cx.assert_editor_state(
12112 &r#"
12113 <!-- <p>A«</p>
12114 <p>ˇ»B</p>ˇ -->
12115 <!-- <p>C«</p>
12116 <p>ˇ»D</p>ˇ -->
12117 "#
12118 .unindent(),
12119 );
12120 cx.update_editor(|editor, window, cx| {
12121 editor.toggle_comments(&ToggleComments::default(), window, cx)
12122 });
12123 cx.assert_editor_state(
12124 &r#"
12125 <p>A«</p>
12126 <p>ˇ»B</p>ˇ
12127 <p>C«</p>
12128 <p>ˇ»D</p>ˇ
12129 "#
12130 .unindent(),
12131 );
12132
12133 // Toggle comments when different languages are active for different
12134 // selections.
12135 cx.set_state(
12136 &r#"
12137 ˇ<script>
12138 ˇvar x = new Y();
12139 ˇ</script>
12140 "#
12141 .unindent(),
12142 );
12143 cx.executor().run_until_parked();
12144 cx.update_editor(|editor, window, cx| {
12145 editor.toggle_comments(&ToggleComments::default(), window, cx)
12146 });
12147 // TODO this is how it actually worked in Zed Stable, which is not very ergonomic.
12148 // Uncommenting and commenting from this position brings in even more wrong artifacts.
12149 cx.assert_editor_state(
12150 &r#"
12151 <!-- ˇ<script> -->
12152 // ˇvar x = new Y();
12153 <!-- ˇ</script> -->
12154 "#
12155 .unindent(),
12156 );
12157}
12158
12159#[gpui::test]
12160fn test_editing_disjoint_excerpts(cx: &mut TestAppContext) {
12161 init_test(cx, |_| {});
12162
12163 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12164 let multibuffer = cx.new(|cx| {
12165 let mut multibuffer = MultiBuffer::new(ReadWrite);
12166 multibuffer.push_excerpts(
12167 buffer.clone(),
12168 [
12169 ExcerptRange::new(Point::new(0, 0)..Point::new(0, 4)),
12170 ExcerptRange::new(Point::new(1, 0)..Point::new(1, 4)),
12171 ],
12172 cx,
12173 );
12174 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb");
12175 multibuffer
12176 });
12177
12178 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12179 editor.update_in(cx, |editor, window, cx| {
12180 assert_eq!(editor.text(cx), "aaaa\nbbbb");
12181 editor.change_selections(None, window, cx, |s| {
12182 s.select_ranges([
12183 Point::new(0, 0)..Point::new(0, 0),
12184 Point::new(1, 0)..Point::new(1, 0),
12185 ])
12186 });
12187
12188 editor.handle_input("X", window, cx);
12189 assert_eq!(editor.text(cx), "Xaaaa\nXbbbb");
12190 assert_eq!(
12191 editor.selections.ranges(cx),
12192 [
12193 Point::new(0, 1)..Point::new(0, 1),
12194 Point::new(1, 1)..Point::new(1, 1),
12195 ]
12196 );
12197
12198 // Ensure the cursor's head is respected when deleting across an excerpt boundary.
12199 editor.change_selections(None, window, cx, |s| {
12200 s.select_ranges([Point::new(0, 2)..Point::new(1, 2)])
12201 });
12202 editor.backspace(&Default::default(), window, cx);
12203 assert_eq!(editor.text(cx), "Xa\nbbb");
12204 assert_eq!(
12205 editor.selections.ranges(cx),
12206 [Point::new(1, 0)..Point::new(1, 0)]
12207 );
12208
12209 editor.change_selections(None, window, cx, |s| {
12210 s.select_ranges([Point::new(1, 1)..Point::new(0, 1)])
12211 });
12212 editor.backspace(&Default::default(), window, cx);
12213 assert_eq!(editor.text(cx), "X\nbb");
12214 assert_eq!(
12215 editor.selections.ranges(cx),
12216 [Point::new(0, 1)..Point::new(0, 1)]
12217 );
12218 });
12219}
12220
12221#[gpui::test]
12222fn test_editing_overlapping_excerpts(cx: &mut TestAppContext) {
12223 init_test(cx, |_| {});
12224
12225 let markers = vec![('[', ']').into(), ('(', ')').into()];
12226 let (initial_text, mut excerpt_ranges) = marked_text_ranges_by(
12227 indoc! {"
12228 [aaaa
12229 (bbbb]
12230 cccc)",
12231 },
12232 markers.clone(),
12233 );
12234 let excerpt_ranges = markers.into_iter().map(|marker| {
12235 let context = excerpt_ranges.remove(&marker).unwrap()[0].clone();
12236 ExcerptRange::new(context.clone())
12237 });
12238 let buffer = cx.new(|cx| Buffer::local(initial_text, cx));
12239 let multibuffer = cx.new(|cx| {
12240 let mut multibuffer = MultiBuffer::new(ReadWrite);
12241 multibuffer.push_excerpts(buffer, excerpt_ranges, cx);
12242 multibuffer
12243 });
12244
12245 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(multibuffer, window, cx));
12246 editor.update_in(cx, |editor, window, cx| {
12247 let (expected_text, selection_ranges) = marked_text_ranges(
12248 indoc! {"
12249 aaaa
12250 bˇbbb
12251 bˇbbˇb
12252 cccc"
12253 },
12254 true,
12255 );
12256 assert_eq!(editor.text(cx), expected_text);
12257 editor.change_selections(None, window, cx, |s| s.select_ranges(selection_ranges));
12258
12259 editor.handle_input("X", window, cx);
12260
12261 let (expected_text, expected_selections) = marked_text_ranges(
12262 indoc! {"
12263 aaaa
12264 bXˇbbXb
12265 bXˇbbXˇb
12266 cccc"
12267 },
12268 false,
12269 );
12270 assert_eq!(editor.text(cx), expected_text);
12271 assert_eq!(editor.selections.ranges(cx), expected_selections);
12272
12273 editor.newline(&Newline, window, cx);
12274 let (expected_text, expected_selections) = marked_text_ranges(
12275 indoc! {"
12276 aaaa
12277 bX
12278 ˇbbX
12279 b
12280 bX
12281 ˇbbX
12282 ˇb
12283 cccc"
12284 },
12285 false,
12286 );
12287 assert_eq!(editor.text(cx), expected_text);
12288 assert_eq!(editor.selections.ranges(cx), expected_selections);
12289 });
12290}
12291
12292#[gpui::test]
12293fn test_refresh_selections(cx: &mut TestAppContext) {
12294 init_test(cx, |_| {});
12295
12296 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12297 let mut excerpt1_id = None;
12298 let multibuffer = cx.new(|cx| {
12299 let mut multibuffer = MultiBuffer::new(ReadWrite);
12300 excerpt1_id = multibuffer
12301 .push_excerpts(
12302 buffer.clone(),
12303 [
12304 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12305 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12306 ],
12307 cx,
12308 )
12309 .into_iter()
12310 .next();
12311 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12312 multibuffer
12313 });
12314
12315 let editor = cx.add_window(|window, cx| {
12316 let mut editor = build_editor(multibuffer.clone(), window, cx);
12317 let snapshot = editor.snapshot(window, cx);
12318 editor.change_selections(None, window, cx, |s| {
12319 s.select_ranges([Point::new(1, 3)..Point::new(1, 3)])
12320 });
12321 editor.begin_selection(
12322 Point::new(2, 1).to_display_point(&snapshot),
12323 true,
12324 1,
12325 window,
12326 cx,
12327 );
12328 assert_eq!(
12329 editor.selections.ranges(cx),
12330 [
12331 Point::new(1, 3)..Point::new(1, 3),
12332 Point::new(2, 1)..Point::new(2, 1),
12333 ]
12334 );
12335 editor
12336 });
12337
12338 // Refreshing selections is a no-op when excerpts haven't changed.
12339 _ = editor.update(cx, |editor, window, cx| {
12340 editor.change_selections(None, window, cx, |s| s.refresh());
12341 assert_eq!(
12342 editor.selections.ranges(cx),
12343 [
12344 Point::new(1, 3)..Point::new(1, 3),
12345 Point::new(2, 1)..Point::new(2, 1),
12346 ]
12347 );
12348 });
12349
12350 multibuffer.update(cx, |multibuffer, cx| {
12351 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12352 });
12353 _ = editor.update(cx, |editor, window, cx| {
12354 // Removing an excerpt causes the first selection to become degenerate.
12355 assert_eq!(
12356 editor.selections.ranges(cx),
12357 [
12358 Point::new(0, 0)..Point::new(0, 0),
12359 Point::new(0, 1)..Point::new(0, 1)
12360 ]
12361 );
12362
12363 // Refreshing selections will relocate the first selection to the original buffer
12364 // location.
12365 editor.change_selections(None, window, cx, |s| s.refresh());
12366 assert_eq!(
12367 editor.selections.ranges(cx),
12368 [
12369 Point::new(0, 1)..Point::new(0, 1),
12370 Point::new(0, 3)..Point::new(0, 3)
12371 ]
12372 );
12373 assert!(editor.selections.pending_anchor().is_some());
12374 });
12375}
12376
12377#[gpui::test]
12378fn test_refresh_selections_while_selecting_with_mouse(cx: &mut TestAppContext) {
12379 init_test(cx, |_| {});
12380
12381 let buffer = cx.new(|cx| Buffer::local(sample_text(3, 4, 'a'), cx));
12382 let mut excerpt1_id = None;
12383 let multibuffer = cx.new(|cx| {
12384 let mut multibuffer = MultiBuffer::new(ReadWrite);
12385 excerpt1_id = multibuffer
12386 .push_excerpts(
12387 buffer.clone(),
12388 [
12389 ExcerptRange::new(Point::new(0, 0)..Point::new(1, 4)),
12390 ExcerptRange::new(Point::new(1, 0)..Point::new(2, 4)),
12391 ],
12392 cx,
12393 )
12394 .into_iter()
12395 .next();
12396 assert_eq!(multibuffer.read(cx).text(), "aaaa\nbbbb\nbbbb\ncccc");
12397 multibuffer
12398 });
12399
12400 let editor = cx.add_window(|window, cx| {
12401 let mut editor = build_editor(multibuffer.clone(), window, cx);
12402 let snapshot = editor.snapshot(window, cx);
12403 editor.begin_selection(
12404 Point::new(1, 3).to_display_point(&snapshot),
12405 false,
12406 1,
12407 window,
12408 cx,
12409 );
12410 assert_eq!(
12411 editor.selections.ranges(cx),
12412 [Point::new(1, 3)..Point::new(1, 3)]
12413 );
12414 editor
12415 });
12416
12417 multibuffer.update(cx, |multibuffer, cx| {
12418 multibuffer.remove_excerpts([excerpt1_id.unwrap()], cx);
12419 });
12420 _ = editor.update(cx, |editor, window, cx| {
12421 assert_eq!(
12422 editor.selections.ranges(cx),
12423 [Point::new(0, 0)..Point::new(0, 0)]
12424 );
12425
12426 // Ensure we don't panic when selections are refreshed and that the pending selection is finalized.
12427 editor.change_selections(None, window, cx, |s| s.refresh());
12428 assert_eq!(
12429 editor.selections.ranges(cx),
12430 [Point::new(0, 3)..Point::new(0, 3)]
12431 );
12432 assert!(editor.selections.pending_anchor().is_some());
12433 });
12434}
12435
12436#[gpui::test]
12437async fn test_extra_newline_insertion(cx: &mut TestAppContext) {
12438 init_test(cx, |_| {});
12439
12440 let language = Arc::new(
12441 Language::new(
12442 LanguageConfig {
12443 brackets: BracketPairConfig {
12444 pairs: vec![
12445 BracketPair {
12446 start: "{".to_string(),
12447 end: "}".to_string(),
12448 close: true,
12449 surround: true,
12450 newline: true,
12451 },
12452 BracketPair {
12453 start: "/* ".to_string(),
12454 end: " */".to_string(),
12455 close: true,
12456 surround: true,
12457 newline: true,
12458 },
12459 ],
12460 ..Default::default()
12461 },
12462 ..Default::default()
12463 },
12464 Some(tree_sitter_rust::LANGUAGE.into()),
12465 )
12466 .with_indents_query("")
12467 .unwrap(),
12468 );
12469
12470 let text = concat!(
12471 "{ }\n", //
12472 " x\n", //
12473 " /* */\n", //
12474 "x\n", //
12475 "{{} }\n", //
12476 );
12477
12478 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
12479 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
12480 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
12481 editor
12482 .condition::<crate::EditorEvent>(cx, |editor, cx| !editor.buffer.read(cx).is_parsing(cx))
12483 .await;
12484
12485 editor.update_in(cx, |editor, window, cx| {
12486 editor.change_selections(None, window, cx, |s| {
12487 s.select_display_ranges([
12488 DisplayPoint::new(DisplayRow(0), 2)..DisplayPoint::new(DisplayRow(0), 3),
12489 DisplayPoint::new(DisplayRow(2), 5)..DisplayPoint::new(DisplayRow(2), 5),
12490 DisplayPoint::new(DisplayRow(4), 4)..DisplayPoint::new(DisplayRow(4), 4),
12491 ])
12492 });
12493 editor.newline(&Newline, window, cx);
12494
12495 assert_eq!(
12496 editor.buffer().read(cx).read(cx).text(),
12497 concat!(
12498 "{ \n", // Suppress rustfmt
12499 "\n", //
12500 "}\n", //
12501 " x\n", //
12502 " /* \n", //
12503 " \n", //
12504 " */\n", //
12505 "x\n", //
12506 "{{} \n", //
12507 "}\n", //
12508 )
12509 );
12510 });
12511}
12512
12513#[gpui::test]
12514fn test_highlighted_ranges(cx: &mut TestAppContext) {
12515 init_test(cx, |_| {});
12516
12517 let editor = cx.add_window(|window, cx| {
12518 let buffer = MultiBuffer::build_simple(&sample_text(16, 8, 'a'), cx);
12519 build_editor(buffer.clone(), window, cx)
12520 });
12521
12522 _ = editor.update(cx, |editor, window, cx| {
12523 struct Type1;
12524 struct Type2;
12525
12526 let buffer = editor.buffer.read(cx).snapshot(cx);
12527
12528 let anchor_range =
12529 |range: Range<Point>| buffer.anchor_after(range.start)..buffer.anchor_after(range.end);
12530
12531 editor.highlight_background::<Type1>(
12532 &[
12533 anchor_range(Point::new(2, 1)..Point::new(2, 3)),
12534 anchor_range(Point::new(4, 2)..Point::new(4, 4)),
12535 anchor_range(Point::new(6, 3)..Point::new(6, 5)),
12536 anchor_range(Point::new(8, 4)..Point::new(8, 6)),
12537 ],
12538 |_| Hsla::red(),
12539 cx,
12540 );
12541 editor.highlight_background::<Type2>(
12542 &[
12543 anchor_range(Point::new(3, 2)..Point::new(3, 5)),
12544 anchor_range(Point::new(5, 3)..Point::new(5, 6)),
12545 anchor_range(Point::new(7, 4)..Point::new(7, 7)),
12546 anchor_range(Point::new(9, 5)..Point::new(9, 8)),
12547 ],
12548 |_| Hsla::green(),
12549 cx,
12550 );
12551
12552 let snapshot = editor.snapshot(window, cx);
12553 let mut highlighted_ranges = editor.background_highlights_in_range(
12554 anchor_range(Point::new(3, 4)..Point::new(7, 4)),
12555 &snapshot,
12556 cx.theme().colors(),
12557 );
12558 // Enforce a consistent ordering based on color without relying on the ordering of the
12559 // highlight's `TypeId` which is non-executor.
12560 highlighted_ranges.sort_unstable_by_key(|(_, color)| *color);
12561 assert_eq!(
12562 highlighted_ranges,
12563 &[
12564 (
12565 DisplayPoint::new(DisplayRow(4), 2)..DisplayPoint::new(DisplayRow(4), 4),
12566 Hsla::red(),
12567 ),
12568 (
12569 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12570 Hsla::red(),
12571 ),
12572 (
12573 DisplayPoint::new(DisplayRow(3), 2)..DisplayPoint::new(DisplayRow(3), 5),
12574 Hsla::green(),
12575 ),
12576 (
12577 DisplayPoint::new(DisplayRow(5), 3)..DisplayPoint::new(DisplayRow(5), 6),
12578 Hsla::green(),
12579 ),
12580 ]
12581 );
12582 assert_eq!(
12583 editor.background_highlights_in_range(
12584 anchor_range(Point::new(5, 6)..Point::new(6, 4)),
12585 &snapshot,
12586 cx.theme().colors(),
12587 ),
12588 &[(
12589 DisplayPoint::new(DisplayRow(6), 3)..DisplayPoint::new(DisplayRow(6), 5),
12590 Hsla::red(),
12591 )]
12592 );
12593 });
12594}
12595
12596#[gpui::test]
12597async fn test_following(cx: &mut TestAppContext) {
12598 init_test(cx, |_| {});
12599
12600 let fs = FakeFs::new(cx.executor());
12601 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12602
12603 let buffer = project.update(cx, |project, cx| {
12604 let buffer = project.create_local_buffer(&sample_text(16, 8, 'a'), None, cx);
12605 cx.new(|cx| MultiBuffer::singleton(buffer, cx))
12606 });
12607 let leader = cx.add_window(|window, cx| build_editor(buffer.clone(), window, cx));
12608 let follower = cx.update(|cx| {
12609 cx.open_window(
12610 WindowOptions {
12611 window_bounds: Some(WindowBounds::Windowed(Bounds::from_corners(
12612 gpui::Point::new(px(0.), px(0.)),
12613 gpui::Point::new(px(10.), px(80.)),
12614 ))),
12615 ..Default::default()
12616 },
12617 |window, cx| cx.new(|cx| build_editor(buffer.clone(), window, cx)),
12618 )
12619 .unwrap()
12620 });
12621
12622 let is_still_following = Rc::new(RefCell::new(true));
12623 let follower_edit_event_count = Rc::new(RefCell::new(0));
12624 let pending_update = Rc::new(RefCell::new(None));
12625 let leader_entity = leader.root(cx).unwrap();
12626 let follower_entity = follower.root(cx).unwrap();
12627 _ = follower.update(cx, {
12628 let update = pending_update.clone();
12629 let is_still_following = is_still_following.clone();
12630 let follower_edit_event_count = follower_edit_event_count.clone();
12631 |_, window, cx| {
12632 cx.subscribe_in(
12633 &leader_entity,
12634 window,
12635 move |_, leader, event, window, cx| {
12636 leader.read(cx).add_event_to_update_proto(
12637 event,
12638 &mut update.borrow_mut(),
12639 window,
12640 cx,
12641 );
12642 },
12643 )
12644 .detach();
12645
12646 cx.subscribe_in(
12647 &follower_entity,
12648 window,
12649 move |_, _, event: &EditorEvent, _window, _cx| {
12650 if matches!(Editor::to_follow_event(event), Some(FollowEvent::Unfollow)) {
12651 *is_still_following.borrow_mut() = false;
12652 }
12653
12654 if let EditorEvent::BufferEdited = event {
12655 *follower_edit_event_count.borrow_mut() += 1;
12656 }
12657 },
12658 )
12659 .detach();
12660 }
12661 });
12662
12663 // Update the selections only
12664 _ = leader.update(cx, |leader, window, cx| {
12665 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12666 });
12667 follower
12668 .update(cx, |follower, window, cx| {
12669 follower.apply_update_proto(
12670 &project,
12671 pending_update.borrow_mut().take().unwrap(),
12672 window,
12673 cx,
12674 )
12675 })
12676 .unwrap()
12677 .await
12678 .unwrap();
12679 _ = follower.update(cx, |follower, _, cx| {
12680 assert_eq!(follower.selections.ranges(cx), vec![1..1]);
12681 });
12682 assert!(*is_still_following.borrow());
12683 assert_eq!(*follower_edit_event_count.borrow(), 0);
12684
12685 // Update the scroll position only
12686 _ = leader.update(cx, |leader, window, cx| {
12687 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12688 });
12689 follower
12690 .update(cx, |follower, window, cx| {
12691 follower.apply_update_proto(
12692 &project,
12693 pending_update.borrow_mut().take().unwrap(),
12694 window,
12695 cx,
12696 )
12697 })
12698 .unwrap()
12699 .await
12700 .unwrap();
12701 assert_eq!(
12702 follower
12703 .update(cx, |follower, _, cx| follower.scroll_position(cx))
12704 .unwrap(),
12705 gpui::Point::new(1.5, 3.5)
12706 );
12707 assert!(*is_still_following.borrow());
12708 assert_eq!(*follower_edit_event_count.borrow(), 0);
12709
12710 // Update the selections and scroll position. The follower's scroll position is updated
12711 // via autoscroll, not via the leader's exact scroll position.
12712 _ = leader.update(cx, |leader, window, cx| {
12713 leader.change_selections(None, window, cx, |s| s.select_ranges([0..0]));
12714 leader.request_autoscroll(Autoscroll::newest(), cx);
12715 leader.set_scroll_position(gpui::Point::new(1.5, 3.5), window, cx);
12716 });
12717 follower
12718 .update(cx, |follower, window, cx| {
12719 follower.apply_update_proto(
12720 &project,
12721 pending_update.borrow_mut().take().unwrap(),
12722 window,
12723 cx,
12724 )
12725 })
12726 .unwrap()
12727 .await
12728 .unwrap();
12729 _ = follower.update(cx, |follower, _, cx| {
12730 assert_eq!(follower.scroll_position(cx), gpui::Point::new(1.5, 0.0));
12731 assert_eq!(follower.selections.ranges(cx), vec![0..0]);
12732 });
12733 assert!(*is_still_following.borrow());
12734
12735 // Creating a pending selection that precedes another selection
12736 _ = leader.update(cx, |leader, window, cx| {
12737 leader.change_selections(None, window, cx, |s| s.select_ranges([1..1]));
12738 leader.begin_selection(DisplayPoint::new(DisplayRow(0), 0), true, 1, window, cx);
12739 });
12740 follower
12741 .update(cx, |follower, window, cx| {
12742 follower.apply_update_proto(
12743 &project,
12744 pending_update.borrow_mut().take().unwrap(),
12745 window,
12746 cx,
12747 )
12748 })
12749 .unwrap()
12750 .await
12751 .unwrap();
12752 _ = follower.update(cx, |follower, _, cx| {
12753 assert_eq!(follower.selections.ranges(cx), vec![0..0, 1..1]);
12754 });
12755 assert!(*is_still_following.borrow());
12756
12757 // Extend the pending selection so that it surrounds another selection
12758 _ = leader.update(cx, |leader, window, cx| {
12759 leader.extend_selection(DisplayPoint::new(DisplayRow(0), 2), 1, window, cx);
12760 });
12761 follower
12762 .update(cx, |follower, window, cx| {
12763 follower.apply_update_proto(
12764 &project,
12765 pending_update.borrow_mut().take().unwrap(),
12766 window,
12767 cx,
12768 )
12769 })
12770 .unwrap()
12771 .await
12772 .unwrap();
12773 _ = follower.update(cx, |follower, _, cx| {
12774 assert_eq!(follower.selections.ranges(cx), vec![0..2]);
12775 });
12776
12777 // Scrolling locally breaks the follow
12778 _ = follower.update(cx, |follower, window, cx| {
12779 let top_anchor = follower.buffer().read(cx).read(cx).anchor_after(0);
12780 follower.set_scroll_anchor(
12781 ScrollAnchor {
12782 anchor: top_anchor,
12783 offset: gpui::Point::new(0.0, 0.5),
12784 },
12785 window,
12786 cx,
12787 );
12788 });
12789 assert!(!(*is_still_following.borrow()));
12790}
12791
12792#[gpui::test]
12793async fn test_following_with_multiple_excerpts(cx: &mut TestAppContext) {
12794 init_test(cx, |_| {});
12795
12796 let fs = FakeFs::new(cx.executor());
12797 let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
12798 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
12799 let pane = workspace
12800 .update(cx, |workspace, _, _| workspace.active_pane().clone())
12801 .unwrap();
12802
12803 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
12804
12805 let leader = pane.update_in(cx, |_, window, cx| {
12806 let multibuffer = cx.new(|_| MultiBuffer::new(ReadWrite));
12807 cx.new(|cx| build_editor(multibuffer.clone(), window, cx))
12808 });
12809
12810 // Start following the editor when it has no excerpts.
12811 let mut state_message =
12812 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12813 let workspace_entity = workspace.root(cx).unwrap();
12814 let follower_1 = cx
12815 .update_window(*workspace.deref(), |_, window, cx| {
12816 Editor::from_state_proto(
12817 workspace_entity,
12818 ViewId {
12819 creator: CollaboratorId::PeerId(PeerId::default()),
12820 id: 0,
12821 },
12822 &mut state_message,
12823 window,
12824 cx,
12825 )
12826 })
12827 .unwrap()
12828 .unwrap()
12829 .await
12830 .unwrap();
12831
12832 let update_message = Rc::new(RefCell::new(None));
12833 follower_1.update_in(cx, {
12834 let update = update_message.clone();
12835 |_, window, cx| {
12836 cx.subscribe_in(&leader, window, move |_, leader, event, window, cx| {
12837 leader.read(cx).add_event_to_update_proto(
12838 event,
12839 &mut update.borrow_mut(),
12840 window,
12841 cx,
12842 );
12843 })
12844 .detach();
12845 }
12846 });
12847
12848 let (buffer_1, buffer_2) = project.update(cx, |project, cx| {
12849 (
12850 project.create_local_buffer("abc\ndef\nghi\njkl\n", None, cx),
12851 project.create_local_buffer("mno\npqr\nstu\nvwx\n", None, cx),
12852 )
12853 });
12854
12855 // Insert some excerpts.
12856 leader.update(cx, |leader, cx| {
12857 leader.buffer.update(cx, |multibuffer, cx| {
12858 multibuffer.set_excerpts_for_path(
12859 PathKey::namespaced(1, Arc::from(Path::new("b.txt"))),
12860 buffer_1.clone(),
12861 vec![
12862 Point::row_range(0..3),
12863 Point::row_range(1..6),
12864 Point::row_range(12..15),
12865 ],
12866 0,
12867 cx,
12868 );
12869 multibuffer.set_excerpts_for_path(
12870 PathKey::namespaced(1, Arc::from(Path::new("a.txt"))),
12871 buffer_2.clone(),
12872 vec![Point::row_range(0..6), Point::row_range(8..12)],
12873 0,
12874 cx,
12875 );
12876 });
12877 });
12878
12879 // Apply the update of adding the excerpts.
12880 follower_1
12881 .update_in(cx, |follower, window, cx| {
12882 follower.apply_update_proto(
12883 &project,
12884 update_message.borrow().clone().unwrap(),
12885 window,
12886 cx,
12887 )
12888 })
12889 .await
12890 .unwrap();
12891 assert_eq!(
12892 follower_1.update(cx, |editor, cx| editor.text(cx)),
12893 leader.update(cx, |editor, cx| editor.text(cx))
12894 );
12895 update_message.borrow_mut().take();
12896
12897 // Start following separately after it already has excerpts.
12898 let mut state_message =
12899 leader.update_in(cx, |leader, window, cx| leader.to_state_proto(window, cx));
12900 let workspace_entity = workspace.root(cx).unwrap();
12901 let follower_2 = cx
12902 .update_window(*workspace.deref(), |_, window, cx| {
12903 Editor::from_state_proto(
12904 workspace_entity,
12905 ViewId {
12906 creator: CollaboratorId::PeerId(PeerId::default()),
12907 id: 0,
12908 },
12909 &mut state_message,
12910 window,
12911 cx,
12912 )
12913 })
12914 .unwrap()
12915 .unwrap()
12916 .await
12917 .unwrap();
12918 assert_eq!(
12919 follower_2.update(cx, |editor, cx| editor.text(cx)),
12920 leader.update(cx, |editor, cx| editor.text(cx))
12921 );
12922
12923 // Remove some excerpts.
12924 leader.update(cx, |leader, cx| {
12925 leader.buffer.update(cx, |multibuffer, cx| {
12926 let excerpt_ids = multibuffer.excerpt_ids();
12927 multibuffer.remove_excerpts([excerpt_ids[1], excerpt_ids[2]], cx);
12928 multibuffer.remove_excerpts([excerpt_ids[0]], cx);
12929 });
12930 });
12931
12932 // Apply the update of removing the excerpts.
12933 follower_1
12934 .update_in(cx, |follower, window, cx| {
12935 follower.apply_update_proto(
12936 &project,
12937 update_message.borrow().clone().unwrap(),
12938 window,
12939 cx,
12940 )
12941 })
12942 .await
12943 .unwrap();
12944 follower_2
12945 .update_in(cx, |follower, window, cx| {
12946 follower.apply_update_proto(
12947 &project,
12948 update_message.borrow().clone().unwrap(),
12949 window,
12950 cx,
12951 )
12952 })
12953 .await
12954 .unwrap();
12955 update_message.borrow_mut().take();
12956 assert_eq!(
12957 follower_1.update(cx, |editor, cx| editor.text(cx)),
12958 leader.update(cx, |editor, cx| editor.text(cx))
12959 );
12960}
12961
12962#[gpui::test]
12963async fn go_to_prev_overlapping_diagnostic(executor: BackgroundExecutor, cx: &mut TestAppContext) {
12964 init_test(cx, |_| {});
12965
12966 let mut cx = EditorTestContext::new(cx).await;
12967 let lsp_store =
12968 cx.update_editor(|editor, _, cx| editor.project.as_ref().unwrap().read(cx).lsp_store());
12969
12970 cx.set_state(indoc! {"
12971 ˇfn func(abc def: i32) -> u32 {
12972 }
12973 "});
12974
12975 cx.update(|_, cx| {
12976 lsp_store.update(cx, |lsp_store, cx| {
12977 lsp_store
12978 .update_diagnostics(
12979 LanguageServerId(0),
12980 lsp::PublishDiagnosticsParams {
12981 uri: lsp::Url::from_file_path(path!("/root/file")).unwrap(),
12982 version: None,
12983 diagnostics: vec![
12984 lsp::Diagnostic {
12985 range: lsp::Range::new(
12986 lsp::Position::new(0, 11),
12987 lsp::Position::new(0, 12),
12988 ),
12989 severity: Some(lsp::DiagnosticSeverity::ERROR),
12990 ..Default::default()
12991 },
12992 lsp::Diagnostic {
12993 range: lsp::Range::new(
12994 lsp::Position::new(0, 12),
12995 lsp::Position::new(0, 15),
12996 ),
12997 severity: Some(lsp::DiagnosticSeverity::ERROR),
12998 ..Default::default()
12999 },
13000 lsp::Diagnostic {
13001 range: lsp::Range::new(
13002 lsp::Position::new(0, 25),
13003 lsp::Position::new(0, 28),
13004 ),
13005 severity: Some(lsp::DiagnosticSeverity::ERROR),
13006 ..Default::default()
13007 },
13008 ],
13009 },
13010 &[],
13011 cx,
13012 )
13013 .unwrap()
13014 });
13015 });
13016
13017 executor.run_until_parked();
13018
13019 cx.update_editor(|editor, window, cx| {
13020 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13021 });
13022
13023 cx.assert_editor_state(indoc! {"
13024 fn func(abc def: i32) -> ˇu32 {
13025 }
13026 "});
13027
13028 cx.update_editor(|editor, window, cx| {
13029 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13030 });
13031
13032 cx.assert_editor_state(indoc! {"
13033 fn func(abc ˇdef: i32) -> u32 {
13034 }
13035 "});
13036
13037 cx.update_editor(|editor, window, cx| {
13038 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13039 });
13040
13041 cx.assert_editor_state(indoc! {"
13042 fn func(abcˇ def: i32) -> u32 {
13043 }
13044 "});
13045
13046 cx.update_editor(|editor, window, cx| {
13047 editor.go_to_prev_diagnostic(&GoToPreviousDiagnostic, window, cx);
13048 });
13049
13050 cx.assert_editor_state(indoc! {"
13051 fn func(abc def: i32) -> ˇu32 {
13052 }
13053 "});
13054}
13055
13056#[gpui::test]
13057async fn test_go_to_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
13058 init_test(cx, |_| {});
13059
13060 let mut cx = EditorTestContext::new(cx).await;
13061
13062 let diff_base = r#"
13063 use some::mod;
13064
13065 const A: u32 = 42;
13066
13067 fn main() {
13068 println!("hello");
13069
13070 println!("world");
13071 }
13072 "#
13073 .unindent();
13074
13075 // Edits are modified, removed, modified, added
13076 cx.set_state(
13077 &r#"
13078 use some::modified;
13079
13080 ˇ
13081 fn main() {
13082 println!("hello there");
13083
13084 println!("around the");
13085 println!("world");
13086 }
13087 "#
13088 .unindent(),
13089 );
13090
13091 cx.set_head_text(&diff_base);
13092 executor.run_until_parked();
13093
13094 cx.update_editor(|editor, window, cx| {
13095 //Wrap around the bottom of the buffer
13096 for _ in 0..3 {
13097 editor.go_to_next_hunk(&GoToHunk, window, cx);
13098 }
13099 });
13100
13101 cx.assert_editor_state(
13102 &r#"
13103 ˇuse some::modified;
13104
13105
13106 fn main() {
13107 println!("hello there");
13108
13109 println!("around the");
13110 println!("world");
13111 }
13112 "#
13113 .unindent(),
13114 );
13115
13116 cx.update_editor(|editor, window, cx| {
13117 //Wrap around the top of the buffer
13118 for _ in 0..2 {
13119 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13120 }
13121 });
13122
13123 cx.assert_editor_state(
13124 &r#"
13125 use some::modified;
13126
13127
13128 fn main() {
13129 ˇ println!("hello there");
13130
13131 println!("around the");
13132 println!("world");
13133 }
13134 "#
13135 .unindent(),
13136 );
13137
13138 cx.update_editor(|editor, window, cx| {
13139 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13140 });
13141
13142 cx.assert_editor_state(
13143 &r#"
13144 use some::modified;
13145
13146 ˇ
13147 fn main() {
13148 println!("hello there");
13149
13150 println!("around the");
13151 println!("world");
13152 }
13153 "#
13154 .unindent(),
13155 );
13156
13157 cx.update_editor(|editor, window, cx| {
13158 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13159 });
13160
13161 cx.assert_editor_state(
13162 &r#"
13163 ˇuse some::modified;
13164
13165
13166 fn main() {
13167 println!("hello there");
13168
13169 println!("around the");
13170 println!("world");
13171 }
13172 "#
13173 .unindent(),
13174 );
13175
13176 cx.update_editor(|editor, window, cx| {
13177 for _ in 0..2 {
13178 editor.go_to_prev_hunk(&GoToPreviousHunk, window, cx);
13179 }
13180 });
13181
13182 cx.assert_editor_state(
13183 &r#"
13184 use some::modified;
13185
13186
13187 fn main() {
13188 ˇ println!("hello there");
13189
13190 println!("around the");
13191 println!("world");
13192 }
13193 "#
13194 .unindent(),
13195 );
13196
13197 cx.update_editor(|editor, window, cx| {
13198 editor.fold(&Fold, window, cx);
13199 });
13200
13201 cx.update_editor(|editor, window, cx| {
13202 editor.go_to_next_hunk(&GoToHunk, window, cx);
13203 });
13204
13205 cx.assert_editor_state(
13206 &r#"
13207 ˇuse some::modified;
13208
13209
13210 fn main() {
13211 println!("hello there");
13212
13213 println!("around the");
13214 println!("world");
13215 }
13216 "#
13217 .unindent(),
13218 );
13219}
13220
13221#[test]
13222fn test_split_words() {
13223 fn split(text: &str) -> Vec<&str> {
13224 split_words(text).collect()
13225 }
13226
13227 assert_eq!(split("HelloWorld"), &["Hello", "World"]);
13228 assert_eq!(split("hello_world"), &["hello_", "world"]);
13229 assert_eq!(split("_hello_world_"), &["_", "hello_", "world_"]);
13230 assert_eq!(split("Hello_World"), &["Hello_", "World"]);
13231 assert_eq!(split("helloWOrld"), &["hello", "WOrld"]);
13232 assert_eq!(split("helloworld"), &["helloworld"]);
13233
13234 assert_eq!(split(":do_the_thing"), &[":", "do_", "the_", "thing"]);
13235}
13236
13237#[gpui::test]
13238async fn test_move_to_enclosing_bracket(cx: &mut TestAppContext) {
13239 init_test(cx, |_| {});
13240
13241 let mut cx = EditorLspTestContext::new_typescript(Default::default(), cx).await;
13242 let mut assert = |before, after| {
13243 let _state_context = cx.set_state(before);
13244 cx.run_until_parked();
13245 cx.update_editor(|editor, window, cx| {
13246 editor.move_to_enclosing_bracket(&MoveToEnclosingBracket, window, cx)
13247 });
13248 cx.run_until_parked();
13249 cx.assert_editor_state(after);
13250 };
13251
13252 // Outside bracket jumps to outside of matching bracket
13253 assert("console.logˇ(var);", "console.log(var)ˇ;");
13254 assert("console.log(var)ˇ;", "console.logˇ(var);");
13255
13256 // Inside bracket jumps to inside of matching bracket
13257 assert("console.log(ˇvar);", "console.log(varˇ);");
13258 assert("console.log(varˇ);", "console.log(ˇvar);");
13259
13260 // When outside a bracket and inside, favor jumping to the inside bracket
13261 assert(
13262 "console.log('foo', [1, 2, 3]ˇ);",
13263 "console.log(ˇ'foo', [1, 2, 3]);",
13264 );
13265 assert(
13266 "console.log(ˇ'foo', [1, 2, 3]);",
13267 "console.log('foo', [1, 2, 3]ˇ);",
13268 );
13269
13270 // Bias forward if two options are equally likely
13271 assert(
13272 "let result = curried_fun()ˇ();",
13273 "let result = curried_fun()()ˇ;",
13274 );
13275
13276 // If directly adjacent to a smaller pair but inside a larger (not adjacent), pick the smaller
13277 assert(
13278 indoc! {"
13279 function test() {
13280 console.log('test')ˇ
13281 }"},
13282 indoc! {"
13283 function test() {
13284 console.logˇ('test')
13285 }"},
13286 );
13287}
13288
13289#[gpui::test]
13290async fn test_on_type_formatting_not_triggered(cx: &mut TestAppContext) {
13291 init_test(cx, |_| {});
13292
13293 let fs = FakeFs::new(cx.executor());
13294 fs.insert_tree(
13295 path!("/a"),
13296 json!({
13297 "main.rs": "fn main() { let a = 5; }",
13298 "other.rs": "// Test file",
13299 }),
13300 )
13301 .await;
13302 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13303
13304 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13305 language_registry.add(Arc::new(Language::new(
13306 LanguageConfig {
13307 name: "Rust".into(),
13308 matcher: LanguageMatcher {
13309 path_suffixes: vec!["rs".to_string()],
13310 ..Default::default()
13311 },
13312 brackets: BracketPairConfig {
13313 pairs: vec![BracketPair {
13314 start: "{".to_string(),
13315 end: "}".to_string(),
13316 close: true,
13317 surround: true,
13318 newline: true,
13319 }],
13320 disabled_scopes_by_bracket_ix: Vec::new(),
13321 },
13322 ..Default::default()
13323 },
13324 Some(tree_sitter_rust::LANGUAGE.into()),
13325 )));
13326 let mut fake_servers = language_registry.register_fake_lsp(
13327 "Rust",
13328 FakeLspAdapter {
13329 capabilities: lsp::ServerCapabilities {
13330 document_on_type_formatting_provider: Some(lsp::DocumentOnTypeFormattingOptions {
13331 first_trigger_character: "{".to_string(),
13332 more_trigger_character: None,
13333 }),
13334 ..Default::default()
13335 },
13336 ..Default::default()
13337 },
13338 );
13339
13340 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13341
13342 let cx = &mut VisualTestContext::from_window(*workspace, cx);
13343
13344 let worktree_id = workspace
13345 .update(cx, |workspace, _, cx| {
13346 workspace.project().update(cx, |project, cx| {
13347 project.worktrees(cx).next().unwrap().read(cx).id()
13348 })
13349 })
13350 .unwrap();
13351
13352 let buffer = project
13353 .update(cx, |project, cx| {
13354 project.open_local_buffer(path!("/a/main.rs"), cx)
13355 })
13356 .await
13357 .unwrap();
13358 let editor_handle = workspace
13359 .update(cx, |workspace, window, cx| {
13360 workspace.open_path((worktree_id, "main.rs"), None, true, window, cx)
13361 })
13362 .unwrap()
13363 .await
13364 .unwrap()
13365 .downcast::<Editor>()
13366 .unwrap();
13367
13368 cx.executor().start_waiting();
13369 let fake_server = fake_servers.next().await.unwrap();
13370
13371 fake_server.set_request_handler::<lsp::request::OnTypeFormatting, _, _>(
13372 |params, _| async move {
13373 assert_eq!(
13374 params.text_document_position.text_document.uri,
13375 lsp::Url::from_file_path(path!("/a/main.rs")).unwrap(),
13376 );
13377 assert_eq!(
13378 params.text_document_position.position,
13379 lsp::Position::new(0, 21),
13380 );
13381
13382 Ok(Some(vec![lsp::TextEdit {
13383 new_text: "]".to_string(),
13384 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13385 }]))
13386 },
13387 );
13388
13389 editor_handle.update_in(cx, |editor, window, cx| {
13390 window.focus(&editor.focus_handle(cx));
13391 editor.change_selections(None, window, cx, |s| {
13392 s.select_ranges([Point::new(0, 21)..Point::new(0, 20)])
13393 });
13394 editor.handle_input("{", window, cx);
13395 });
13396
13397 cx.executor().run_until_parked();
13398
13399 buffer.update(cx, |buffer, _| {
13400 assert_eq!(
13401 buffer.text(),
13402 "fn main() { let a = {5}; }",
13403 "No extra braces from on type formatting should appear in the buffer"
13404 )
13405 });
13406}
13407
13408#[gpui::test]
13409async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppContext) {
13410 init_test(cx, |_| {});
13411
13412 let fs = FakeFs::new(cx.executor());
13413 fs.insert_tree(
13414 path!("/a"),
13415 json!({
13416 "main.rs": "fn main() { let a = 5; }",
13417 "other.rs": "// Test file",
13418 }),
13419 )
13420 .await;
13421
13422 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
13423
13424 let server_restarts = Arc::new(AtomicUsize::new(0));
13425 let closure_restarts = Arc::clone(&server_restarts);
13426 let language_server_name = "test language server";
13427 let language_name: LanguageName = "Rust".into();
13428
13429 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
13430 language_registry.add(Arc::new(Language::new(
13431 LanguageConfig {
13432 name: language_name.clone(),
13433 matcher: LanguageMatcher {
13434 path_suffixes: vec!["rs".to_string()],
13435 ..Default::default()
13436 },
13437 ..Default::default()
13438 },
13439 Some(tree_sitter_rust::LANGUAGE.into()),
13440 )));
13441 let mut fake_servers = language_registry.register_fake_lsp(
13442 "Rust",
13443 FakeLspAdapter {
13444 name: language_server_name,
13445 initialization_options: Some(json!({
13446 "testOptionValue": true
13447 })),
13448 initializer: Some(Box::new(move |fake_server| {
13449 let task_restarts = Arc::clone(&closure_restarts);
13450 fake_server.set_request_handler::<lsp::request::Shutdown, _, _>(move |_, _| {
13451 task_restarts.fetch_add(1, atomic::Ordering::Release);
13452 futures::future::ready(Ok(()))
13453 });
13454 })),
13455 ..Default::default()
13456 },
13457 );
13458
13459 let _window = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
13460 let _buffer = project
13461 .update(cx, |project, cx| {
13462 project.open_local_buffer_with_lsp(path!("/a/main.rs"), cx)
13463 })
13464 .await
13465 .unwrap();
13466 let _fake_server = fake_servers.next().await.unwrap();
13467 update_test_language_settings(cx, |language_settings| {
13468 language_settings.languages.insert(
13469 language_name.clone(),
13470 LanguageSettingsContent {
13471 tab_size: NonZeroU32::new(8),
13472 ..Default::default()
13473 },
13474 );
13475 });
13476 cx.executor().run_until_parked();
13477 assert_eq!(
13478 server_restarts.load(atomic::Ordering::Acquire),
13479 0,
13480 "Should not restart LSP server on an unrelated change"
13481 );
13482
13483 update_test_project_settings(cx, |project_settings| {
13484 project_settings.lsp.insert(
13485 "Some other server name".into(),
13486 LspSettings {
13487 binary: None,
13488 settings: None,
13489 initialization_options: Some(json!({
13490 "some other init value": false
13491 })),
13492 enable_lsp_tasks: false,
13493 },
13494 );
13495 });
13496 cx.executor().run_until_parked();
13497 assert_eq!(
13498 server_restarts.load(atomic::Ordering::Acquire),
13499 0,
13500 "Should not restart LSP server on an unrelated LSP settings change"
13501 );
13502
13503 update_test_project_settings(cx, |project_settings| {
13504 project_settings.lsp.insert(
13505 language_server_name.into(),
13506 LspSettings {
13507 binary: None,
13508 settings: None,
13509 initialization_options: Some(json!({
13510 "anotherInitValue": false
13511 })),
13512 enable_lsp_tasks: false,
13513 },
13514 );
13515 });
13516 cx.executor().run_until_parked();
13517 assert_eq!(
13518 server_restarts.load(atomic::Ordering::Acquire),
13519 1,
13520 "Should restart LSP server on a related LSP settings change"
13521 );
13522
13523 update_test_project_settings(cx, |project_settings| {
13524 project_settings.lsp.insert(
13525 language_server_name.into(),
13526 LspSettings {
13527 binary: None,
13528 settings: None,
13529 initialization_options: Some(json!({
13530 "anotherInitValue": false
13531 })),
13532 enable_lsp_tasks: false,
13533 },
13534 );
13535 });
13536 cx.executor().run_until_parked();
13537 assert_eq!(
13538 server_restarts.load(atomic::Ordering::Acquire),
13539 1,
13540 "Should not restart LSP server on a related LSP settings change that is the same"
13541 );
13542
13543 update_test_project_settings(cx, |project_settings| {
13544 project_settings.lsp.insert(
13545 language_server_name.into(),
13546 LspSettings {
13547 binary: None,
13548 settings: None,
13549 initialization_options: None,
13550 enable_lsp_tasks: false,
13551 },
13552 );
13553 });
13554 cx.executor().run_until_parked();
13555 assert_eq!(
13556 server_restarts.load(atomic::Ordering::Acquire),
13557 2,
13558 "Should restart LSP server on another related LSP settings change"
13559 );
13560}
13561
13562#[gpui::test]
13563async fn test_completions_with_additional_edits(cx: &mut TestAppContext) {
13564 init_test(cx, |_| {});
13565
13566 let mut cx = EditorLspTestContext::new_rust(
13567 lsp::ServerCapabilities {
13568 completion_provider: Some(lsp::CompletionOptions {
13569 trigger_characters: Some(vec![".".to_string()]),
13570 resolve_provider: Some(true),
13571 ..Default::default()
13572 }),
13573 ..Default::default()
13574 },
13575 cx,
13576 )
13577 .await;
13578
13579 cx.set_state("fn main() { let a = 2ˇ; }");
13580 cx.simulate_keystroke(".");
13581 let completion_item = lsp::CompletionItem {
13582 label: "some".into(),
13583 kind: Some(lsp::CompletionItemKind::SNIPPET),
13584 detail: Some("Wrap the expression in an `Option::Some`".to_string()),
13585 documentation: Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
13586 kind: lsp::MarkupKind::Markdown,
13587 value: "```rust\nSome(2)\n```".to_string(),
13588 })),
13589 deprecated: Some(false),
13590 sort_text: Some("fffffff2".to_string()),
13591 filter_text: Some("some".to_string()),
13592 insert_text_format: Some(lsp::InsertTextFormat::SNIPPET),
13593 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13594 range: lsp::Range {
13595 start: lsp::Position {
13596 line: 0,
13597 character: 22,
13598 },
13599 end: lsp::Position {
13600 line: 0,
13601 character: 22,
13602 },
13603 },
13604 new_text: "Some(2)".to_string(),
13605 })),
13606 additional_text_edits: Some(vec![lsp::TextEdit {
13607 range: lsp::Range {
13608 start: lsp::Position {
13609 line: 0,
13610 character: 20,
13611 },
13612 end: lsp::Position {
13613 line: 0,
13614 character: 22,
13615 },
13616 },
13617 new_text: "".to_string(),
13618 }]),
13619 ..Default::default()
13620 };
13621
13622 let closure_completion_item = completion_item.clone();
13623 let mut request = cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13624 let task_completion_item = closure_completion_item.clone();
13625 async move {
13626 Ok(Some(lsp::CompletionResponse::Array(vec![
13627 task_completion_item,
13628 ])))
13629 }
13630 });
13631
13632 request.next().await;
13633
13634 cx.condition(|editor, _| editor.context_menu_visible())
13635 .await;
13636 let apply_additional_edits = cx.update_editor(|editor, window, cx| {
13637 editor
13638 .confirm_completion(&ConfirmCompletion::default(), window, cx)
13639 .unwrap()
13640 });
13641 cx.assert_editor_state("fn main() { let a = 2.Some(2)ˇ; }");
13642
13643 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
13644 let task_completion_item = completion_item.clone();
13645 async move { Ok(task_completion_item) }
13646 })
13647 .next()
13648 .await
13649 .unwrap();
13650 apply_additional_edits.await.unwrap();
13651 cx.assert_editor_state("fn main() { let a = Some(2)ˇ; }");
13652}
13653
13654#[gpui::test]
13655async fn test_completions_resolve_updates_labels_if_filter_text_matches(cx: &mut TestAppContext) {
13656 init_test(cx, |_| {});
13657
13658 let mut cx = EditorLspTestContext::new_rust(
13659 lsp::ServerCapabilities {
13660 completion_provider: Some(lsp::CompletionOptions {
13661 trigger_characters: Some(vec![".".to_string()]),
13662 resolve_provider: Some(true),
13663 ..Default::default()
13664 }),
13665 ..Default::default()
13666 },
13667 cx,
13668 )
13669 .await;
13670
13671 cx.set_state("fn main() { let a = 2ˇ; }");
13672 cx.simulate_keystroke(".");
13673
13674 let item1 = lsp::CompletionItem {
13675 label: "method id()".to_string(),
13676 filter_text: Some("id".to_string()),
13677 detail: None,
13678 documentation: None,
13679 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13680 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13681 new_text: ".id".to_string(),
13682 })),
13683 ..lsp::CompletionItem::default()
13684 };
13685
13686 let item2 = lsp::CompletionItem {
13687 label: "other".to_string(),
13688 filter_text: Some("other".to_string()),
13689 detail: None,
13690 documentation: None,
13691 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13692 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13693 new_text: ".other".to_string(),
13694 })),
13695 ..lsp::CompletionItem::default()
13696 };
13697
13698 let item1 = item1.clone();
13699 cx.set_request_handler::<lsp::request::Completion, _, _>({
13700 let item1 = item1.clone();
13701 move |_, _, _| {
13702 let item1 = item1.clone();
13703 let item2 = item2.clone();
13704 async move { Ok(Some(lsp::CompletionResponse::Array(vec![item1, item2]))) }
13705 }
13706 })
13707 .next()
13708 .await;
13709
13710 cx.condition(|editor, _| editor.context_menu_visible())
13711 .await;
13712 cx.update_editor(|editor, _, _| {
13713 let context_menu = editor.context_menu.borrow_mut();
13714 let context_menu = context_menu
13715 .as_ref()
13716 .expect("Should have the context menu deployed");
13717 match context_menu {
13718 CodeContextMenu::Completions(completions_menu) => {
13719 let completions = completions_menu.completions.borrow_mut();
13720 assert_eq!(
13721 completions
13722 .iter()
13723 .map(|completion| &completion.label.text)
13724 .collect::<Vec<_>>(),
13725 vec!["method id()", "other"]
13726 )
13727 }
13728 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13729 }
13730 });
13731
13732 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>({
13733 let item1 = item1.clone();
13734 move |_, item_to_resolve, _| {
13735 let item1 = item1.clone();
13736 async move {
13737 if item1 == item_to_resolve {
13738 Ok(lsp::CompletionItem {
13739 label: "method id()".to_string(),
13740 filter_text: Some("id".to_string()),
13741 detail: Some("Now resolved!".to_string()),
13742 documentation: Some(lsp::Documentation::String("Docs".to_string())),
13743 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13744 range: lsp::Range::new(
13745 lsp::Position::new(0, 22),
13746 lsp::Position::new(0, 22),
13747 ),
13748 new_text: ".id".to_string(),
13749 })),
13750 ..lsp::CompletionItem::default()
13751 })
13752 } else {
13753 Ok(item_to_resolve)
13754 }
13755 }
13756 }
13757 })
13758 .next()
13759 .await
13760 .unwrap();
13761 cx.run_until_parked();
13762
13763 cx.update_editor(|editor, window, cx| {
13764 editor.context_menu_next(&Default::default(), window, cx);
13765 });
13766
13767 cx.update_editor(|editor, _, _| {
13768 let context_menu = editor.context_menu.borrow_mut();
13769 let context_menu = context_menu
13770 .as_ref()
13771 .expect("Should have the context menu deployed");
13772 match context_menu {
13773 CodeContextMenu::Completions(completions_menu) => {
13774 let completions = completions_menu.completions.borrow_mut();
13775 assert_eq!(
13776 completions
13777 .iter()
13778 .map(|completion| &completion.label.text)
13779 .collect::<Vec<_>>(),
13780 vec!["method id() Now resolved!", "other"],
13781 "Should update first completion label, but not second as the filter text did not match."
13782 );
13783 }
13784 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13785 }
13786 });
13787}
13788
13789#[gpui::test]
13790async fn test_completions_resolve_happens_once(cx: &mut TestAppContext) {
13791 init_test(cx, |_| {});
13792
13793 let mut cx = EditorLspTestContext::new_rust(
13794 lsp::ServerCapabilities {
13795 completion_provider: Some(lsp::CompletionOptions {
13796 trigger_characters: Some(vec![".".to_string()]),
13797 resolve_provider: Some(true),
13798 ..Default::default()
13799 }),
13800 ..Default::default()
13801 },
13802 cx,
13803 )
13804 .await;
13805
13806 cx.set_state("fn main() { let a = 2ˇ; }");
13807 cx.simulate_keystroke(".");
13808
13809 let unresolved_item_1 = lsp::CompletionItem {
13810 label: "id".to_string(),
13811 filter_text: Some("id".to_string()),
13812 detail: None,
13813 documentation: None,
13814 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13815 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13816 new_text: ".id".to_string(),
13817 })),
13818 ..lsp::CompletionItem::default()
13819 };
13820 let resolved_item_1 = lsp::CompletionItem {
13821 additional_text_edits: Some(vec![lsp::TextEdit {
13822 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13823 new_text: "!!".to_string(),
13824 }]),
13825 ..unresolved_item_1.clone()
13826 };
13827 let unresolved_item_2 = lsp::CompletionItem {
13828 label: "other".to_string(),
13829 filter_text: Some("other".to_string()),
13830 detail: None,
13831 documentation: None,
13832 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
13833 range: lsp::Range::new(lsp::Position::new(0, 22), lsp::Position::new(0, 22)),
13834 new_text: ".other".to_string(),
13835 })),
13836 ..lsp::CompletionItem::default()
13837 };
13838 let resolved_item_2 = lsp::CompletionItem {
13839 additional_text_edits: Some(vec![lsp::TextEdit {
13840 range: lsp::Range::new(lsp::Position::new(0, 20), lsp::Position::new(0, 22)),
13841 new_text: "??".to_string(),
13842 }]),
13843 ..unresolved_item_2.clone()
13844 };
13845
13846 let resolve_requests_1 = Arc::new(AtomicUsize::new(0));
13847 let resolve_requests_2 = Arc::new(AtomicUsize::new(0));
13848 cx.lsp
13849 .server
13850 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
13851 let unresolved_item_1 = unresolved_item_1.clone();
13852 let resolved_item_1 = resolved_item_1.clone();
13853 let unresolved_item_2 = unresolved_item_2.clone();
13854 let resolved_item_2 = resolved_item_2.clone();
13855 let resolve_requests_1 = resolve_requests_1.clone();
13856 let resolve_requests_2 = resolve_requests_2.clone();
13857 move |unresolved_request, _| {
13858 let unresolved_item_1 = unresolved_item_1.clone();
13859 let resolved_item_1 = resolved_item_1.clone();
13860 let unresolved_item_2 = unresolved_item_2.clone();
13861 let resolved_item_2 = resolved_item_2.clone();
13862 let resolve_requests_1 = resolve_requests_1.clone();
13863 let resolve_requests_2 = resolve_requests_2.clone();
13864 async move {
13865 if unresolved_request == unresolved_item_1 {
13866 resolve_requests_1.fetch_add(1, atomic::Ordering::Release);
13867 Ok(resolved_item_1.clone())
13868 } else if unresolved_request == unresolved_item_2 {
13869 resolve_requests_2.fetch_add(1, atomic::Ordering::Release);
13870 Ok(resolved_item_2.clone())
13871 } else {
13872 panic!("Unexpected completion item {unresolved_request:?}")
13873 }
13874 }
13875 }
13876 })
13877 .detach();
13878
13879 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
13880 let unresolved_item_1 = unresolved_item_1.clone();
13881 let unresolved_item_2 = unresolved_item_2.clone();
13882 async move {
13883 Ok(Some(lsp::CompletionResponse::Array(vec![
13884 unresolved_item_1,
13885 unresolved_item_2,
13886 ])))
13887 }
13888 })
13889 .next()
13890 .await;
13891
13892 cx.condition(|editor, _| editor.context_menu_visible())
13893 .await;
13894 cx.update_editor(|editor, _, _| {
13895 let context_menu = editor.context_menu.borrow_mut();
13896 let context_menu = context_menu
13897 .as_ref()
13898 .expect("Should have the context menu deployed");
13899 match context_menu {
13900 CodeContextMenu::Completions(completions_menu) => {
13901 let completions = completions_menu.completions.borrow_mut();
13902 assert_eq!(
13903 completions
13904 .iter()
13905 .map(|completion| &completion.label.text)
13906 .collect::<Vec<_>>(),
13907 vec!["id", "other"]
13908 )
13909 }
13910 CodeContextMenu::CodeActions(_) => panic!("Should show the completions menu"),
13911 }
13912 });
13913 cx.run_until_parked();
13914
13915 cx.update_editor(|editor, window, cx| {
13916 editor.context_menu_next(&ContextMenuNext, window, cx);
13917 });
13918 cx.run_until_parked();
13919 cx.update_editor(|editor, window, cx| {
13920 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
13921 });
13922 cx.run_until_parked();
13923 cx.update_editor(|editor, window, cx| {
13924 editor.context_menu_next(&ContextMenuNext, window, cx);
13925 });
13926 cx.run_until_parked();
13927 cx.update_editor(|editor, window, cx| {
13928 editor
13929 .compose_completion(&ComposeCompletion::default(), window, cx)
13930 .expect("No task returned")
13931 })
13932 .await
13933 .expect("Completion failed");
13934 cx.run_until_parked();
13935
13936 cx.update_editor(|editor, _, cx| {
13937 assert_eq!(
13938 resolve_requests_1.load(atomic::Ordering::Acquire),
13939 1,
13940 "Should always resolve once despite multiple selections"
13941 );
13942 assert_eq!(
13943 resolve_requests_2.load(atomic::Ordering::Acquire),
13944 1,
13945 "Should always resolve once after multiple selections and applying the completion"
13946 );
13947 assert_eq!(
13948 editor.text(cx),
13949 "fn main() { let a = ??.other; }",
13950 "Should use resolved data when applying the completion"
13951 );
13952 });
13953}
13954
13955#[gpui::test]
13956async fn test_completions_default_resolve_data_handling(cx: &mut TestAppContext) {
13957 init_test(cx, |_| {});
13958
13959 let item_0 = lsp::CompletionItem {
13960 label: "abs".into(),
13961 insert_text: Some("abs".into()),
13962 data: Some(json!({ "very": "special"})),
13963 insert_text_mode: Some(lsp::InsertTextMode::ADJUST_INDENTATION),
13964 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
13965 lsp::InsertReplaceEdit {
13966 new_text: "abs".to_string(),
13967 insert: lsp::Range::default(),
13968 replace: lsp::Range::default(),
13969 },
13970 )),
13971 ..lsp::CompletionItem::default()
13972 };
13973 let items = iter::once(item_0.clone())
13974 .chain((11..51).map(|i| lsp::CompletionItem {
13975 label: format!("item_{}", i),
13976 insert_text: Some(format!("item_{}", i)),
13977 insert_text_format: Some(lsp::InsertTextFormat::PLAIN_TEXT),
13978 ..lsp::CompletionItem::default()
13979 }))
13980 .collect::<Vec<_>>();
13981
13982 let default_commit_characters = vec!["?".to_string()];
13983 let default_data = json!({ "default": "data"});
13984 let default_insert_text_format = lsp::InsertTextFormat::SNIPPET;
13985 let default_insert_text_mode = lsp::InsertTextMode::AS_IS;
13986 let default_edit_range = lsp::Range {
13987 start: lsp::Position {
13988 line: 0,
13989 character: 5,
13990 },
13991 end: lsp::Position {
13992 line: 0,
13993 character: 5,
13994 },
13995 };
13996
13997 let mut cx = EditorLspTestContext::new_rust(
13998 lsp::ServerCapabilities {
13999 completion_provider: Some(lsp::CompletionOptions {
14000 trigger_characters: Some(vec![".".to_string()]),
14001 resolve_provider: Some(true),
14002 ..Default::default()
14003 }),
14004 ..Default::default()
14005 },
14006 cx,
14007 )
14008 .await;
14009
14010 cx.set_state("fn main() { let a = 2ˇ; }");
14011 cx.simulate_keystroke(".");
14012
14013 let completion_data = default_data.clone();
14014 let completion_characters = default_commit_characters.clone();
14015 let completion_items = items.clone();
14016 cx.set_request_handler::<lsp::request::Completion, _, _>(move |_, _, _| {
14017 let default_data = completion_data.clone();
14018 let default_commit_characters = completion_characters.clone();
14019 let items = completion_items.clone();
14020 async move {
14021 Ok(Some(lsp::CompletionResponse::List(lsp::CompletionList {
14022 items,
14023 item_defaults: Some(lsp::CompletionListItemDefaults {
14024 data: Some(default_data.clone()),
14025 commit_characters: Some(default_commit_characters.clone()),
14026 edit_range: Some(lsp::CompletionListItemDefaultsEditRange::Range(
14027 default_edit_range,
14028 )),
14029 insert_text_format: Some(default_insert_text_format),
14030 insert_text_mode: Some(default_insert_text_mode),
14031 }),
14032 ..lsp::CompletionList::default()
14033 })))
14034 }
14035 })
14036 .next()
14037 .await;
14038
14039 let resolved_items = Arc::new(Mutex::new(Vec::new()));
14040 cx.lsp
14041 .server
14042 .on_request::<lsp::request::ResolveCompletionItem, _, _>({
14043 let closure_resolved_items = resolved_items.clone();
14044 move |item_to_resolve, _| {
14045 let closure_resolved_items = closure_resolved_items.clone();
14046 async move {
14047 closure_resolved_items.lock().push(item_to_resolve.clone());
14048 Ok(item_to_resolve)
14049 }
14050 }
14051 })
14052 .detach();
14053
14054 cx.condition(|editor, _| editor.context_menu_visible())
14055 .await;
14056 cx.run_until_parked();
14057 cx.update_editor(|editor, _, _| {
14058 let menu = editor.context_menu.borrow_mut();
14059 match menu.as_ref().expect("should have the completions menu") {
14060 CodeContextMenu::Completions(completions_menu) => {
14061 assert_eq!(
14062 completions_menu
14063 .entries
14064 .borrow()
14065 .iter()
14066 .map(|mat| mat.string.clone())
14067 .collect::<Vec<String>>(),
14068 items
14069 .iter()
14070 .map(|completion| completion.label.clone())
14071 .collect::<Vec<String>>()
14072 );
14073 }
14074 CodeContextMenu::CodeActions(_) => panic!("Expected to have the completions menu"),
14075 }
14076 });
14077 // Approximate initial displayed interval is 0..12. With extra item padding of 4 this is 0..16
14078 // with 4 from the end.
14079 assert_eq!(
14080 *resolved_items.lock(),
14081 [&items[0..16], &items[items.len() - 4..items.len()]]
14082 .concat()
14083 .iter()
14084 .cloned()
14085 .map(|mut item| {
14086 if item.data.is_none() {
14087 item.data = Some(default_data.clone());
14088 }
14089 item
14090 })
14091 .collect::<Vec<lsp::CompletionItem>>(),
14092 "Items sent for resolve should be unchanged modulo resolve `data` filled with default if missing"
14093 );
14094 resolved_items.lock().clear();
14095
14096 cx.update_editor(|editor, window, cx| {
14097 editor.context_menu_prev(&ContextMenuPrevious, window, cx);
14098 });
14099 cx.run_until_parked();
14100 // Completions that have already been resolved are skipped.
14101 assert_eq!(
14102 *resolved_items.lock(),
14103 items[items.len() - 16..items.len() - 4]
14104 .iter()
14105 .cloned()
14106 .map(|mut item| {
14107 if item.data.is_none() {
14108 item.data = Some(default_data.clone());
14109 }
14110 item
14111 })
14112 .collect::<Vec<lsp::CompletionItem>>()
14113 );
14114 resolved_items.lock().clear();
14115}
14116
14117#[gpui::test]
14118async fn test_completions_in_languages_with_extra_word_characters(cx: &mut TestAppContext) {
14119 init_test(cx, |_| {});
14120
14121 let mut cx = EditorLspTestContext::new(
14122 Language::new(
14123 LanguageConfig {
14124 matcher: LanguageMatcher {
14125 path_suffixes: vec!["jsx".into()],
14126 ..Default::default()
14127 },
14128 overrides: [(
14129 "element".into(),
14130 LanguageConfigOverride {
14131 completion_query_characters: Override::Set(['-'].into_iter().collect()),
14132 ..Default::default()
14133 },
14134 )]
14135 .into_iter()
14136 .collect(),
14137 ..Default::default()
14138 },
14139 Some(tree_sitter_typescript::LANGUAGE_TSX.into()),
14140 )
14141 .with_override_query("(jsx_self_closing_element) @element")
14142 .unwrap(),
14143 lsp::ServerCapabilities {
14144 completion_provider: Some(lsp::CompletionOptions {
14145 trigger_characters: Some(vec![":".to_string()]),
14146 ..Default::default()
14147 }),
14148 ..Default::default()
14149 },
14150 cx,
14151 )
14152 .await;
14153
14154 cx.lsp
14155 .set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
14156 Ok(Some(lsp::CompletionResponse::Array(vec![
14157 lsp::CompletionItem {
14158 label: "bg-blue".into(),
14159 ..Default::default()
14160 },
14161 lsp::CompletionItem {
14162 label: "bg-red".into(),
14163 ..Default::default()
14164 },
14165 lsp::CompletionItem {
14166 label: "bg-yellow".into(),
14167 ..Default::default()
14168 },
14169 ])))
14170 });
14171
14172 cx.set_state(r#"<p class="bgˇ" />"#);
14173
14174 // Trigger completion when typing a dash, because the dash is an extra
14175 // word character in the 'element' scope, which contains the cursor.
14176 cx.simulate_keystroke("-");
14177 cx.executor().run_until_parked();
14178 cx.update_editor(|editor, _, _| {
14179 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14180 {
14181 assert_eq!(
14182 completion_menu_entries(&menu),
14183 &["bg-red", "bg-blue", "bg-yellow"]
14184 );
14185 } else {
14186 panic!("expected completion menu to be open");
14187 }
14188 });
14189
14190 cx.simulate_keystroke("l");
14191 cx.executor().run_until_parked();
14192 cx.update_editor(|editor, _, _| {
14193 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14194 {
14195 assert_eq!(completion_menu_entries(&menu), &["bg-blue", "bg-yellow"]);
14196 } else {
14197 panic!("expected completion menu to be open");
14198 }
14199 });
14200
14201 // When filtering completions, consider the character after the '-' to
14202 // be the start of a subword.
14203 cx.set_state(r#"<p class="yelˇ" />"#);
14204 cx.simulate_keystroke("l");
14205 cx.executor().run_until_parked();
14206 cx.update_editor(|editor, _, _| {
14207 if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
14208 {
14209 assert_eq!(completion_menu_entries(&menu), &["bg-yellow"]);
14210 } else {
14211 panic!("expected completion menu to be open");
14212 }
14213 });
14214}
14215
14216fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
14217 let entries = menu.entries.borrow();
14218 entries.iter().map(|mat| mat.string.clone()).collect()
14219}
14220
14221#[gpui::test]
14222async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
14223 init_test(cx, |settings| {
14224 settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
14225 FormatterList(vec![Formatter::Prettier].into()),
14226 ))
14227 });
14228
14229 let fs = FakeFs::new(cx.executor());
14230 fs.insert_file(path!("/file.ts"), Default::default()).await;
14231
14232 let project = Project::test(fs, [path!("/file.ts").as_ref()], cx).await;
14233 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
14234
14235 language_registry.add(Arc::new(Language::new(
14236 LanguageConfig {
14237 name: "TypeScript".into(),
14238 matcher: LanguageMatcher {
14239 path_suffixes: vec!["ts".to_string()],
14240 ..Default::default()
14241 },
14242 ..Default::default()
14243 },
14244 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
14245 )));
14246 update_test_language_settings(cx, |settings| {
14247 settings.defaults.prettier = Some(PrettierSettings {
14248 allowed: true,
14249 ..PrettierSettings::default()
14250 });
14251 });
14252
14253 let test_plugin = "test_plugin";
14254 let _ = language_registry.register_fake_lsp(
14255 "TypeScript",
14256 FakeLspAdapter {
14257 prettier_plugins: vec![test_plugin],
14258 ..Default::default()
14259 },
14260 );
14261
14262 let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
14263 let buffer = project
14264 .update(cx, |project, cx| {
14265 project.open_local_buffer(path!("/file.ts"), cx)
14266 })
14267 .await
14268 .unwrap();
14269
14270 let buffer_text = "one\ntwo\nthree\n";
14271 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
14272 let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
14273 editor.update_in(cx, |editor, window, cx| {
14274 editor.set_text(buffer_text, window, cx)
14275 });
14276
14277 editor
14278 .update_in(cx, |editor, window, cx| {
14279 editor.perform_format(
14280 project.clone(),
14281 FormatTrigger::Manual,
14282 FormatTarget::Buffers,
14283 window,
14284 cx,
14285 )
14286 })
14287 .unwrap()
14288 .await;
14289 assert_eq!(
14290 editor.update(cx, |editor, cx| editor.text(cx)),
14291 buffer_text.to_string() + prettier_format_suffix,
14292 "Test prettier formatting was not applied to the original buffer text",
14293 );
14294
14295 update_test_language_settings(cx, |settings| {
14296 settings.defaults.formatter = Some(language_settings::SelectedFormatter::Auto)
14297 });
14298 let format = editor.update_in(cx, |editor, window, cx| {
14299 editor.perform_format(
14300 project.clone(),
14301 FormatTrigger::Manual,
14302 FormatTarget::Buffers,
14303 window,
14304 cx,
14305 )
14306 });
14307 format.await.unwrap();
14308 assert_eq!(
14309 editor.update(cx, |editor, cx| editor.text(cx)),
14310 buffer_text.to_string() + prettier_format_suffix + "\n" + prettier_format_suffix,
14311 "Autoformatting (via test prettier) was not applied to the original buffer text",
14312 );
14313}
14314
14315#[gpui::test]
14316async fn test_addition_reverts(cx: &mut TestAppContext) {
14317 init_test(cx, |_| {});
14318 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14319 let base_text = indoc! {r#"
14320 struct Row;
14321 struct Row1;
14322 struct Row2;
14323
14324 struct Row4;
14325 struct Row5;
14326 struct Row6;
14327
14328 struct Row8;
14329 struct Row9;
14330 struct Row10;"#};
14331
14332 // When addition hunks are not adjacent to carets, no hunk revert is performed
14333 assert_hunk_revert(
14334 indoc! {r#"struct Row;
14335 struct Row1;
14336 struct Row1.1;
14337 struct Row1.2;
14338 struct Row2;ˇ
14339
14340 struct Row4;
14341 struct Row5;
14342 struct Row6;
14343
14344 struct Row8;
14345 ˇstruct Row9;
14346 struct Row9.1;
14347 struct Row9.2;
14348 struct Row9.3;
14349 struct Row10;"#},
14350 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14351 indoc! {r#"struct Row;
14352 struct Row1;
14353 struct Row1.1;
14354 struct Row1.2;
14355 struct Row2;ˇ
14356
14357 struct Row4;
14358 struct Row5;
14359 struct Row6;
14360
14361 struct Row8;
14362 ˇstruct Row9;
14363 struct Row9.1;
14364 struct Row9.2;
14365 struct Row9.3;
14366 struct Row10;"#},
14367 base_text,
14368 &mut cx,
14369 );
14370 // Same for selections
14371 assert_hunk_revert(
14372 indoc! {r#"struct Row;
14373 struct Row1;
14374 struct Row2;
14375 struct Row2.1;
14376 struct Row2.2;
14377 «ˇ
14378 struct Row4;
14379 struct» Row5;
14380 «struct Row6;
14381 ˇ»
14382 struct Row9.1;
14383 struct Row9.2;
14384 struct Row9.3;
14385 struct Row8;
14386 struct Row9;
14387 struct Row10;"#},
14388 vec![DiffHunkStatusKind::Added, DiffHunkStatusKind::Added],
14389 indoc! {r#"struct Row;
14390 struct Row1;
14391 struct Row2;
14392 struct Row2.1;
14393 struct Row2.2;
14394 «ˇ
14395 struct Row4;
14396 struct» Row5;
14397 «struct Row6;
14398 ˇ»
14399 struct Row9.1;
14400 struct Row9.2;
14401 struct Row9.3;
14402 struct Row8;
14403 struct Row9;
14404 struct Row10;"#},
14405 base_text,
14406 &mut cx,
14407 );
14408
14409 // When carets and selections intersect the addition hunks, those are reverted.
14410 // Adjacent carets got merged.
14411 assert_hunk_revert(
14412 indoc! {r#"struct Row;
14413 ˇ// something on the top
14414 struct Row1;
14415 struct Row2;
14416 struct Roˇw3.1;
14417 struct Row2.2;
14418 struct Row2.3;ˇ
14419
14420 struct Row4;
14421 struct ˇRow5.1;
14422 struct Row5.2;
14423 struct «Rowˇ»5.3;
14424 struct Row5;
14425 struct Row6;
14426 ˇ
14427 struct Row9.1;
14428 struct «Rowˇ»9.2;
14429 struct «ˇRow»9.3;
14430 struct Row8;
14431 struct Row9;
14432 «ˇ// something on bottom»
14433 struct Row10;"#},
14434 vec![
14435 DiffHunkStatusKind::Added,
14436 DiffHunkStatusKind::Added,
14437 DiffHunkStatusKind::Added,
14438 DiffHunkStatusKind::Added,
14439 DiffHunkStatusKind::Added,
14440 ],
14441 indoc! {r#"struct Row;
14442 ˇstruct Row1;
14443 struct Row2;
14444 ˇ
14445 struct Row4;
14446 ˇstruct Row5;
14447 struct Row6;
14448 ˇ
14449 ˇstruct Row8;
14450 struct Row9;
14451 ˇstruct Row10;"#},
14452 base_text,
14453 &mut cx,
14454 );
14455}
14456
14457#[gpui::test]
14458async fn test_modification_reverts(cx: &mut TestAppContext) {
14459 init_test(cx, |_| {});
14460 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14461 let base_text = indoc! {r#"
14462 struct Row;
14463 struct Row1;
14464 struct Row2;
14465
14466 struct Row4;
14467 struct Row5;
14468 struct Row6;
14469
14470 struct Row8;
14471 struct Row9;
14472 struct Row10;"#};
14473
14474 // Modification hunks behave the same as the addition ones.
14475 assert_hunk_revert(
14476 indoc! {r#"struct Row;
14477 struct Row1;
14478 struct Row33;
14479 ˇ
14480 struct Row4;
14481 struct Row5;
14482 struct Row6;
14483 ˇ
14484 struct Row99;
14485 struct Row9;
14486 struct Row10;"#},
14487 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14488 indoc! {r#"struct Row;
14489 struct Row1;
14490 struct Row33;
14491 ˇ
14492 struct Row4;
14493 struct Row5;
14494 struct Row6;
14495 ˇ
14496 struct Row99;
14497 struct Row9;
14498 struct Row10;"#},
14499 base_text,
14500 &mut cx,
14501 );
14502 assert_hunk_revert(
14503 indoc! {r#"struct Row;
14504 struct Row1;
14505 struct Row33;
14506 «ˇ
14507 struct Row4;
14508 struct» Row5;
14509 «struct Row6;
14510 ˇ»
14511 struct Row99;
14512 struct Row9;
14513 struct Row10;"#},
14514 vec![DiffHunkStatusKind::Modified, DiffHunkStatusKind::Modified],
14515 indoc! {r#"struct Row;
14516 struct Row1;
14517 struct Row33;
14518 «ˇ
14519 struct Row4;
14520 struct» Row5;
14521 «struct Row6;
14522 ˇ»
14523 struct Row99;
14524 struct Row9;
14525 struct Row10;"#},
14526 base_text,
14527 &mut cx,
14528 );
14529
14530 assert_hunk_revert(
14531 indoc! {r#"ˇstruct Row1.1;
14532 struct Row1;
14533 «ˇstr»uct Row22;
14534
14535 struct ˇRow44;
14536 struct Row5;
14537 struct «Rˇ»ow66;ˇ
14538
14539 «struˇ»ct Row88;
14540 struct Row9;
14541 struct Row1011;ˇ"#},
14542 vec![
14543 DiffHunkStatusKind::Modified,
14544 DiffHunkStatusKind::Modified,
14545 DiffHunkStatusKind::Modified,
14546 DiffHunkStatusKind::Modified,
14547 DiffHunkStatusKind::Modified,
14548 DiffHunkStatusKind::Modified,
14549 ],
14550 indoc! {r#"struct Row;
14551 ˇstruct Row1;
14552 struct Row2;
14553 ˇ
14554 struct Row4;
14555 ˇstruct Row5;
14556 struct Row6;
14557 ˇ
14558 struct Row8;
14559 ˇstruct Row9;
14560 struct Row10;ˇ"#},
14561 base_text,
14562 &mut cx,
14563 );
14564}
14565
14566#[gpui::test]
14567async fn test_deleting_over_diff_hunk(cx: &mut TestAppContext) {
14568 init_test(cx, |_| {});
14569 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14570 let base_text = indoc! {r#"
14571 one
14572
14573 two
14574 three
14575 "#};
14576
14577 cx.set_head_text(base_text);
14578 cx.set_state("\nˇ\n");
14579 cx.executor().run_until_parked();
14580 cx.update_editor(|editor, _window, cx| {
14581 editor.expand_selected_diff_hunks(cx);
14582 });
14583 cx.executor().run_until_parked();
14584 cx.update_editor(|editor, window, cx| {
14585 editor.backspace(&Default::default(), window, cx);
14586 });
14587 cx.run_until_parked();
14588 cx.assert_state_with_diff(
14589 indoc! {r#"
14590
14591 - two
14592 - threeˇ
14593 +
14594 "#}
14595 .to_string(),
14596 );
14597}
14598
14599#[gpui::test]
14600async fn test_deletion_reverts(cx: &mut TestAppContext) {
14601 init_test(cx, |_| {});
14602 let mut cx = EditorLspTestContext::new_rust(lsp::ServerCapabilities::default(), cx).await;
14603 let base_text = indoc! {r#"struct Row;
14604struct Row1;
14605struct Row2;
14606
14607struct Row4;
14608struct Row5;
14609struct Row6;
14610
14611struct Row8;
14612struct Row9;
14613struct Row10;"#};
14614
14615 // Deletion hunks trigger with carets on adjacent rows, so carets and selections have to stay farther to avoid the revert
14616 assert_hunk_revert(
14617 indoc! {r#"struct Row;
14618 struct Row2;
14619
14620 ˇstruct Row4;
14621 struct Row5;
14622 struct Row6;
14623 ˇ
14624 struct Row8;
14625 struct Row10;"#},
14626 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14627 indoc! {r#"struct Row;
14628 struct Row2;
14629
14630 ˇstruct Row4;
14631 struct Row5;
14632 struct Row6;
14633 ˇ
14634 struct Row8;
14635 struct Row10;"#},
14636 base_text,
14637 &mut cx,
14638 );
14639 assert_hunk_revert(
14640 indoc! {r#"struct Row;
14641 struct Row2;
14642
14643 «ˇstruct Row4;
14644 struct» Row5;
14645 «struct Row6;
14646 ˇ»
14647 struct Row8;
14648 struct Row10;"#},
14649 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14650 indoc! {r#"struct Row;
14651 struct Row2;
14652
14653 «ˇstruct Row4;
14654 struct» Row5;
14655 «struct Row6;
14656 ˇ»
14657 struct Row8;
14658 struct Row10;"#},
14659 base_text,
14660 &mut cx,
14661 );
14662
14663 // Deletion hunks are ephemeral, so it's impossible to place the caret into them — Zed triggers reverts for lines, adjacent to carets and selections.
14664 assert_hunk_revert(
14665 indoc! {r#"struct Row;
14666 ˇstruct Row2;
14667
14668 struct Row4;
14669 struct Row5;
14670 struct Row6;
14671
14672 struct Row8;ˇ
14673 struct Row10;"#},
14674 vec![DiffHunkStatusKind::Deleted, DiffHunkStatusKind::Deleted],
14675 indoc! {r#"struct Row;
14676 struct Row1;
14677 ˇstruct Row2;
14678
14679 struct Row4;
14680 struct Row5;
14681 struct Row6;
14682
14683 struct Row8;ˇ
14684 struct Row9;
14685 struct Row10;"#},
14686 base_text,
14687 &mut cx,
14688 );
14689 assert_hunk_revert(
14690 indoc! {r#"struct Row;
14691 struct Row2«ˇ;
14692 struct Row4;
14693 struct» Row5;
14694 «struct Row6;
14695
14696 struct Row8;ˇ»
14697 struct Row10;"#},
14698 vec![
14699 DiffHunkStatusKind::Deleted,
14700 DiffHunkStatusKind::Deleted,
14701 DiffHunkStatusKind::Deleted,
14702 ],
14703 indoc! {r#"struct Row;
14704 struct Row1;
14705 struct Row2«ˇ;
14706
14707 struct Row4;
14708 struct» Row5;
14709 «struct Row6;
14710
14711 struct Row8;ˇ»
14712 struct Row9;
14713 struct Row10;"#},
14714 base_text,
14715 &mut cx,
14716 );
14717}
14718
14719#[gpui::test]
14720async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
14721 init_test(cx, |_| {});
14722
14723 let base_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj";
14724 let base_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu";
14725 let base_text_3 =
14726 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}";
14727
14728 let text_1 = edit_first_char_of_every_line(base_text_1);
14729 let text_2 = edit_first_char_of_every_line(base_text_2);
14730 let text_3 = edit_first_char_of_every_line(base_text_3);
14731
14732 let buffer_1 = cx.new(|cx| Buffer::local(text_1.clone(), cx));
14733 let buffer_2 = cx.new(|cx| Buffer::local(text_2.clone(), cx));
14734 let buffer_3 = cx.new(|cx| Buffer::local(text_3.clone(), cx));
14735
14736 let multibuffer = cx.new(|cx| {
14737 let mut multibuffer = MultiBuffer::new(ReadWrite);
14738 multibuffer.push_excerpts(
14739 buffer_1.clone(),
14740 [
14741 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14742 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14743 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14744 ],
14745 cx,
14746 );
14747 multibuffer.push_excerpts(
14748 buffer_2.clone(),
14749 [
14750 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14751 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14752 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14753 ],
14754 cx,
14755 );
14756 multibuffer.push_excerpts(
14757 buffer_3.clone(),
14758 [
14759 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14760 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14761 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14762 ],
14763 cx,
14764 );
14765 multibuffer
14766 });
14767
14768 let fs = FakeFs::new(cx.executor());
14769 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
14770 let (editor, cx) = cx
14771 .add_window_view(|window, cx| build_editor_with_project(project, multibuffer, window, cx));
14772 editor.update_in(cx, |editor, _window, cx| {
14773 for (buffer, diff_base) in [
14774 (buffer_1.clone(), base_text_1),
14775 (buffer_2.clone(), base_text_2),
14776 (buffer_3.clone(), base_text_3),
14777 ] {
14778 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
14779 editor
14780 .buffer
14781 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
14782 }
14783 });
14784 cx.executor().run_until_parked();
14785
14786 editor.update_in(cx, |editor, window, cx| {
14787 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}");
14788 editor.select_all(&SelectAll, window, cx);
14789 editor.git_restore(&Default::default(), window, cx);
14790 });
14791 cx.executor().run_until_parked();
14792
14793 // When all ranges are selected, all buffer hunks are reverted.
14794 editor.update(cx, |editor, cx| {
14795 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");
14796 });
14797 buffer_1.update(cx, |buffer, _| {
14798 assert_eq!(buffer.text(), base_text_1);
14799 });
14800 buffer_2.update(cx, |buffer, _| {
14801 assert_eq!(buffer.text(), base_text_2);
14802 });
14803 buffer_3.update(cx, |buffer, _| {
14804 assert_eq!(buffer.text(), base_text_3);
14805 });
14806
14807 editor.update_in(cx, |editor, window, cx| {
14808 editor.undo(&Default::default(), window, cx);
14809 });
14810
14811 editor.update_in(cx, |editor, window, cx| {
14812 editor.change_selections(None, window, cx, |s| {
14813 s.select_ranges(Some(Point::new(0, 0)..Point::new(6, 0)));
14814 });
14815 editor.git_restore(&Default::default(), window, cx);
14816 });
14817
14818 // Now, when all ranges selected belong to buffer_1, the revert should succeed,
14819 // but not affect buffer_2 and its related excerpts.
14820 editor.update(cx, |editor, cx| {
14821 assert_eq!(
14822 editor.text(cx),
14823 "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}"
14824 );
14825 });
14826 buffer_1.update(cx, |buffer, _| {
14827 assert_eq!(buffer.text(), base_text_1);
14828 });
14829 buffer_2.update(cx, |buffer, _| {
14830 assert_eq!(
14831 buffer.text(),
14832 "Xlll\nXmmm\nXnnn\nXooo\nXppp\nXqqq\nXrrr\nXsss\nXttt\nXuuu"
14833 );
14834 });
14835 buffer_3.update(cx, |buffer, _| {
14836 assert_eq!(
14837 buffer.text(),
14838 "Xvvv\nXwww\nXxxx\nXyyy\nXzzz\nX{{{\nX|||\nX}}}\nX~~~\nX\u{7f}\u{7f}\u{7f}"
14839 );
14840 });
14841
14842 fn edit_first_char_of_every_line(text: &str) -> String {
14843 text.split('\n')
14844 .map(|line| format!("X{}", &line[1..]))
14845 .collect::<Vec<_>>()
14846 .join("\n")
14847 }
14848}
14849
14850#[gpui::test]
14851async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) {
14852 init_test(cx, |_| {});
14853
14854 let cols = 4;
14855 let rows = 10;
14856 let sample_text_1 = sample_text(rows, cols, 'a');
14857 assert_eq!(
14858 sample_text_1,
14859 "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj"
14860 );
14861 let sample_text_2 = sample_text(rows, cols, 'l');
14862 assert_eq!(
14863 sample_text_2,
14864 "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu"
14865 );
14866 let sample_text_3 = sample_text(rows, cols, 'v');
14867 assert_eq!(
14868 sample_text_3,
14869 "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n{{{{\n||||\n}}}}\n~~~~\n\u{7f}\u{7f}\u{7f}\u{7f}"
14870 );
14871
14872 let buffer_1 = cx.new(|cx| Buffer::local(sample_text_1.clone(), cx));
14873 let buffer_2 = cx.new(|cx| Buffer::local(sample_text_2.clone(), cx));
14874 let buffer_3 = cx.new(|cx| Buffer::local(sample_text_3.clone(), cx));
14875
14876 let multi_buffer = cx.new(|cx| {
14877 let mut multibuffer = MultiBuffer::new(ReadWrite);
14878 multibuffer.push_excerpts(
14879 buffer_1.clone(),
14880 [
14881 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14882 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14883 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14884 ],
14885 cx,
14886 );
14887 multibuffer.push_excerpts(
14888 buffer_2.clone(),
14889 [
14890 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14891 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14892 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14893 ],
14894 cx,
14895 );
14896 multibuffer.push_excerpts(
14897 buffer_3.clone(),
14898 [
14899 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
14900 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
14901 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
14902 ],
14903 cx,
14904 );
14905 multibuffer
14906 });
14907
14908 let fs = FakeFs::new(cx.executor());
14909 fs.insert_tree(
14910 "/a",
14911 json!({
14912 "main.rs": sample_text_1,
14913 "other.rs": sample_text_2,
14914 "lib.rs": sample_text_3,
14915 }),
14916 )
14917 .await;
14918 let project = Project::test(fs, ["/a".as_ref()], cx).await;
14919 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
14920 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
14921 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
14922 Editor::new(
14923 EditorMode::full(),
14924 multi_buffer,
14925 Some(project.clone()),
14926 window,
14927 cx,
14928 )
14929 });
14930 let multibuffer_item_id = workspace
14931 .update(cx, |workspace, window, cx| {
14932 assert!(
14933 workspace.active_item(cx).is_none(),
14934 "active item should be None before the first item is added"
14935 );
14936 workspace.add_item_to_active_pane(
14937 Box::new(multi_buffer_editor.clone()),
14938 None,
14939 true,
14940 window,
14941 cx,
14942 );
14943 let active_item = workspace
14944 .active_item(cx)
14945 .expect("should have an active item after adding the multi buffer");
14946 assert!(
14947 !active_item.is_singleton(cx),
14948 "A multi buffer was expected to active after adding"
14949 );
14950 active_item.item_id()
14951 })
14952 .unwrap();
14953 cx.executor().run_until_parked();
14954
14955 multi_buffer_editor.update_in(cx, |editor, window, cx| {
14956 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
14957 s.select_ranges(Some(1..2))
14958 });
14959 editor.open_excerpts(&OpenExcerpts, window, cx);
14960 });
14961 cx.executor().run_until_parked();
14962 let first_item_id = workspace
14963 .update(cx, |workspace, window, cx| {
14964 let active_item = workspace
14965 .active_item(cx)
14966 .expect("should have an active item after navigating into the 1st buffer");
14967 let first_item_id = active_item.item_id();
14968 assert_ne!(
14969 first_item_id, multibuffer_item_id,
14970 "Should navigate into the 1st buffer and activate it"
14971 );
14972 assert!(
14973 active_item.is_singleton(cx),
14974 "New active item should be a singleton buffer"
14975 );
14976 assert_eq!(
14977 active_item
14978 .act_as::<Editor>(cx)
14979 .expect("should have navigated into an editor for the 1st buffer")
14980 .read(cx)
14981 .text(cx),
14982 sample_text_1
14983 );
14984
14985 workspace
14986 .go_back(workspace.active_pane().downgrade(), window, cx)
14987 .detach_and_log_err(cx);
14988
14989 first_item_id
14990 })
14991 .unwrap();
14992 cx.executor().run_until_parked();
14993 workspace
14994 .update(cx, |workspace, _, cx| {
14995 let active_item = workspace
14996 .active_item(cx)
14997 .expect("should have an active item after navigating back");
14998 assert_eq!(
14999 active_item.item_id(),
15000 multibuffer_item_id,
15001 "Should navigate back to the multi buffer"
15002 );
15003 assert!(!active_item.is_singleton(cx));
15004 })
15005 .unwrap();
15006
15007 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15008 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15009 s.select_ranges(Some(39..40))
15010 });
15011 editor.open_excerpts(&OpenExcerpts, window, cx);
15012 });
15013 cx.executor().run_until_parked();
15014 let second_item_id = workspace
15015 .update(cx, |workspace, window, cx| {
15016 let active_item = workspace
15017 .active_item(cx)
15018 .expect("should have an active item after navigating into the 2nd buffer");
15019 let second_item_id = active_item.item_id();
15020 assert_ne!(
15021 second_item_id, multibuffer_item_id,
15022 "Should navigate away from the multibuffer"
15023 );
15024 assert_ne!(
15025 second_item_id, first_item_id,
15026 "Should navigate into the 2nd buffer and activate it"
15027 );
15028 assert!(
15029 active_item.is_singleton(cx),
15030 "New active item should be a singleton buffer"
15031 );
15032 assert_eq!(
15033 active_item
15034 .act_as::<Editor>(cx)
15035 .expect("should have navigated into an editor")
15036 .read(cx)
15037 .text(cx),
15038 sample_text_2
15039 );
15040
15041 workspace
15042 .go_back(workspace.active_pane().downgrade(), window, cx)
15043 .detach_and_log_err(cx);
15044
15045 second_item_id
15046 })
15047 .unwrap();
15048 cx.executor().run_until_parked();
15049 workspace
15050 .update(cx, |workspace, _, cx| {
15051 let active_item = workspace
15052 .active_item(cx)
15053 .expect("should have an active item after navigating back from the 2nd buffer");
15054 assert_eq!(
15055 active_item.item_id(),
15056 multibuffer_item_id,
15057 "Should navigate back from the 2nd buffer to the multi buffer"
15058 );
15059 assert!(!active_item.is_singleton(cx));
15060 })
15061 .unwrap();
15062
15063 multi_buffer_editor.update_in(cx, |editor, window, cx| {
15064 editor.change_selections(Some(Autoscroll::Next), window, cx, |s| {
15065 s.select_ranges(Some(70..70))
15066 });
15067 editor.open_excerpts(&OpenExcerpts, window, cx);
15068 });
15069 cx.executor().run_until_parked();
15070 workspace
15071 .update(cx, |workspace, window, cx| {
15072 let active_item = workspace
15073 .active_item(cx)
15074 .expect("should have an active item after navigating into the 3rd buffer");
15075 let third_item_id = active_item.item_id();
15076 assert_ne!(
15077 third_item_id, multibuffer_item_id,
15078 "Should navigate into the 3rd buffer and activate it"
15079 );
15080 assert_ne!(third_item_id, first_item_id);
15081 assert_ne!(third_item_id, second_item_id);
15082 assert!(
15083 active_item.is_singleton(cx),
15084 "New active item should be a singleton buffer"
15085 );
15086 assert_eq!(
15087 active_item
15088 .act_as::<Editor>(cx)
15089 .expect("should have navigated into an editor")
15090 .read(cx)
15091 .text(cx),
15092 sample_text_3
15093 );
15094
15095 workspace
15096 .go_back(workspace.active_pane().downgrade(), window, cx)
15097 .detach_and_log_err(cx);
15098 })
15099 .unwrap();
15100 cx.executor().run_until_parked();
15101 workspace
15102 .update(cx, |workspace, _, cx| {
15103 let active_item = workspace
15104 .active_item(cx)
15105 .expect("should have an active item after navigating back from the 3rd buffer");
15106 assert_eq!(
15107 active_item.item_id(),
15108 multibuffer_item_id,
15109 "Should navigate back from the 3rd buffer to the multi buffer"
15110 );
15111 assert!(!active_item.is_singleton(cx));
15112 })
15113 .unwrap();
15114}
15115
15116#[gpui::test]
15117async fn test_toggle_selected_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15118 init_test(cx, |_| {});
15119
15120 let mut cx = EditorTestContext::new(cx).await;
15121
15122 let diff_base = r#"
15123 use some::mod;
15124
15125 const A: u32 = 42;
15126
15127 fn main() {
15128 println!("hello");
15129
15130 println!("world");
15131 }
15132 "#
15133 .unindent();
15134
15135 cx.set_state(
15136 &r#"
15137 use some::modified;
15138
15139 ˇ
15140 fn main() {
15141 println!("hello there");
15142
15143 println!("around the");
15144 println!("world");
15145 }
15146 "#
15147 .unindent(),
15148 );
15149
15150 cx.set_head_text(&diff_base);
15151 executor.run_until_parked();
15152
15153 cx.update_editor(|editor, window, cx| {
15154 editor.go_to_next_hunk(&GoToHunk, window, cx);
15155 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15156 });
15157 executor.run_until_parked();
15158 cx.assert_state_with_diff(
15159 r#"
15160 use some::modified;
15161
15162
15163 fn main() {
15164 - println!("hello");
15165 + ˇ println!("hello there");
15166
15167 println!("around the");
15168 println!("world");
15169 }
15170 "#
15171 .unindent(),
15172 );
15173
15174 cx.update_editor(|editor, window, cx| {
15175 for _ in 0..2 {
15176 editor.go_to_next_hunk(&GoToHunk, window, cx);
15177 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15178 }
15179 });
15180 executor.run_until_parked();
15181 cx.assert_state_with_diff(
15182 r#"
15183 - use some::mod;
15184 + ˇuse some::modified;
15185
15186
15187 fn main() {
15188 - println!("hello");
15189 + println!("hello there");
15190
15191 + println!("around the");
15192 println!("world");
15193 }
15194 "#
15195 .unindent(),
15196 );
15197
15198 cx.update_editor(|editor, window, cx| {
15199 editor.go_to_next_hunk(&GoToHunk, window, cx);
15200 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15201 });
15202 executor.run_until_parked();
15203 cx.assert_state_with_diff(
15204 r#"
15205 - use some::mod;
15206 + use some::modified;
15207
15208 - const A: u32 = 42;
15209 ˇ
15210 fn main() {
15211 - println!("hello");
15212 + println!("hello there");
15213
15214 + println!("around the");
15215 println!("world");
15216 }
15217 "#
15218 .unindent(),
15219 );
15220
15221 cx.update_editor(|editor, window, cx| {
15222 editor.cancel(&Cancel, window, cx);
15223 });
15224
15225 cx.assert_state_with_diff(
15226 r#"
15227 use some::modified;
15228
15229 ˇ
15230 fn main() {
15231 println!("hello there");
15232
15233 println!("around the");
15234 println!("world");
15235 }
15236 "#
15237 .unindent(),
15238 );
15239}
15240
15241#[gpui::test]
15242async fn test_diff_base_change_with_expanded_diff_hunks(
15243 executor: BackgroundExecutor,
15244 cx: &mut TestAppContext,
15245) {
15246 init_test(cx, |_| {});
15247
15248 let mut cx = EditorTestContext::new(cx).await;
15249
15250 let diff_base = r#"
15251 use some::mod1;
15252 use some::mod2;
15253
15254 const A: u32 = 42;
15255 const B: u32 = 42;
15256 const C: u32 = 42;
15257
15258 fn main() {
15259 println!("hello");
15260
15261 println!("world");
15262 }
15263 "#
15264 .unindent();
15265
15266 cx.set_state(
15267 &r#"
15268 use some::mod2;
15269
15270 const A: u32 = 42;
15271 const C: u32 = 42;
15272
15273 fn main(ˇ) {
15274 //println!("hello");
15275
15276 println!("world");
15277 //
15278 //
15279 }
15280 "#
15281 .unindent(),
15282 );
15283
15284 cx.set_head_text(&diff_base);
15285 executor.run_until_parked();
15286
15287 cx.update_editor(|editor, window, cx| {
15288 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15289 });
15290 executor.run_until_parked();
15291 cx.assert_state_with_diff(
15292 r#"
15293 - use some::mod1;
15294 use some::mod2;
15295
15296 const A: u32 = 42;
15297 - const B: u32 = 42;
15298 const C: u32 = 42;
15299
15300 fn main(ˇ) {
15301 - println!("hello");
15302 + //println!("hello");
15303
15304 println!("world");
15305 + //
15306 + //
15307 }
15308 "#
15309 .unindent(),
15310 );
15311
15312 cx.set_head_text("new diff base!");
15313 executor.run_until_parked();
15314 cx.assert_state_with_diff(
15315 r#"
15316 - new diff base!
15317 + use some::mod2;
15318 +
15319 + const A: u32 = 42;
15320 + const C: u32 = 42;
15321 +
15322 + fn main(ˇ) {
15323 + //println!("hello");
15324 +
15325 + println!("world");
15326 + //
15327 + //
15328 + }
15329 "#
15330 .unindent(),
15331 );
15332}
15333
15334#[gpui::test]
15335async fn test_toggle_diff_expand_in_multi_buffer(cx: &mut TestAppContext) {
15336 init_test(cx, |_| {});
15337
15338 let file_1_old = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15339 let file_1_new = "aaa\nccc\nddd\neee\nfff\nggg\nhhh\niii\njjj";
15340 let file_2_old = "lll\nmmm\nnnn\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15341 let file_2_new = "lll\nmmm\nNNN\nooo\nppp\nqqq\nrrr\nsss\nttt\nuuu";
15342 let file_3_old = "111\n222\n333\n444\n555\n777\n888\n999\n000\n!!!";
15343 let file_3_new = "111\n222\n333\n444\n555\n666\n777\n888\n999\n000\n!!!";
15344
15345 let buffer_1 = cx.new(|cx| Buffer::local(file_1_new.to_string(), cx));
15346 let buffer_2 = cx.new(|cx| Buffer::local(file_2_new.to_string(), cx));
15347 let buffer_3 = cx.new(|cx| Buffer::local(file_3_new.to_string(), cx));
15348
15349 let multi_buffer = cx.new(|cx| {
15350 let mut multibuffer = MultiBuffer::new(ReadWrite);
15351 multibuffer.push_excerpts(
15352 buffer_1.clone(),
15353 [
15354 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15355 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15356 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15357 ],
15358 cx,
15359 );
15360 multibuffer.push_excerpts(
15361 buffer_2.clone(),
15362 [
15363 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15364 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15365 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15366 ],
15367 cx,
15368 );
15369 multibuffer.push_excerpts(
15370 buffer_3.clone(),
15371 [
15372 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
15373 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
15374 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 3)),
15375 ],
15376 cx,
15377 );
15378 multibuffer
15379 });
15380
15381 let editor =
15382 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15383 editor
15384 .update(cx, |editor, _window, cx| {
15385 for (buffer, diff_base) in [
15386 (buffer_1.clone(), file_1_old),
15387 (buffer_2.clone(), file_2_old),
15388 (buffer_3.clone(), file_3_old),
15389 ] {
15390 let diff = cx.new(|cx| BufferDiff::new_with_base_text(&diff_base, &buffer, cx));
15391 editor
15392 .buffer
15393 .update(cx, |buffer, cx| buffer.add_diff(diff, cx));
15394 }
15395 })
15396 .unwrap();
15397
15398 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15399 cx.run_until_parked();
15400
15401 cx.assert_editor_state(
15402 &"
15403 ˇaaa
15404 ccc
15405 ddd
15406
15407 ggg
15408 hhh
15409
15410
15411 lll
15412 mmm
15413 NNN
15414
15415 qqq
15416 rrr
15417
15418 uuu
15419 111
15420 222
15421 333
15422
15423 666
15424 777
15425
15426 000
15427 !!!"
15428 .unindent(),
15429 );
15430
15431 cx.update_editor(|editor, window, cx| {
15432 editor.select_all(&SelectAll, window, cx);
15433 editor.toggle_selected_diff_hunks(&ToggleSelectedDiffHunks, window, cx);
15434 });
15435 cx.executor().run_until_parked();
15436
15437 cx.assert_state_with_diff(
15438 "
15439 «aaa
15440 - bbb
15441 ccc
15442 ddd
15443
15444 ggg
15445 hhh
15446
15447
15448 lll
15449 mmm
15450 - nnn
15451 + NNN
15452
15453 qqq
15454 rrr
15455
15456 uuu
15457 111
15458 222
15459 333
15460
15461 + 666
15462 777
15463
15464 000
15465 !!!ˇ»"
15466 .unindent(),
15467 );
15468}
15469
15470#[gpui::test]
15471async fn test_expand_diff_hunk_at_excerpt_boundary(cx: &mut TestAppContext) {
15472 init_test(cx, |_| {});
15473
15474 let base = "aaa\nbbb\nccc\nddd\neee\nfff\nggg\n";
15475 let text = "aaa\nBBB\nBB2\nccc\nDDD\nEEE\nfff\nggg\nhhh\niii\n";
15476
15477 let buffer = cx.new(|cx| Buffer::local(text.to_string(), cx));
15478 let multi_buffer = cx.new(|cx| {
15479 let mut multibuffer = MultiBuffer::new(ReadWrite);
15480 multibuffer.push_excerpts(
15481 buffer.clone(),
15482 [
15483 ExcerptRange::new(Point::new(0, 0)..Point::new(2, 0)),
15484 ExcerptRange::new(Point::new(4, 0)..Point::new(7, 0)),
15485 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 0)),
15486 ],
15487 cx,
15488 );
15489 multibuffer
15490 });
15491
15492 let editor =
15493 cx.add_window(|window, cx| Editor::new(EditorMode::full(), multi_buffer, None, window, cx));
15494 editor
15495 .update(cx, |editor, _window, cx| {
15496 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base, &buffer, cx));
15497 editor
15498 .buffer
15499 .update(cx, |buffer, cx| buffer.add_diff(diff, cx))
15500 })
15501 .unwrap();
15502
15503 let mut cx = EditorTestContext::for_editor(editor, cx).await;
15504 cx.run_until_parked();
15505
15506 cx.update_editor(|editor, window, cx| {
15507 editor.expand_all_diff_hunks(&Default::default(), window, cx)
15508 });
15509 cx.executor().run_until_parked();
15510
15511 // When the start of a hunk coincides with the start of its excerpt,
15512 // the hunk is expanded. When the start of a a hunk is earlier than
15513 // the start of its excerpt, the hunk is not expanded.
15514 cx.assert_state_with_diff(
15515 "
15516 ˇaaa
15517 - bbb
15518 + BBB
15519
15520 - ddd
15521 - eee
15522 + DDD
15523 + EEE
15524 fff
15525
15526 iii
15527 "
15528 .unindent(),
15529 );
15530}
15531
15532#[gpui::test]
15533async fn test_edits_around_expanded_insertion_hunks(
15534 executor: BackgroundExecutor,
15535 cx: &mut TestAppContext,
15536) {
15537 init_test(cx, |_| {});
15538
15539 let mut cx = EditorTestContext::new(cx).await;
15540
15541 let diff_base = r#"
15542 use some::mod1;
15543 use some::mod2;
15544
15545 const A: u32 = 42;
15546
15547 fn main() {
15548 println!("hello");
15549
15550 println!("world");
15551 }
15552 "#
15553 .unindent();
15554 executor.run_until_parked();
15555 cx.set_state(
15556 &r#"
15557 use some::mod1;
15558 use some::mod2;
15559
15560 const A: u32 = 42;
15561 const B: u32 = 42;
15562 const C: u32 = 42;
15563 ˇ
15564
15565 fn main() {
15566 println!("hello");
15567
15568 println!("world");
15569 }
15570 "#
15571 .unindent(),
15572 );
15573
15574 cx.set_head_text(&diff_base);
15575 executor.run_until_parked();
15576
15577 cx.update_editor(|editor, window, cx| {
15578 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15579 });
15580 executor.run_until_parked();
15581
15582 cx.assert_state_with_diff(
15583 r#"
15584 use some::mod1;
15585 use some::mod2;
15586
15587 const A: u32 = 42;
15588 + const B: u32 = 42;
15589 + const C: u32 = 42;
15590 + ˇ
15591
15592 fn main() {
15593 println!("hello");
15594
15595 println!("world");
15596 }
15597 "#
15598 .unindent(),
15599 );
15600
15601 cx.update_editor(|editor, window, cx| editor.handle_input("const D: u32 = 42;\n", window, cx));
15602 executor.run_until_parked();
15603
15604 cx.assert_state_with_diff(
15605 r#"
15606 use some::mod1;
15607 use some::mod2;
15608
15609 const A: u32 = 42;
15610 + const B: u32 = 42;
15611 + const C: u32 = 42;
15612 + const D: u32 = 42;
15613 + ˇ
15614
15615 fn main() {
15616 println!("hello");
15617
15618 println!("world");
15619 }
15620 "#
15621 .unindent(),
15622 );
15623
15624 cx.update_editor(|editor, window, cx| editor.handle_input("const E: u32 = 42;\n", window, cx));
15625 executor.run_until_parked();
15626
15627 cx.assert_state_with_diff(
15628 r#"
15629 use some::mod1;
15630 use some::mod2;
15631
15632 const A: u32 = 42;
15633 + const B: u32 = 42;
15634 + const C: u32 = 42;
15635 + const D: u32 = 42;
15636 + const E: u32 = 42;
15637 + ˇ
15638
15639 fn main() {
15640 println!("hello");
15641
15642 println!("world");
15643 }
15644 "#
15645 .unindent(),
15646 );
15647
15648 cx.update_editor(|editor, window, cx| {
15649 editor.delete_line(&DeleteLine, window, cx);
15650 });
15651 executor.run_until_parked();
15652
15653 cx.assert_state_with_diff(
15654 r#"
15655 use some::mod1;
15656 use some::mod2;
15657
15658 const A: u32 = 42;
15659 + const B: u32 = 42;
15660 + const C: u32 = 42;
15661 + const D: u32 = 42;
15662 + const E: u32 = 42;
15663 ˇ
15664 fn main() {
15665 println!("hello");
15666
15667 println!("world");
15668 }
15669 "#
15670 .unindent(),
15671 );
15672
15673 cx.update_editor(|editor, window, cx| {
15674 editor.move_up(&MoveUp, window, cx);
15675 editor.delete_line(&DeleteLine, window, cx);
15676 editor.move_up(&MoveUp, window, cx);
15677 editor.delete_line(&DeleteLine, window, cx);
15678 editor.move_up(&MoveUp, window, cx);
15679 editor.delete_line(&DeleteLine, window, cx);
15680 });
15681 executor.run_until_parked();
15682 cx.assert_state_with_diff(
15683 r#"
15684 use some::mod1;
15685 use some::mod2;
15686
15687 const A: u32 = 42;
15688 + const B: u32 = 42;
15689 ˇ
15690 fn main() {
15691 println!("hello");
15692
15693 println!("world");
15694 }
15695 "#
15696 .unindent(),
15697 );
15698
15699 cx.update_editor(|editor, window, cx| {
15700 editor.select_up_by_lines(&SelectUpByLines { lines: 5 }, window, cx);
15701 editor.delete_line(&DeleteLine, window, cx);
15702 });
15703 executor.run_until_parked();
15704 cx.assert_state_with_diff(
15705 r#"
15706 ˇ
15707 fn main() {
15708 println!("hello");
15709
15710 println!("world");
15711 }
15712 "#
15713 .unindent(),
15714 );
15715}
15716
15717#[gpui::test]
15718async fn test_toggling_adjacent_diff_hunks(cx: &mut TestAppContext) {
15719 init_test(cx, |_| {});
15720
15721 let mut cx = EditorTestContext::new(cx).await;
15722 cx.set_head_text(indoc! { "
15723 one
15724 two
15725 three
15726 four
15727 five
15728 "
15729 });
15730 cx.set_state(indoc! { "
15731 one
15732 ˇthree
15733 five
15734 "});
15735 cx.run_until_parked();
15736 cx.update_editor(|editor, window, cx| {
15737 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15738 });
15739 cx.assert_state_with_diff(
15740 indoc! { "
15741 one
15742 - two
15743 ˇthree
15744 - four
15745 five
15746 "}
15747 .to_string(),
15748 );
15749 cx.update_editor(|editor, window, cx| {
15750 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15751 });
15752
15753 cx.assert_state_with_diff(
15754 indoc! { "
15755 one
15756 ˇthree
15757 five
15758 "}
15759 .to_string(),
15760 );
15761
15762 cx.set_state(indoc! { "
15763 one
15764 ˇTWO
15765 three
15766 four
15767 five
15768 "});
15769 cx.run_until_parked();
15770 cx.update_editor(|editor, window, cx| {
15771 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15772 });
15773
15774 cx.assert_state_with_diff(
15775 indoc! { "
15776 one
15777 - two
15778 + ˇTWO
15779 three
15780 four
15781 five
15782 "}
15783 .to_string(),
15784 );
15785 cx.update_editor(|editor, window, cx| {
15786 editor.move_up(&Default::default(), window, cx);
15787 editor.toggle_selected_diff_hunks(&Default::default(), window, cx);
15788 });
15789 cx.assert_state_with_diff(
15790 indoc! { "
15791 one
15792 ˇTWO
15793 three
15794 four
15795 five
15796 "}
15797 .to_string(),
15798 );
15799}
15800
15801#[gpui::test]
15802async fn test_edits_around_expanded_deletion_hunks(
15803 executor: BackgroundExecutor,
15804 cx: &mut TestAppContext,
15805) {
15806 init_test(cx, |_| {});
15807
15808 let mut cx = EditorTestContext::new(cx).await;
15809
15810 let diff_base = r#"
15811 use some::mod1;
15812 use some::mod2;
15813
15814 const A: u32 = 42;
15815 const B: u32 = 42;
15816 const C: u32 = 42;
15817
15818
15819 fn main() {
15820 println!("hello");
15821
15822 println!("world");
15823 }
15824 "#
15825 .unindent();
15826 executor.run_until_parked();
15827 cx.set_state(
15828 &r#"
15829 use some::mod1;
15830 use some::mod2;
15831
15832 ˇconst B: u32 = 42;
15833 const C: u32 = 42;
15834
15835
15836 fn main() {
15837 println!("hello");
15838
15839 println!("world");
15840 }
15841 "#
15842 .unindent(),
15843 );
15844
15845 cx.set_head_text(&diff_base);
15846 executor.run_until_parked();
15847
15848 cx.update_editor(|editor, window, cx| {
15849 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15850 });
15851 executor.run_until_parked();
15852
15853 cx.assert_state_with_diff(
15854 r#"
15855 use some::mod1;
15856 use some::mod2;
15857
15858 - const A: u32 = 42;
15859 ˇconst B: u32 = 42;
15860 const C: u32 = 42;
15861
15862
15863 fn main() {
15864 println!("hello");
15865
15866 println!("world");
15867 }
15868 "#
15869 .unindent(),
15870 );
15871
15872 cx.update_editor(|editor, window, cx| {
15873 editor.delete_line(&DeleteLine, window, cx);
15874 });
15875 executor.run_until_parked();
15876 cx.assert_state_with_diff(
15877 r#"
15878 use some::mod1;
15879 use some::mod2;
15880
15881 - const A: u32 = 42;
15882 - const B: u32 = 42;
15883 ˇconst C: u32 = 42;
15884
15885
15886 fn main() {
15887 println!("hello");
15888
15889 println!("world");
15890 }
15891 "#
15892 .unindent(),
15893 );
15894
15895 cx.update_editor(|editor, window, cx| {
15896 editor.delete_line(&DeleteLine, window, cx);
15897 });
15898 executor.run_until_parked();
15899 cx.assert_state_with_diff(
15900 r#"
15901 use some::mod1;
15902 use some::mod2;
15903
15904 - const A: u32 = 42;
15905 - const B: u32 = 42;
15906 - const C: u32 = 42;
15907 ˇ
15908
15909 fn main() {
15910 println!("hello");
15911
15912 println!("world");
15913 }
15914 "#
15915 .unindent(),
15916 );
15917
15918 cx.update_editor(|editor, window, cx| {
15919 editor.handle_input("replacement", window, cx);
15920 });
15921 executor.run_until_parked();
15922 cx.assert_state_with_diff(
15923 r#"
15924 use some::mod1;
15925 use some::mod2;
15926
15927 - const A: u32 = 42;
15928 - const B: u32 = 42;
15929 - const C: u32 = 42;
15930 -
15931 + replacementˇ
15932
15933 fn main() {
15934 println!("hello");
15935
15936 println!("world");
15937 }
15938 "#
15939 .unindent(),
15940 );
15941}
15942
15943#[gpui::test]
15944async fn test_backspace_after_deletion_hunk(executor: BackgroundExecutor, cx: &mut TestAppContext) {
15945 init_test(cx, |_| {});
15946
15947 let mut cx = EditorTestContext::new(cx).await;
15948
15949 let base_text = r#"
15950 one
15951 two
15952 three
15953 four
15954 five
15955 "#
15956 .unindent();
15957 executor.run_until_parked();
15958 cx.set_state(
15959 &r#"
15960 one
15961 two
15962 fˇour
15963 five
15964 "#
15965 .unindent(),
15966 );
15967
15968 cx.set_head_text(&base_text);
15969 executor.run_until_parked();
15970
15971 cx.update_editor(|editor, window, cx| {
15972 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
15973 });
15974 executor.run_until_parked();
15975
15976 cx.assert_state_with_diff(
15977 r#"
15978 one
15979 two
15980 - three
15981 fˇour
15982 five
15983 "#
15984 .unindent(),
15985 );
15986
15987 cx.update_editor(|editor, window, cx| {
15988 editor.backspace(&Backspace, window, cx);
15989 editor.backspace(&Backspace, window, cx);
15990 });
15991 executor.run_until_parked();
15992 cx.assert_state_with_diff(
15993 r#"
15994 one
15995 two
15996 - threeˇ
15997 - four
15998 + our
15999 five
16000 "#
16001 .unindent(),
16002 );
16003}
16004
16005#[gpui::test]
16006async fn test_edit_after_expanded_modification_hunk(
16007 executor: BackgroundExecutor,
16008 cx: &mut TestAppContext,
16009) {
16010 init_test(cx, |_| {});
16011
16012 let mut cx = EditorTestContext::new(cx).await;
16013
16014 let diff_base = r#"
16015 use some::mod1;
16016 use some::mod2;
16017
16018 const A: u32 = 42;
16019 const B: u32 = 42;
16020 const C: u32 = 42;
16021 const D: u32 = 42;
16022
16023
16024 fn main() {
16025 println!("hello");
16026
16027 println!("world");
16028 }"#
16029 .unindent();
16030
16031 cx.set_state(
16032 &r#"
16033 use some::mod1;
16034 use some::mod2;
16035
16036 const A: u32 = 42;
16037 const B: u32 = 42;
16038 const C: u32 = 43ˇ
16039 const D: u32 = 42;
16040
16041
16042 fn main() {
16043 println!("hello");
16044
16045 println!("world");
16046 }"#
16047 .unindent(),
16048 );
16049
16050 cx.set_head_text(&diff_base);
16051 executor.run_until_parked();
16052 cx.update_editor(|editor, window, cx| {
16053 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16054 });
16055 executor.run_until_parked();
16056
16057 cx.assert_state_with_diff(
16058 r#"
16059 use some::mod1;
16060 use some::mod2;
16061
16062 const A: u32 = 42;
16063 const B: u32 = 42;
16064 - const C: u32 = 42;
16065 + const C: u32 = 43ˇ
16066 const D: u32 = 42;
16067
16068
16069 fn main() {
16070 println!("hello");
16071
16072 println!("world");
16073 }"#
16074 .unindent(),
16075 );
16076
16077 cx.update_editor(|editor, window, cx| {
16078 editor.handle_input("\nnew_line\n", window, cx);
16079 });
16080 executor.run_until_parked();
16081
16082 cx.assert_state_with_diff(
16083 r#"
16084 use some::mod1;
16085 use some::mod2;
16086
16087 const A: u32 = 42;
16088 const B: u32 = 42;
16089 - const C: u32 = 42;
16090 + const C: u32 = 43
16091 + new_line
16092 + ˇ
16093 const D: u32 = 42;
16094
16095
16096 fn main() {
16097 println!("hello");
16098
16099 println!("world");
16100 }"#
16101 .unindent(),
16102 );
16103}
16104
16105#[gpui::test]
16106async fn test_stage_and_unstage_added_file_hunk(
16107 executor: BackgroundExecutor,
16108 cx: &mut TestAppContext,
16109) {
16110 init_test(cx, |_| {});
16111
16112 let mut cx = EditorTestContext::new(cx).await;
16113 cx.update_editor(|editor, _, cx| {
16114 editor.set_expand_all_diff_hunks(cx);
16115 });
16116
16117 let working_copy = r#"
16118 ˇfn main() {
16119 println!("hello, world!");
16120 }
16121 "#
16122 .unindent();
16123
16124 cx.set_state(&working_copy);
16125 executor.run_until_parked();
16126
16127 cx.assert_state_with_diff(
16128 r#"
16129 + ˇfn main() {
16130 + println!("hello, world!");
16131 + }
16132 "#
16133 .unindent(),
16134 );
16135 cx.assert_index_text(None);
16136
16137 cx.update_editor(|editor, window, cx| {
16138 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16139 });
16140 executor.run_until_parked();
16141 cx.assert_index_text(Some(&working_copy.replace("ˇ", "")));
16142 cx.assert_state_with_diff(
16143 r#"
16144 + ˇfn main() {
16145 + println!("hello, world!");
16146 + }
16147 "#
16148 .unindent(),
16149 );
16150
16151 cx.update_editor(|editor, window, cx| {
16152 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
16153 });
16154 executor.run_until_parked();
16155 cx.assert_index_text(None);
16156}
16157
16158async fn setup_indent_guides_editor(
16159 text: &str,
16160 cx: &mut TestAppContext,
16161) -> (BufferId, EditorTestContext) {
16162 init_test(cx, |_| {});
16163
16164 let mut cx = EditorTestContext::new(cx).await;
16165
16166 let buffer_id = cx.update_editor(|editor, window, cx| {
16167 editor.set_text(text, window, cx);
16168 let buffer_ids = editor.buffer().read(cx).excerpt_buffer_ids();
16169
16170 buffer_ids[0]
16171 });
16172
16173 (buffer_id, cx)
16174}
16175
16176fn assert_indent_guides(
16177 range: Range<u32>,
16178 expected: Vec<IndentGuide>,
16179 active_indices: Option<Vec<usize>>,
16180 cx: &mut EditorTestContext,
16181) {
16182 let indent_guides = cx.update_editor(|editor, window, cx| {
16183 let snapshot = editor.snapshot(window, cx).display_snapshot;
16184 let mut indent_guides: Vec<_> = crate::indent_guides::indent_guides_in_range(
16185 editor,
16186 MultiBufferRow(range.start)..MultiBufferRow(range.end),
16187 true,
16188 &snapshot,
16189 cx,
16190 );
16191
16192 indent_guides.sort_by(|a, b| {
16193 a.depth.cmp(&b.depth).then(
16194 a.start_row
16195 .cmp(&b.start_row)
16196 .then(a.end_row.cmp(&b.end_row)),
16197 )
16198 });
16199 indent_guides
16200 });
16201
16202 if let Some(expected) = active_indices {
16203 let active_indices = cx.update_editor(|editor, window, cx| {
16204 let snapshot = editor.snapshot(window, cx).display_snapshot;
16205 editor.find_active_indent_guide_indices(&indent_guides, &snapshot, window, cx)
16206 });
16207
16208 assert_eq!(
16209 active_indices.unwrap().into_iter().collect::<Vec<_>>(),
16210 expected,
16211 "Active indent guide indices do not match"
16212 );
16213 }
16214
16215 assert_eq!(indent_guides, expected, "Indent guides do not match");
16216}
16217
16218fn indent_guide(buffer_id: BufferId, start_row: u32, end_row: u32, depth: u32) -> IndentGuide {
16219 IndentGuide {
16220 buffer_id,
16221 start_row: MultiBufferRow(start_row),
16222 end_row: MultiBufferRow(end_row),
16223 depth,
16224 tab_size: 4,
16225 settings: IndentGuideSettings {
16226 enabled: true,
16227 line_width: 1,
16228 active_line_width: 1,
16229 ..Default::default()
16230 },
16231 }
16232}
16233
16234#[gpui::test]
16235async fn test_indent_guide_single_line(cx: &mut TestAppContext) {
16236 let (buffer_id, mut cx) = setup_indent_guides_editor(
16237 &"
16238 fn main() {
16239 let a = 1;
16240 }"
16241 .unindent(),
16242 cx,
16243 )
16244 .await;
16245
16246 assert_indent_guides(0..3, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16247}
16248
16249#[gpui::test]
16250async fn test_indent_guide_simple_block(cx: &mut TestAppContext) {
16251 let (buffer_id, mut cx) = setup_indent_guides_editor(
16252 &"
16253 fn main() {
16254 let a = 1;
16255 let b = 2;
16256 }"
16257 .unindent(),
16258 cx,
16259 )
16260 .await;
16261
16262 assert_indent_guides(0..4, vec![indent_guide(buffer_id, 1, 2, 0)], None, &mut cx);
16263}
16264
16265#[gpui::test]
16266async fn test_indent_guide_nested(cx: &mut TestAppContext) {
16267 let (buffer_id, mut cx) = setup_indent_guides_editor(
16268 &"
16269 fn main() {
16270 let a = 1;
16271 if a == 3 {
16272 let b = 2;
16273 } else {
16274 let c = 3;
16275 }
16276 }"
16277 .unindent(),
16278 cx,
16279 )
16280 .await;
16281
16282 assert_indent_guides(
16283 0..8,
16284 vec![
16285 indent_guide(buffer_id, 1, 6, 0),
16286 indent_guide(buffer_id, 3, 3, 1),
16287 indent_guide(buffer_id, 5, 5, 1),
16288 ],
16289 None,
16290 &mut cx,
16291 );
16292}
16293
16294#[gpui::test]
16295async fn test_indent_guide_tab(cx: &mut TestAppContext) {
16296 let (buffer_id, mut cx) = setup_indent_guides_editor(
16297 &"
16298 fn main() {
16299 let a = 1;
16300 let b = 2;
16301 let c = 3;
16302 }"
16303 .unindent(),
16304 cx,
16305 )
16306 .await;
16307
16308 assert_indent_guides(
16309 0..5,
16310 vec![
16311 indent_guide(buffer_id, 1, 3, 0),
16312 indent_guide(buffer_id, 2, 2, 1),
16313 ],
16314 None,
16315 &mut cx,
16316 );
16317}
16318
16319#[gpui::test]
16320async fn test_indent_guide_continues_on_empty_line(cx: &mut TestAppContext) {
16321 let (buffer_id, mut cx) = setup_indent_guides_editor(
16322 &"
16323 fn main() {
16324 let a = 1;
16325
16326 let c = 3;
16327 }"
16328 .unindent(),
16329 cx,
16330 )
16331 .await;
16332
16333 assert_indent_guides(0..5, vec![indent_guide(buffer_id, 1, 3, 0)], None, &mut cx);
16334}
16335
16336#[gpui::test]
16337async fn test_indent_guide_complex(cx: &mut TestAppContext) {
16338 let (buffer_id, mut cx) = setup_indent_guides_editor(
16339 &"
16340 fn main() {
16341 let a = 1;
16342
16343 let c = 3;
16344
16345 if a == 3 {
16346 let b = 2;
16347 } else {
16348 let c = 3;
16349 }
16350 }"
16351 .unindent(),
16352 cx,
16353 )
16354 .await;
16355
16356 assert_indent_guides(
16357 0..11,
16358 vec![
16359 indent_guide(buffer_id, 1, 9, 0),
16360 indent_guide(buffer_id, 6, 6, 1),
16361 indent_guide(buffer_id, 8, 8, 1),
16362 ],
16363 None,
16364 &mut cx,
16365 );
16366}
16367
16368#[gpui::test]
16369async fn test_indent_guide_starts_off_screen(cx: &mut TestAppContext) {
16370 let (buffer_id, mut cx) = setup_indent_guides_editor(
16371 &"
16372 fn main() {
16373 let a = 1;
16374
16375 let c = 3;
16376
16377 if a == 3 {
16378 let b = 2;
16379 } else {
16380 let c = 3;
16381 }
16382 }"
16383 .unindent(),
16384 cx,
16385 )
16386 .await;
16387
16388 assert_indent_guides(
16389 1..11,
16390 vec![
16391 indent_guide(buffer_id, 1, 9, 0),
16392 indent_guide(buffer_id, 6, 6, 1),
16393 indent_guide(buffer_id, 8, 8, 1),
16394 ],
16395 None,
16396 &mut cx,
16397 );
16398}
16399
16400#[gpui::test]
16401async fn test_indent_guide_ends_off_screen(cx: &mut TestAppContext) {
16402 let (buffer_id, mut cx) = setup_indent_guides_editor(
16403 &"
16404 fn main() {
16405 let a = 1;
16406
16407 let c = 3;
16408
16409 if a == 3 {
16410 let b = 2;
16411 } else {
16412 let c = 3;
16413 }
16414 }"
16415 .unindent(),
16416 cx,
16417 )
16418 .await;
16419
16420 assert_indent_guides(
16421 1..10,
16422 vec![
16423 indent_guide(buffer_id, 1, 9, 0),
16424 indent_guide(buffer_id, 6, 6, 1),
16425 indent_guide(buffer_id, 8, 8, 1),
16426 ],
16427 None,
16428 &mut cx,
16429 );
16430}
16431
16432#[gpui::test]
16433async fn test_indent_guide_without_brackets(cx: &mut TestAppContext) {
16434 let (buffer_id, mut cx) = setup_indent_guides_editor(
16435 &"
16436 block1
16437 block2
16438 block3
16439 block4
16440 block2
16441 block1
16442 block1"
16443 .unindent(),
16444 cx,
16445 )
16446 .await;
16447
16448 assert_indent_guides(
16449 1..10,
16450 vec![
16451 indent_guide(buffer_id, 1, 4, 0),
16452 indent_guide(buffer_id, 2, 3, 1),
16453 indent_guide(buffer_id, 3, 3, 2),
16454 ],
16455 None,
16456 &mut cx,
16457 );
16458}
16459
16460#[gpui::test]
16461async fn test_indent_guide_ends_before_empty_line(cx: &mut TestAppContext) {
16462 let (buffer_id, mut cx) = setup_indent_guides_editor(
16463 &"
16464 block1
16465 block2
16466 block3
16467
16468 block1
16469 block1"
16470 .unindent(),
16471 cx,
16472 )
16473 .await;
16474
16475 assert_indent_guides(
16476 0..6,
16477 vec![
16478 indent_guide(buffer_id, 1, 2, 0),
16479 indent_guide(buffer_id, 2, 2, 1),
16480 ],
16481 None,
16482 &mut cx,
16483 );
16484}
16485
16486#[gpui::test]
16487async fn test_indent_guide_continuing_off_screen(cx: &mut TestAppContext) {
16488 let (buffer_id, mut cx) = setup_indent_guides_editor(
16489 &"
16490 block1
16491
16492
16493
16494 block2
16495 "
16496 .unindent(),
16497 cx,
16498 )
16499 .await;
16500
16501 assert_indent_guides(0..1, vec![indent_guide(buffer_id, 1, 1, 0)], None, &mut cx);
16502}
16503
16504#[gpui::test]
16505async fn test_indent_guide_tabs(cx: &mut TestAppContext) {
16506 let (buffer_id, mut cx) = setup_indent_guides_editor(
16507 &"
16508 def a:
16509 \tb = 3
16510 \tif True:
16511 \t\tc = 4
16512 \t\td = 5
16513 \tprint(b)
16514 "
16515 .unindent(),
16516 cx,
16517 )
16518 .await;
16519
16520 assert_indent_guides(
16521 0..6,
16522 vec![
16523 indent_guide(buffer_id, 1, 5, 0),
16524 indent_guide(buffer_id, 3, 4, 1),
16525 ],
16526 None,
16527 &mut cx,
16528 );
16529}
16530
16531#[gpui::test]
16532async fn test_active_indent_guide_single_line(cx: &mut TestAppContext) {
16533 let (buffer_id, mut cx) = setup_indent_guides_editor(
16534 &"
16535 fn main() {
16536 let a = 1;
16537 }"
16538 .unindent(),
16539 cx,
16540 )
16541 .await;
16542
16543 cx.update_editor(|editor, window, cx| {
16544 editor.change_selections(None, window, cx, |s| {
16545 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16546 });
16547 });
16548
16549 assert_indent_guides(
16550 0..3,
16551 vec![indent_guide(buffer_id, 1, 1, 0)],
16552 Some(vec![0]),
16553 &mut cx,
16554 );
16555}
16556
16557#[gpui::test]
16558async fn test_active_indent_guide_respect_indented_range(cx: &mut TestAppContext) {
16559 let (buffer_id, mut cx) = setup_indent_guides_editor(
16560 &"
16561 fn main() {
16562 if 1 == 2 {
16563 let a = 1;
16564 }
16565 }"
16566 .unindent(),
16567 cx,
16568 )
16569 .await;
16570
16571 cx.update_editor(|editor, window, cx| {
16572 editor.change_selections(None, window, cx, |s| {
16573 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16574 });
16575 });
16576
16577 assert_indent_guides(
16578 0..4,
16579 vec![
16580 indent_guide(buffer_id, 1, 3, 0),
16581 indent_guide(buffer_id, 2, 2, 1),
16582 ],
16583 Some(vec![1]),
16584 &mut cx,
16585 );
16586
16587 cx.update_editor(|editor, window, cx| {
16588 editor.change_selections(None, window, cx, |s| {
16589 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16590 });
16591 });
16592
16593 assert_indent_guides(
16594 0..4,
16595 vec![
16596 indent_guide(buffer_id, 1, 3, 0),
16597 indent_guide(buffer_id, 2, 2, 1),
16598 ],
16599 Some(vec![1]),
16600 &mut cx,
16601 );
16602
16603 cx.update_editor(|editor, window, cx| {
16604 editor.change_selections(None, window, cx, |s| {
16605 s.select_ranges([Point::new(3, 0)..Point::new(3, 0)])
16606 });
16607 });
16608
16609 assert_indent_guides(
16610 0..4,
16611 vec![
16612 indent_guide(buffer_id, 1, 3, 0),
16613 indent_guide(buffer_id, 2, 2, 1),
16614 ],
16615 Some(vec![0]),
16616 &mut cx,
16617 );
16618}
16619
16620#[gpui::test]
16621async fn test_active_indent_guide_empty_line(cx: &mut TestAppContext) {
16622 let (buffer_id, mut cx) = setup_indent_guides_editor(
16623 &"
16624 fn main() {
16625 let a = 1;
16626
16627 let b = 2;
16628 }"
16629 .unindent(),
16630 cx,
16631 )
16632 .await;
16633
16634 cx.update_editor(|editor, window, cx| {
16635 editor.change_selections(None, window, cx, |s| {
16636 s.select_ranges([Point::new(2, 0)..Point::new(2, 0)])
16637 });
16638 });
16639
16640 assert_indent_guides(
16641 0..5,
16642 vec![indent_guide(buffer_id, 1, 3, 0)],
16643 Some(vec![0]),
16644 &mut cx,
16645 );
16646}
16647
16648#[gpui::test]
16649async fn test_active_indent_guide_non_matching_indent(cx: &mut TestAppContext) {
16650 let (buffer_id, mut cx) = setup_indent_guides_editor(
16651 &"
16652 def m:
16653 a = 1
16654 pass"
16655 .unindent(),
16656 cx,
16657 )
16658 .await;
16659
16660 cx.update_editor(|editor, window, cx| {
16661 editor.change_selections(None, window, cx, |s| {
16662 s.select_ranges([Point::new(1, 0)..Point::new(1, 0)])
16663 });
16664 });
16665
16666 assert_indent_guides(
16667 0..3,
16668 vec![indent_guide(buffer_id, 1, 2, 0)],
16669 Some(vec![0]),
16670 &mut cx,
16671 );
16672}
16673
16674#[gpui::test]
16675async fn test_indent_guide_with_expanded_diff_hunks(cx: &mut TestAppContext) {
16676 init_test(cx, |_| {});
16677 let mut cx = EditorTestContext::new(cx).await;
16678 let text = indoc! {
16679 "
16680 impl A {
16681 fn b() {
16682 0;
16683 3;
16684 5;
16685 6;
16686 7;
16687 }
16688 }
16689 "
16690 };
16691 let base_text = indoc! {
16692 "
16693 impl A {
16694 fn b() {
16695 0;
16696 1;
16697 2;
16698 3;
16699 4;
16700 }
16701 fn c() {
16702 5;
16703 6;
16704 7;
16705 }
16706 }
16707 "
16708 };
16709
16710 cx.update_editor(|editor, window, cx| {
16711 editor.set_text(text, window, cx);
16712
16713 editor.buffer().update(cx, |multibuffer, cx| {
16714 let buffer = multibuffer.as_singleton().unwrap();
16715 let diff = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer, cx));
16716
16717 multibuffer.set_all_diff_hunks_expanded(cx);
16718 multibuffer.add_diff(diff, cx);
16719
16720 buffer.read(cx).remote_id()
16721 })
16722 });
16723 cx.run_until_parked();
16724
16725 cx.assert_state_with_diff(
16726 indoc! { "
16727 impl A {
16728 fn b() {
16729 0;
16730 - 1;
16731 - 2;
16732 3;
16733 - 4;
16734 - }
16735 - fn c() {
16736 5;
16737 6;
16738 7;
16739 }
16740 }
16741 ˇ"
16742 }
16743 .to_string(),
16744 );
16745
16746 let mut actual_guides = cx.update_editor(|editor, window, cx| {
16747 editor
16748 .snapshot(window, cx)
16749 .buffer_snapshot
16750 .indent_guides_in_range(Anchor::min()..Anchor::max(), false, cx)
16751 .map(|guide| (guide.start_row..=guide.end_row, guide.depth))
16752 .collect::<Vec<_>>()
16753 });
16754 actual_guides.sort_by_key(|item| (*item.0.start(), item.1));
16755 assert_eq!(
16756 actual_guides,
16757 vec![
16758 (MultiBufferRow(1)..=MultiBufferRow(12), 0),
16759 (MultiBufferRow(2)..=MultiBufferRow(6), 1),
16760 (MultiBufferRow(9)..=MultiBufferRow(11), 1),
16761 ]
16762 );
16763}
16764
16765#[gpui::test]
16766async fn test_adjacent_diff_hunks(executor: BackgroundExecutor, cx: &mut TestAppContext) {
16767 init_test(cx, |_| {});
16768 let mut cx = EditorTestContext::new(cx).await;
16769
16770 let diff_base = r#"
16771 a
16772 b
16773 c
16774 "#
16775 .unindent();
16776
16777 cx.set_state(
16778 &r#"
16779 ˇA
16780 b
16781 C
16782 "#
16783 .unindent(),
16784 );
16785 cx.set_head_text(&diff_base);
16786 cx.update_editor(|editor, window, cx| {
16787 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16788 });
16789 executor.run_until_parked();
16790
16791 let both_hunks_expanded = r#"
16792 - a
16793 + ˇA
16794 b
16795 - c
16796 + C
16797 "#
16798 .unindent();
16799
16800 cx.assert_state_with_diff(both_hunks_expanded.clone());
16801
16802 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16803 let snapshot = editor.snapshot(window, cx);
16804 let hunks = editor
16805 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16806 .collect::<Vec<_>>();
16807 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16808 let buffer_id = hunks[0].buffer_id;
16809 hunks
16810 .into_iter()
16811 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16812 .collect::<Vec<_>>()
16813 });
16814 assert_eq!(hunk_ranges.len(), 2);
16815
16816 cx.update_editor(|editor, _, cx| {
16817 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16818 });
16819 executor.run_until_parked();
16820
16821 let second_hunk_expanded = r#"
16822 ˇA
16823 b
16824 - c
16825 + C
16826 "#
16827 .unindent();
16828
16829 cx.assert_state_with_diff(second_hunk_expanded);
16830
16831 cx.update_editor(|editor, _, cx| {
16832 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16833 });
16834 executor.run_until_parked();
16835
16836 cx.assert_state_with_diff(both_hunks_expanded.clone());
16837
16838 cx.update_editor(|editor, _, cx| {
16839 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16840 });
16841 executor.run_until_parked();
16842
16843 let first_hunk_expanded = r#"
16844 - a
16845 + ˇA
16846 b
16847 C
16848 "#
16849 .unindent();
16850
16851 cx.assert_state_with_diff(first_hunk_expanded);
16852
16853 cx.update_editor(|editor, _, cx| {
16854 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16855 });
16856 executor.run_until_parked();
16857
16858 cx.assert_state_with_diff(both_hunks_expanded);
16859
16860 cx.set_state(
16861 &r#"
16862 ˇA
16863 b
16864 "#
16865 .unindent(),
16866 );
16867 cx.run_until_parked();
16868
16869 // TODO this cursor position seems bad
16870 cx.assert_state_with_diff(
16871 r#"
16872 - ˇa
16873 + A
16874 b
16875 "#
16876 .unindent(),
16877 );
16878
16879 cx.update_editor(|editor, window, cx| {
16880 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16881 });
16882
16883 cx.assert_state_with_diff(
16884 r#"
16885 - ˇa
16886 + A
16887 b
16888 - c
16889 "#
16890 .unindent(),
16891 );
16892
16893 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16894 let snapshot = editor.snapshot(window, cx);
16895 let hunks = editor
16896 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16897 .collect::<Vec<_>>();
16898 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16899 let buffer_id = hunks[0].buffer_id;
16900 hunks
16901 .into_iter()
16902 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16903 .collect::<Vec<_>>()
16904 });
16905 assert_eq!(hunk_ranges.len(), 2);
16906
16907 cx.update_editor(|editor, _, cx| {
16908 editor.toggle_single_diff_hunk(hunk_ranges[1].clone(), cx);
16909 });
16910 executor.run_until_parked();
16911
16912 cx.assert_state_with_diff(
16913 r#"
16914 - ˇa
16915 + A
16916 b
16917 "#
16918 .unindent(),
16919 );
16920}
16921
16922#[gpui::test]
16923async fn test_toggle_deletion_hunk_at_start_of_file(
16924 executor: BackgroundExecutor,
16925 cx: &mut TestAppContext,
16926) {
16927 init_test(cx, |_| {});
16928 let mut cx = EditorTestContext::new(cx).await;
16929
16930 let diff_base = r#"
16931 a
16932 b
16933 c
16934 "#
16935 .unindent();
16936
16937 cx.set_state(
16938 &r#"
16939 ˇb
16940 c
16941 "#
16942 .unindent(),
16943 );
16944 cx.set_head_text(&diff_base);
16945 cx.update_editor(|editor, window, cx| {
16946 editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
16947 });
16948 executor.run_until_parked();
16949
16950 let hunk_expanded = r#"
16951 - a
16952 ˇb
16953 c
16954 "#
16955 .unindent();
16956
16957 cx.assert_state_with_diff(hunk_expanded.clone());
16958
16959 let hunk_ranges = cx.update_editor(|editor, window, cx| {
16960 let snapshot = editor.snapshot(window, cx);
16961 let hunks = editor
16962 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
16963 .collect::<Vec<_>>();
16964 let excerpt_id = editor.buffer.read(cx).excerpt_ids()[0];
16965 let buffer_id = hunks[0].buffer_id;
16966 hunks
16967 .into_iter()
16968 .map(|hunk| Anchor::range_in_buffer(excerpt_id, buffer_id, hunk.buffer_range.clone()))
16969 .collect::<Vec<_>>()
16970 });
16971 assert_eq!(hunk_ranges.len(), 1);
16972
16973 cx.update_editor(|editor, _, cx| {
16974 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16975 });
16976 executor.run_until_parked();
16977
16978 let hunk_collapsed = r#"
16979 ˇb
16980 c
16981 "#
16982 .unindent();
16983
16984 cx.assert_state_with_diff(hunk_collapsed);
16985
16986 cx.update_editor(|editor, _, cx| {
16987 editor.toggle_single_diff_hunk(hunk_ranges[0].clone(), cx);
16988 });
16989 executor.run_until_parked();
16990
16991 cx.assert_state_with_diff(hunk_expanded.clone());
16992}
16993
16994#[gpui::test]
16995async fn test_display_diff_hunks(cx: &mut TestAppContext) {
16996 init_test(cx, |_| {});
16997
16998 let fs = FakeFs::new(cx.executor());
16999 fs.insert_tree(
17000 path!("/test"),
17001 json!({
17002 ".git": {},
17003 "file-1": "ONE\n",
17004 "file-2": "TWO\n",
17005 "file-3": "THREE\n",
17006 }),
17007 )
17008 .await;
17009
17010 fs.set_head_for_repo(
17011 path!("/test/.git").as_ref(),
17012 &[
17013 ("file-1".into(), "one\n".into()),
17014 ("file-2".into(), "two\n".into()),
17015 ("file-3".into(), "three\n".into()),
17016 ],
17017 );
17018
17019 let project = Project::test(fs, [path!("/test").as_ref()], cx).await;
17020 let mut buffers = vec![];
17021 for i in 1..=3 {
17022 let buffer = project
17023 .update(cx, |project, cx| {
17024 let path = format!(path!("/test/file-{}"), i);
17025 project.open_local_buffer(path, cx)
17026 })
17027 .await
17028 .unwrap();
17029 buffers.push(buffer);
17030 }
17031
17032 let multibuffer = cx.new(|cx| {
17033 let mut multibuffer = MultiBuffer::new(Capability::ReadWrite);
17034 multibuffer.set_all_diff_hunks_expanded(cx);
17035 for buffer in &buffers {
17036 let snapshot = buffer.read(cx).snapshot();
17037 multibuffer.set_excerpts_for_path(
17038 PathKey::namespaced(0, buffer.read(cx).file().unwrap().path().clone()),
17039 buffer.clone(),
17040 vec![text::Anchor::MIN.to_point(&snapshot)..text::Anchor::MAX.to_point(&snapshot)],
17041 DEFAULT_MULTIBUFFER_CONTEXT,
17042 cx,
17043 );
17044 }
17045 multibuffer
17046 });
17047
17048 let editor = cx.add_window(|window, cx| {
17049 Editor::new(EditorMode::full(), multibuffer, Some(project), window, cx)
17050 });
17051 cx.run_until_parked();
17052
17053 let snapshot = editor
17054 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17055 .unwrap();
17056 let hunks = snapshot
17057 .display_diff_hunks_for_rows(DisplayRow(0)..DisplayRow(u32::MAX), &Default::default())
17058 .map(|hunk| match hunk {
17059 DisplayDiffHunk::Unfolded {
17060 display_row_range, ..
17061 } => display_row_range,
17062 DisplayDiffHunk::Folded { .. } => unreachable!(),
17063 })
17064 .collect::<Vec<_>>();
17065 assert_eq!(
17066 hunks,
17067 [
17068 DisplayRow(2)..DisplayRow(4),
17069 DisplayRow(7)..DisplayRow(9),
17070 DisplayRow(12)..DisplayRow(14),
17071 ]
17072 );
17073}
17074
17075#[gpui::test]
17076async fn test_partially_staged_hunk(cx: &mut TestAppContext) {
17077 init_test(cx, |_| {});
17078
17079 let mut cx = EditorTestContext::new(cx).await;
17080 cx.set_head_text(indoc! { "
17081 one
17082 two
17083 three
17084 four
17085 five
17086 "
17087 });
17088 cx.set_index_text(indoc! { "
17089 one
17090 two
17091 three
17092 four
17093 five
17094 "
17095 });
17096 cx.set_state(indoc! {"
17097 one
17098 TWO
17099 ˇTHREE
17100 FOUR
17101 five
17102 "});
17103 cx.run_until_parked();
17104 cx.update_editor(|editor, window, cx| {
17105 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17106 });
17107 cx.run_until_parked();
17108 cx.assert_index_text(Some(indoc! {"
17109 one
17110 TWO
17111 THREE
17112 FOUR
17113 five
17114 "}));
17115 cx.set_state(indoc! { "
17116 one
17117 TWO
17118 ˇTHREE-HUNDRED
17119 FOUR
17120 five
17121 "});
17122 cx.run_until_parked();
17123 cx.update_editor(|editor, window, cx| {
17124 let snapshot = editor.snapshot(window, cx);
17125 let hunks = editor
17126 .diff_hunks_in_ranges(&[Anchor::min()..Anchor::max()], &snapshot.buffer_snapshot)
17127 .collect::<Vec<_>>();
17128 assert_eq!(hunks.len(), 1);
17129 assert_eq!(
17130 hunks[0].status(),
17131 DiffHunkStatus {
17132 kind: DiffHunkStatusKind::Modified,
17133 secondary: DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk
17134 }
17135 );
17136
17137 editor.toggle_staged_selected_diff_hunks(&Default::default(), window, cx);
17138 });
17139 cx.run_until_parked();
17140 cx.assert_index_text(Some(indoc! {"
17141 one
17142 TWO
17143 THREE-HUNDRED
17144 FOUR
17145 five
17146 "}));
17147}
17148
17149#[gpui::test]
17150fn test_crease_insertion_and_rendering(cx: &mut TestAppContext) {
17151 init_test(cx, |_| {});
17152
17153 let editor = cx.add_window(|window, cx| {
17154 let buffer = MultiBuffer::build_simple("aaaaaa\nbbbbbb\ncccccc\nddddddd\n", cx);
17155 build_editor(buffer, window, cx)
17156 });
17157
17158 let render_args = Arc::new(Mutex::new(None));
17159 let snapshot = editor
17160 .update(cx, |editor, window, cx| {
17161 let snapshot = editor.buffer().read(cx).snapshot(cx);
17162 let range =
17163 snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(2, 6));
17164
17165 struct RenderArgs {
17166 row: MultiBufferRow,
17167 folded: bool,
17168 callback: Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
17169 }
17170
17171 let crease = Crease::inline(
17172 range,
17173 FoldPlaceholder::test(),
17174 {
17175 let toggle_callback = render_args.clone();
17176 move |row, folded, callback, _window, _cx| {
17177 *toggle_callback.lock() = Some(RenderArgs {
17178 row,
17179 folded,
17180 callback,
17181 });
17182 div()
17183 }
17184 },
17185 |_row, _folded, _window, _cx| div(),
17186 );
17187
17188 editor.insert_creases(Some(crease), cx);
17189 let snapshot = editor.snapshot(window, cx);
17190 let _div = snapshot.render_crease_toggle(
17191 MultiBufferRow(1),
17192 false,
17193 cx.entity().clone(),
17194 window,
17195 cx,
17196 );
17197 snapshot
17198 })
17199 .unwrap();
17200
17201 let render_args = render_args.lock().take().unwrap();
17202 assert_eq!(render_args.row, MultiBufferRow(1));
17203 assert!(!render_args.folded);
17204 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17205
17206 cx.update_window(*editor, |_, window, cx| {
17207 (render_args.callback)(true, window, cx)
17208 })
17209 .unwrap();
17210 let snapshot = editor
17211 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17212 .unwrap();
17213 assert!(snapshot.is_line_folded(MultiBufferRow(1)));
17214
17215 cx.update_window(*editor, |_, window, cx| {
17216 (render_args.callback)(false, window, cx)
17217 })
17218 .unwrap();
17219 let snapshot = editor
17220 .update(cx, |editor, window, cx| editor.snapshot(window, cx))
17221 .unwrap();
17222 assert!(!snapshot.is_line_folded(MultiBufferRow(1)));
17223}
17224
17225#[gpui::test]
17226async fn test_input_text(cx: &mut TestAppContext) {
17227 init_test(cx, |_| {});
17228 let mut cx = EditorTestContext::new(cx).await;
17229
17230 cx.set_state(
17231 &r#"ˇone
17232 two
17233
17234 three
17235 fourˇ
17236 five
17237
17238 siˇx"#
17239 .unindent(),
17240 );
17241
17242 cx.dispatch_action(HandleInput(String::new()));
17243 cx.assert_editor_state(
17244 &r#"ˇone
17245 two
17246
17247 three
17248 fourˇ
17249 five
17250
17251 siˇx"#
17252 .unindent(),
17253 );
17254
17255 cx.dispatch_action(HandleInput("AAAA".to_string()));
17256 cx.assert_editor_state(
17257 &r#"AAAAˇone
17258 two
17259
17260 three
17261 fourAAAAˇ
17262 five
17263
17264 siAAAAˇx"#
17265 .unindent(),
17266 );
17267}
17268
17269#[gpui::test]
17270async fn test_scroll_cursor_center_top_bottom(cx: &mut TestAppContext) {
17271 init_test(cx, |_| {});
17272
17273 let mut cx = EditorTestContext::new(cx).await;
17274 cx.set_state(
17275 r#"let foo = 1;
17276let foo = 2;
17277let foo = 3;
17278let fooˇ = 4;
17279let foo = 5;
17280let foo = 6;
17281let foo = 7;
17282let foo = 8;
17283let foo = 9;
17284let foo = 10;
17285let foo = 11;
17286let foo = 12;
17287let foo = 13;
17288let foo = 14;
17289let foo = 15;"#,
17290 );
17291
17292 cx.update_editor(|e, window, cx| {
17293 assert_eq!(
17294 e.next_scroll_position,
17295 NextScrollCursorCenterTopBottom::Center,
17296 "Default next scroll direction is center",
17297 );
17298
17299 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17300 assert_eq!(
17301 e.next_scroll_position,
17302 NextScrollCursorCenterTopBottom::Top,
17303 "After center, next scroll direction should be top",
17304 );
17305
17306 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17307 assert_eq!(
17308 e.next_scroll_position,
17309 NextScrollCursorCenterTopBottom::Bottom,
17310 "After top, next scroll direction should be bottom",
17311 );
17312
17313 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17314 assert_eq!(
17315 e.next_scroll_position,
17316 NextScrollCursorCenterTopBottom::Center,
17317 "After bottom, scrolling should start over",
17318 );
17319
17320 e.scroll_cursor_center_top_bottom(&ScrollCursorCenterTopBottom, window, cx);
17321 assert_eq!(
17322 e.next_scroll_position,
17323 NextScrollCursorCenterTopBottom::Top,
17324 "Scrolling continues if retriggered fast enough"
17325 );
17326 });
17327
17328 cx.executor()
17329 .advance_clock(SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT + Duration::from_millis(200));
17330 cx.executor().run_until_parked();
17331 cx.update_editor(|e, _, _| {
17332 assert_eq!(
17333 e.next_scroll_position,
17334 NextScrollCursorCenterTopBottom::Center,
17335 "If scrolling is not triggered fast enough, it should reset"
17336 );
17337 });
17338}
17339
17340#[gpui::test]
17341async fn test_goto_definition_with_find_all_references_fallback(cx: &mut TestAppContext) {
17342 init_test(cx, |_| {});
17343 let mut cx = EditorLspTestContext::new_rust(
17344 lsp::ServerCapabilities {
17345 definition_provider: Some(lsp::OneOf::Left(true)),
17346 references_provider: Some(lsp::OneOf::Left(true)),
17347 ..lsp::ServerCapabilities::default()
17348 },
17349 cx,
17350 )
17351 .await;
17352
17353 let set_up_lsp_handlers = |empty_go_to_definition: bool, cx: &mut EditorLspTestContext| {
17354 let go_to_definition = cx
17355 .lsp
17356 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17357 move |params, _| async move {
17358 if empty_go_to_definition {
17359 Ok(None)
17360 } else {
17361 Ok(Some(lsp::GotoDefinitionResponse::Scalar(lsp::Location {
17362 uri: params.text_document_position_params.text_document.uri,
17363 range: lsp::Range::new(
17364 lsp::Position::new(4, 3),
17365 lsp::Position::new(4, 6),
17366 ),
17367 })))
17368 }
17369 },
17370 );
17371 let references = cx
17372 .lsp
17373 .set_request_handler::<lsp::request::References, _, _>(move |params, _| async move {
17374 Ok(Some(vec![lsp::Location {
17375 uri: params.text_document_position.text_document.uri,
17376 range: lsp::Range::new(lsp::Position::new(0, 8), lsp::Position::new(0, 11)),
17377 }]))
17378 });
17379 (go_to_definition, references)
17380 };
17381
17382 cx.set_state(
17383 &r#"fn one() {
17384 let mut a = ˇtwo();
17385 }
17386
17387 fn two() {}"#
17388 .unindent(),
17389 );
17390 set_up_lsp_handlers(false, &mut cx);
17391 let navigated = cx
17392 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17393 .await
17394 .expect("Failed to navigate to definition");
17395 assert_eq!(
17396 navigated,
17397 Navigated::Yes,
17398 "Should have navigated to definition from the GetDefinition response"
17399 );
17400 cx.assert_editor_state(
17401 &r#"fn one() {
17402 let mut a = two();
17403 }
17404
17405 fn «twoˇ»() {}"#
17406 .unindent(),
17407 );
17408
17409 let editors = cx.update_workspace(|workspace, _, cx| {
17410 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17411 });
17412 cx.update_editor(|_, _, test_editor_cx| {
17413 assert_eq!(
17414 editors.len(),
17415 1,
17416 "Initially, only one, test, editor should be open in the workspace"
17417 );
17418 assert_eq!(
17419 test_editor_cx.entity(),
17420 editors.last().expect("Asserted len is 1").clone()
17421 );
17422 });
17423
17424 set_up_lsp_handlers(true, &mut cx);
17425 let navigated = cx
17426 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17427 .await
17428 .expect("Failed to navigate to lookup references");
17429 assert_eq!(
17430 navigated,
17431 Navigated::Yes,
17432 "Should have navigated to references as a fallback after empty GoToDefinition response"
17433 );
17434 // We should not change the selections in the existing file,
17435 // if opening another milti buffer with the references
17436 cx.assert_editor_state(
17437 &r#"fn one() {
17438 let mut a = two();
17439 }
17440
17441 fn «twoˇ»() {}"#
17442 .unindent(),
17443 );
17444 let editors = cx.update_workspace(|workspace, _, cx| {
17445 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17446 });
17447 cx.update_editor(|_, _, test_editor_cx| {
17448 assert_eq!(
17449 editors.len(),
17450 2,
17451 "After falling back to references search, we open a new editor with the results"
17452 );
17453 let references_fallback_text = editors
17454 .into_iter()
17455 .find(|new_editor| *new_editor != test_editor_cx.entity())
17456 .expect("Should have one non-test editor now")
17457 .read(test_editor_cx)
17458 .text(test_editor_cx);
17459 assert_eq!(
17460 references_fallback_text, "fn one() {\n let mut a = two();\n}",
17461 "Should use the range from the references response and not the GoToDefinition one"
17462 );
17463 });
17464}
17465
17466#[gpui::test]
17467async fn test_goto_definition_no_fallback(cx: &mut TestAppContext) {
17468 init_test(cx, |_| {});
17469 cx.update(|cx| {
17470 let mut editor_settings = EditorSettings::get_global(cx).clone();
17471 editor_settings.go_to_definition_fallback = GoToDefinitionFallback::None;
17472 EditorSettings::override_global(editor_settings, cx);
17473 });
17474 let mut cx = EditorLspTestContext::new_rust(
17475 lsp::ServerCapabilities {
17476 definition_provider: Some(lsp::OneOf::Left(true)),
17477 references_provider: Some(lsp::OneOf::Left(true)),
17478 ..lsp::ServerCapabilities::default()
17479 },
17480 cx,
17481 )
17482 .await;
17483 let original_state = r#"fn one() {
17484 let mut a = ˇtwo();
17485 }
17486
17487 fn two() {}"#
17488 .unindent();
17489 cx.set_state(&original_state);
17490
17491 let mut go_to_definition = cx
17492 .lsp
17493 .set_request_handler::<lsp::request::GotoDefinition, _, _>(
17494 move |_, _| async move { Ok(None) },
17495 );
17496 let _references = cx
17497 .lsp
17498 .set_request_handler::<lsp::request::References, _, _>(move |_, _| async move {
17499 panic!("Should not call for references with no go to definition fallback")
17500 });
17501
17502 let navigated = cx
17503 .update_editor(|editor, window, cx| editor.go_to_definition(&GoToDefinition, window, cx))
17504 .await
17505 .expect("Failed to navigate to lookup references");
17506 go_to_definition
17507 .next()
17508 .await
17509 .expect("Should have called the go_to_definition handler");
17510
17511 assert_eq!(
17512 navigated,
17513 Navigated::No,
17514 "Should have navigated to references as a fallback after empty GoToDefinition response"
17515 );
17516 cx.assert_editor_state(&original_state);
17517 let editors = cx.update_workspace(|workspace, _, cx| {
17518 workspace.items_of_type::<Editor>(cx).collect::<Vec<_>>()
17519 });
17520 cx.update_editor(|_, _, _| {
17521 assert_eq!(
17522 editors.len(),
17523 1,
17524 "After unsuccessful fallback, no other editor should have been opened"
17525 );
17526 });
17527}
17528
17529#[gpui::test]
17530async fn test_find_enclosing_node_with_task(cx: &mut TestAppContext) {
17531 init_test(cx, |_| {});
17532
17533 let language = Arc::new(Language::new(
17534 LanguageConfig::default(),
17535 Some(tree_sitter_rust::LANGUAGE.into()),
17536 ));
17537
17538 let text = r#"
17539 #[cfg(test)]
17540 mod tests() {
17541 #[test]
17542 fn runnable_1() {
17543 let a = 1;
17544 }
17545
17546 #[test]
17547 fn runnable_2() {
17548 let a = 1;
17549 let b = 2;
17550 }
17551 }
17552 "#
17553 .unindent();
17554
17555 let fs = FakeFs::new(cx.executor());
17556 fs.insert_file("/file.rs", Default::default()).await;
17557
17558 let project = Project::test(fs, ["/a".as_ref()], cx).await;
17559 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17560 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17561 let buffer = cx.new(|cx| Buffer::local(text, cx).with_language(language, cx));
17562 let multi_buffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
17563
17564 let editor = cx.new_window_entity(|window, cx| {
17565 Editor::new(
17566 EditorMode::full(),
17567 multi_buffer,
17568 Some(project.clone()),
17569 window,
17570 cx,
17571 )
17572 });
17573
17574 editor.update_in(cx, |editor, window, cx| {
17575 let snapshot = editor.buffer().read(cx).snapshot(cx);
17576 editor.tasks.insert(
17577 (buffer.read(cx).remote_id(), 3),
17578 RunnableTasks {
17579 templates: vec![],
17580 offset: snapshot.anchor_before(43),
17581 column: 0,
17582 extra_variables: HashMap::default(),
17583 context_range: BufferOffset(43)..BufferOffset(85),
17584 },
17585 );
17586 editor.tasks.insert(
17587 (buffer.read(cx).remote_id(), 8),
17588 RunnableTasks {
17589 templates: vec![],
17590 offset: snapshot.anchor_before(86),
17591 column: 0,
17592 extra_variables: HashMap::default(),
17593 context_range: BufferOffset(86)..BufferOffset(191),
17594 },
17595 );
17596
17597 // Test finding task when cursor is inside function body
17598 editor.change_selections(None, window, cx, |s| {
17599 s.select_ranges([Point::new(4, 5)..Point::new(4, 5)])
17600 });
17601 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17602 assert_eq!(row, 3, "Should find task for cursor inside runnable_1");
17603
17604 // Test finding task when cursor is on function name
17605 editor.change_selections(None, window, cx, |s| {
17606 s.select_ranges([Point::new(8, 4)..Point::new(8, 4)])
17607 });
17608 let (_, row, _) = editor.find_enclosing_node_task(cx).unwrap();
17609 assert_eq!(row, 8, "Should find task when cursor is on function name");
17610 });
17611}
17612
17613#[gpui::test]
17614async fn test_folding_buffers(cx: &mut TestAppContext) {
17615 init_test(cx, |_| {});
17616
17617 let sample_text_1 = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17618 let sample_text_2 = "llll\nmmmm\nnnnn\noooo\npppp\nqqqq\nrrrr\nssss\ntttt\nuuuu".to_string();
17619 let sample_text_3 = "vvvv\nwwww\nxxxx\nyyyy\nzzzz\n1111\n2222\n3333\n4444\n5555".to_string();
17620
17621 let fs = FakeFs::new(cx.executor());
17622 fs.insert_tree(
17623 path!("/a"),
17624 json!({
17625 "first.rs": sample_text_1,
17626 "second.rs": sample_text_2,
17627 "third.rs": sample_text_3,
17628 }),
17629 )
17630 .await;
17631 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17632 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17633 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17634 let worktree = project.update(cx, |project, cx| {
17635 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17636 assert_eq!(worktrees.len(), 1);
17637 worktrees.pop().unwrap()
17638 });
17639 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17640
17641 let buffer_1 = project
17642 .update(cx, |project, cx| {
17643 project.open_buffer((worktree_id, "first.rs"), cx)
17644 })
17645 .await
17646 .unwrap();
17647 let buffer_2 = project
17648 .update(cx, |project, cx| {
17649 project.open_buffer((worktree_id, "second.rs"), cx)
17650 })
17651 .await
17652 .unwrap();
17653 let buffer_3 = project
17654 .update(cx, |project, cx| {
17655 project.open_buffer((worktree_id, "third.rs"), cx)
17656 })
17657 .await
17658 .unwrap();
17659
17660 let multi_buffer = cx.new(|cx| {
17661 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17662 multi_buffer.push_excerpts(
17663 buffer_1.clone(),
17664 [
17665 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17666 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17667 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17668 ],
17669 cx,
17670 );
17671 multi_buffer.push_excerpts(
17672 buffer_2.clone(),
17673 [
17674 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17675 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17676 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17677 ],
17678 cx,
17679 );
17680 multi_buffer.push_excerpts(
17681 buffer_3.clone(),
17682 [
17683 ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0)),
17684 ExcerptRange::new(Point::new(5, 0)..Point::new(7, 0)),
17685 ExcerptRange::new(Point::new(9, 0)..Point::new(10, 4)),
17686 ],
17687 cx,
17688 );
17689 multi_buffer
17690 });
17691 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17692 Editor::new(
17693 EditorMode::full(),
17694 multi_buffer.clone(),
17695 Some(project.clone()),
17696 window,
17697 cx,
17698 )
17699 });
17700
17701 assert_eq!(
17702 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17703 "\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",
17704 );
17705
17706 multi_buffer_editor.update(cx, |editor, cx| {
17707 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17708 });
17709 assert_eq!(
17710 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17711 "\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",
17712 "After folding the first buffer, its text should not be displayed"
17713 );
17714
17715 multi_buffer_editor.update(cx, |editor, cx| {
17716 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17717 });
17718 assert_eq!(
17719 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17720 "\n\n\n\n\n\nvvvv\nwwww\nxxxx\n\n\n1111\n2222\n\n\n5555",
17721 "After folding the second buffer, its text should not be displayed"
17722 );
17723
17724 multi_buffer_editor.update(cx, |editor, cx| {
17725 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17726 });
17727 assert_eq!(
17728 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17729 "\n\n\n\n\n",
17730 "After folding the third buffer, its text should not be displayed"
17731 );
17732
17733 // Emulate selection inside the fold logic, that should work
17734 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17735 editor
17736 .snapshot(window, cx)
17737 .next_line_boundary(Point::new(0, 4));
17738 });
17739
17740 multi_buffer_editor.update(cx, |editor, cx| {
17741 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17742 });
17743 assert_eq!(
17744 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17745 "\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17746 "After unfolding the second buffer, its text should be displayed"
17747 );
17748
17749 // Typing inside of buffer 1 causes that buffer to be unfolded.
17750 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17751 assert_eq!(
17752 multi_buffer
17753 .read(cx)
17754 .snapshot(cx)
17755 .text_for_range(Point::new(1, 0)..Point::new(1, 4))
17756 .collect::<String>(),
17757 "bbbb"
17758 );
17759 editor.change_selections(None, window, cx, |selections| {
17760 selections.select_ranges(vec![Point::new(1, 0)..Point::new(1, 0)]);
17761 });
17762 editor.handle_input("B", window, cx);
17763 });
17764
17765 assert_eq!(
17766 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17767 "\n\nB\n\n\n\n\n\n\nllll\nmmmm\nnnnn\n\n\nqqqq\nrrrr\n\n\nuuuu\n\n",
17768 "After unfolding the first buffer, its and 2nd buffer's text should be displayed"
17769 );
17770
17771 multi_buffer_editor.update(cx, |editor, cx| {
17772 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17773 });
17774 assert_eq!(
17775 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17776 "\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",
17777 "After unfolding the all buffers, all original text should be displayed"
17778 );
17779}
17780
17781#[gpui::test]
17782async fn test_folding_buffers_with_one_excerpt(cx: &mut TestAppContext) {
17783 init_test(cx, |_| {});
17784
17785 let sample_text_1 = "1111\n2222\n3333".to_string();
17786 let sample_text_2 = "4444\n5555\n6666".to_string();
17787 let sample_text_3 = "7777\n8888\n9999".to_string();
17788
17789 let fs = FakeFs::new(cx.executor());
17790 fs.insert_tree(
17791 path!("/a"),
17792 json!({
17793 "first.rs": sample_text_1,
17794 "second.rs": sample_text_2,
17795 "third.rs": sample_text_3,
17796 }),
17797 )
17798 .await;
17799 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17800 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17801 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17802 let worktree = project.update(cx, |project, cx| {
17803 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17804 assert_eq!(worktrees.len(), 1);
17805 worktrees.pop().unwrap()
17806 });
17807 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17808
17809 let buffer_1 = project
17810 .update(cx, |project, cx| {
17811 project.open_buffer((worktree_id, "first.rs"), cx)
17812 })
17813 .await
17814 .unwrap();
17815 let buffer_2 = project
17816 .update(cx, |project, cx| {
17817 project.open_buffer((worktree_id, "second.rs"), cx)
17818 })
17819 .await
17820 .unwrap();
17821 let buffer_3 = project
17822 .update(cx, |project, cx| {
17823 project.open_buffer((worktree_id, "third.rs"), cx)
17824 })
17825 .await
17826 .unwrap();
17827
17828 let multi_buffer = cx.new(|cx| {
17829 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17830 multi_buffer.push_excerpts(
17831 buffer_1.clone(),
17832 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17833 cx,
17834 );
17835 multi_buffer.push_excerpts(
17836 buffer_2.clone(),
17837 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17838 cx,
17839 );
17840 multi_buffer.push_excerpts(
17841 buffer_3.clone(),
17842 [ExcerptRange::new(Point::new(0, 0)..Point::new(3, 0))],
17843 cx,
17844 );
17845 multi_buffer
17846 });
17847
17848 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17849 Editor::new(
17850 EditorMode::full(),
17851 multi_buffer,
17852 Some(project.clone()),
17853 window,
17854 cx,
17855 )
17856 });
17857
17858 let full_text = "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999";
17859 assert_eq!(
17860 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17861 full_text,
17862 );
17863
17864 multi_buffer_editor.update(cx, |editor, cx| {
17865 editor.fold_buffer(buffer_1.read(cx).remote_id(), cx)
17866 });
17867 assert_eq!(
17868 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17869 "\n\n\n\n4444\n5555\n6666\n\n\n7777\n8888\n9999",
17870 "After folding the first buffer, its text should not be displayed"
17871 );
17872
17873 multi_buffer_editor.update(cx, |editor, cx| {
17874 editor.fold_buffer(buffer_2.read(cx).remote_id(), cx)
17875 });
17876
17877 assert_eq!(
17878 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17879 "\n\n\n\n\n\n7777\n8888\n9999",
17880 "After folding the second buffer, its text should not be displayed"
17881 );
17882
17883 multi_buffer_editor.update(cx, |editor, cx| {
17884 editor.fold_buffer(buffer_3.read(cx).remote_id(), cx)
17885 });
17886 assert_eq!(
17887 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17888 "\n\n\n\n\n",
17889 "After folding the third buffer, its text should not be displayed"
17890 );
17891
17892 multi_buffer_editor.update(cx, |editor, cx| {
17893 editor.unfold_buffer(buffer_2.read(cx).remote_id(), cx)
17894 });
17895 assert_eq!(
17896 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17897 "\n\n\n\n4444\n5555\n6666\n\n",
17898 "After unfolding the second buffer, its text should be displayed"
17899 );
17900
17901 multi_buffer_editor.update(cx, |editor, cx| {
17902 editor.unfold_buffer(buffer_1.read(cx).remote_id(), cx)
17903 });
17904 assert_eq!(
17905 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17906 "\n\n1111\n2222\n3333\n\n\n4444\n5555\n6666\n\n",
17907 "After unfolding the first buffer, its text should be displayed"
17908 );
17909
17910 multi_buffer_editor.update(cx, |editor, cx| {
17911 editor.unfold_buffer(buffer_3.read(cx).remote_id(), cx)
17912 });
17913 assert_eq!(
17914 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17915 full_text,
17916 "After unfolding all buffers, all original text should be displayed"
17917 );
17918}
17919
17920#[gpui::test]
17921async fn test_folding_buffer_when_multibuffer_has_only_one_excerpt(cx: &mut TestAppContext) {
17922 init_test(cx, |_| {});
17923
17924 let sample_text = "aaaa\nbbbb\ncccc\ndddd\neeee\nffff\ngggg\nhhhh\niiii\njjjj".to_string();
17925
17926 let fs = FakeFs::new(cx.executor());
17927 fs.insert_tree(
17928 path!("/a"),
17929 json!({
17930 "main.rs": sample_text,
17931 }),
17932 )
17933 .await;
17934 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
17935 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
17936 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
17937 let worktree = project.update(cx, |project, cx| {
17938 let mut worktrees = project.worktrees(cx).collect::<Vec<_>>();
17939 assert_eq!(worktrees.len(), 1);
17940 worktrees.pop().unwrap()
17941 });
17942 let worktree_id = worktree.update(cx, |worktree, _| worktree.id());
17943
17944 let buffer_1 = project
17945 .update(cx, |project, cx| {
17946 project.open_buffer((worktree_id, "main.rs"), cx)
17947 })
17948 .await
17949 .unwrap();
17950
17951 let multi_buffer = cx.new(|cx| {
17952 let mut multi_buffer = MultiBuffer::new(ReadWrite);
17953 multi_buffer.push_excerpts(
17954 buffer_1.clone(),
17955 [ExcerptRange::new(
17956 Point::new(0, 0)
17957 ..Point::new(
17958 sample_text.chars().filter(|&c| c == '\n').count() as u32 + 1,
17959 0,
17960 ),
17961 )],
17962 cx,
17963 );
17964 multi_buffer
17965 });
17966 let multi_buffer_editor = cx.new_window_entity(|window, cx| {
17967 Editor::new(
17968 EditorMode::full(),
17969 multi_buffer,
17970 Some(project.clone()),
17971 window,
17972 cx,
17973 )
17974 });
17975
17976 let selection_range = Point::new(1, 0)..Point::new(2, 0);
17977 multi_buffer_editor.update_in(cx, |editor, window, cx| {
17978 enum TestHighlight {}
17979 let multi_buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
17980 let highlight_range = selection_range.clone().to_anchors(&multi_buffer_snapshot);
17981 editor.highlight_text::<TestHighlight>(
17982 vec![highlight_range.clone()],
17983 HighlightStyle::color(Hsla::green()),
17984 cx,
17985 );
17986 editor.change_selections(None, window, cx, |s| s.select_ranges(Some(highlight_range)));
17987 });
17988
17989 let full_text = format!("\n\n{sample_text}");
17990 assert_eq!(
17991 multi_buffer_editor.update(cx, |editor, cx| editor.display_text(cx)),
17992 full_text,
17993 );
17994}
17995
17996#[gpui::test]
17997async fn test_multi_buffer_navigation_with_folded_buffers(cx: &mut TestAppContext) {
17998 init_test(cx, |_| {});
17999 cx.update(|cx| {
18000 let default_key_bindings = settings::KeymapFile::load_asset_allow_partial_failure(
18001 "keymaps/default-linux.json",
18002 cx,
18003 )
18004 .unwrap();
18005 cx.bind_keys(default_key_bindings);
18006 });
18007
18008 let (editor, cx) = cx.add_window_view(|window, cx| {
18009 let multi_buffer = MultiBuffer::build_multi(
18010 [
18011 ("a0\nb0\nc0\nd0\ne0\n", vec![Point::row_range(0..2)]),
18012 ("a1\nb1\nc1\nd1\ne1\n", vec![Point::row_range(0..2)]),
18013 ("a2\nb2\nc2\nd2\ne2\n", vec![Point::row_range(0..2)]),
18014 ("a3\nb3\nc3\nd3\ne3\n", vec![Point::row_range(0..2)]),
18015 ],
18016 cx,
18017 );
18018 let mut editor = Editor::new(EditorMode::full(), multi_buffer.clone(), None, window, cx);
18019
18020 let buffer_ids = multi_buffer.read(cx).excerpt_buffer_ids();
18021 // fold all but the second buffer, so that we test navigating between two
18022 // adjacent folded buffers, as well as folded buffers at the start and
18023 // end the multibuffer
18024 editor.fold_buffer(buffer_ids[0], cx);
18025 editor.fold_buffer(buffer_ids[2], cx);
18026 editor.fold_buffer(buffer_ids[3], cx);
18027
18028 editor
18029 });
18030 cx.simulate_resize(size(px(1000.), px(1000.)));
18031
18032 let mut cx = EditorTestContext::for_editor_in(editor.clone(), cx).await;
18033 cx.assert_excerpts_with_selections(indoc! {"
18034 [EXCERPT]
18035 ˇ[FOLDED]
18036 [EXCERPT]
18037 a1
18038 b1
18039 [EXCERPT]
18040 [FOLDED]
18041 [EXCERPT]
18042 [FOLDED]
18043 "
18044 });
18045 cx.simulate_keystroke("down");
18046 cx.assert_excerpts_with_selections(indoc! {"
18047 [EXCERPT]
18048 [FOLDED]
18049 [EXCERPT]
18050 ˇa1
18051 b1
18052 [EXCERPT]
18053 [FOLDED]
18054 [EXCERPT]
18055 [FOLDED]
18056 "
18057 });
18058 cx.simulate_keystroke("down");
18059 cx.assert_excerpts_with_selections(indoc! {"
18060 [EXCERPT]
18061 [FOLDED]
18062 [EXCERPT]
18063 a1
18064 ˇb1
18065 [EXCERPT]
18066 [FOLDED]
18067 [EXCERPT]
18068 [FOLDED]
18069 "
18070 });
18071 cx.simulate_keystroke("down");
18072 cx.assert_excerpts_with_selections(indoc! {"
18073 [EXCERPT]
18074 [FOLDED]
18075 [EXCERPT]
18076 a1
18077 b1
18078 ˇ[EXCERPT]
18079 [FOLDED]
18080 [EXCERPT]
18081 [FOLDED]
18082 "
18083 });
18084 cx.simulate_keystroke("down");
18085 cx.assert_excerpts_with_selections(indoc! {"
18086 [EXCERPT]
18087 [FOLDED]
18088 [EXCERPT]
18089 a1
18090 b1
18091 [EXCERPT]
18092 ˇ[FOLDED]
18093 [EXCERPT]
18094 [FOLDED]
18095 "
18096 });
18097 for _ in 0..5 {
18098 cx.simulate_keystroke("down");
18099 cx.assert_excerpts_with_selections(indoc! {"
18100 [EXCERPT]
18101 [FOLDED]
18102 [EXCERPT]
18103 a1
18104 b1
18105 [EXCERPT]
18106 [FOLDED]
18107 [EXCERPT]
18108 ˇ[FOLDED]
18109 "
18110 });
18111 }
18112
18113 cx.simulate_keystroke("up");
18114 cx.assert_excerpts_with_selections(indoc! {"
18115 [EXCERPT]
18116 [FOLDED]
18117 [EXCERPT]
18118 a1
18119 b1
18120 [EXCERPT]
18121 ˇ[FOLDED]
18122 [EXCERPT]
18123 [FOLDED]
18124 "
18125 });
18126 cx.simulate_keystroke("up");
18127 cx.assert_excerpts_with_selections(indoc! {"
18128 [EXCERPT]
18129 [FOLDED]
18130 [EXCERPT]
18131 a1
18132 b1
18133 ˇ[EXCERPT]
18134 [FOLDED]
18135 [EXCERPT]
18136 [FOLDED]
18137 "
18138 });
18139 cx.simulate_keystroke("up");
18140 cx.assert_excerpts_with_selections(indoc! {"
18141 [EXCERPT]
18142 [FOLDED]
18143 [EXCERPT]
18144 a1
18145 ˇb1
18146 [EXCERPT]
18147 [FOLDED]
18148 [EXCERPT]
18149 [FOLDED]
18150 "
18151 });
18152 cx.simulate_keystroke("up");
18153 cx.assert_excerpts_with_selections(indoc! {"
18154 [EXCERPT]
18155 [FOLDED]
18156 [EXCERPT]
18157 ˇa1
18158 b1
18159 [EXCERPT]
18160 [FOLDED]
18161 [EXCERPT]
18162 [FOLDED]
18163 "
18164 });
18165 for _ in 0..5 {
18166 cx.simulate_keystroke("up");
18167 cx.assert_excerpts_with_selections(indoc! {"
18168 [EXCERPT]
18169 ˇ[FOLDED]
18170 [EXCERPT]
18171 a1
18172 b1
18173 [EXCERPT]
18174 [FOLDED]
18175 [EXCERPT]
18176 [FOLDED]
18177 "
18178 });
18179 }
18180}
18181
18182#[gpui::test]
18183async fn test_inline_completion_text(cx: &mut TestAppContext) {
18184 init_test(cx, |_| {});
18185
18186 // Simple insertion
18187 assert_highlighted_edits(
18188 "Hello, world!",
18189 vec![(Point::new(0, 6)..Point::new(0, 6), " beautiful".into())],
18190 true,
18191 cx,
18192 |highlighted_edits, cx| {
18193 assert_eq!(highlighted_edits.text, "Hello, beautiful world!");
18194 assert_eq!(highlighted_edits.highlights.len(), 1);
18195 assert_eq!(highlighted_edits.highlights[0].0, 6..16);
18196 assert_eq!(
18197 highlighted_edits.highlights[0].1.background_color,
18198 Some(cx.theme().status().created_background)
18199 );
18200 },
18201 )
18202 .await;
18203
18204 // Replacement
18205 assert_highlighted_edits(
18206 "This is a test.",
18207 vec![(Point::new(0, 0)..Point::new(0, 4), "That".into())],
18208 false,
18209 cx,
18210 |highlighted_edits, cx| {
18211 assert_eq!(highlighted_edits.text, "That is a test.");
18212 assert_eq!(highlighted_edits.highlights.len(), 1);
18213 assert_eq!(highlighted_edits.highlights[0].0, 0..4);
18214 assert_eq!(
18215 highlighted_edits.highlights[0].1.background_color,
18216 Some(cx.theme().status().created_background)
18217 );
18218 },
18219 )
18220 .await;
18221
18222 // Multiple edits
18223 assert_highlighted_edits(
18224 "Hello, world!",
18225 vec![
18226 (Point::new(0, 0)..Point::new(0, 5), "Greetings".into()),
18227 (Point::new(0, 12)..Point::new(0, 12), " and universe".into()),
18228 ],
18229 false,
18230 cx,
18231 |highlighted_edits, cx| {
18232 assert_eq!(highlighted_edits.text, "Greetings, world and universe!");
18233 assert_eq!(highlighted_edits.highlights.len(), 2);
18234 assert_eq!(highlighted_edits.highlights[0].0, 0..9);
18235 assert_eq!(highlighted_edits.highlights[1].0, 16..29);
18236 assert_eq!(
18237 highlighted_edits.highlights[0].1.background_color,
18238 Some(cx.theme().status().created_background)
18239 );
18240 assert_eq!(
18241 highlighted_edits.highlights[1].1.background_color,
18242 Some(cx.theme().status().created_background)
18243 );
18244 },
18245 )
18246 .await;
18247
18248 // Multiple lines with edits
18249 assert_highlighted_edits(
18250 "First line\nSecond line\nThird line\nFourth line",
18251 vec![
18252 (Point::new(1, 7)..Point::new(1, 11), "modified".to_string()),
18253 (
18254 Point::new(2, 0)..Point::new(2, 10),
18255 "New third line".to_string(),
18256 ),
18257 (Point::new(3, 6)..Point::new(3, 6), " updated".to_string()),
18258 ],
18259 false,
18260 cx,
18261 |highlighted_edits, cx| {
18262 assert_eq!(
18263 highlighted_edits.text,
18264 "Second modified\nNew third line\nFourth updated line"
18265 );
18266 assert_eq!(highlighted_edits.highlights.len(), 3);
18267 assert_eq!(highlighted_edits.highlights[0].0, 7..15); // "modified"
18268 assert_eq!(highlighted_edits.highlights[1].0, 16..30); // "New third line"
18269 assert_eq!(highlighted_edits.highlights[2].0, 37..45); // " updated"
18270 for highlight in &highlighted_edits.highlights {
18271 assert_eq!(
18272 highlight.1.background_color,
18273 Some(cx.theme().status().created_background)
18274 );
18275 }
18276 },
18277 )
18278 .await;
18279}
18280
18281#[gpui::test]
18282async fn test_inline_completion_text_with_deletions(cx: &mut TestAppContext) {
18283 init_test(cx, |_| {});
18284
18285 // Deletion
18286 assert_highlighted_edits(
18287 "Hello, world!",
18288 vec![(Point::new(0, 5)..Point::new(0, 11), "".to_string())],
18289 true,
18290 cx,
18291 |highlighted_edits, cx| {
18292 assert_eq!(highlighted_edits.text, "Hello, world!");
18293 assert_eq!(highlighted_edits.highlights.len(), 1);
18294 assert_eq!(highlighted_edits.highlights[0].0, 5..11);
18295 assert_eq!(
18296 highlighted_edits.highlights[0].1.background_color,
18297 Some(cx.theme().status().deleted_background)
18298 );
18299 },
18300 )
18301 .await;
18302
18303 // Insertion
18304 assert_highlighted_edits(
18305 "Hello, world!",
18306 vec![(Point::new(0, 6)..Point::new(0, 6), " digital".to_string())],
18307 true,
18308 cx,
18309 |highlighted_edits, cx| {
18310 assert_eq!(highlighted_edits.highlights.len(), 1);
18311 assert_eq!(highlighted_edits.highlights[0].0, 6..14);
18312 assert_eq!(
18313 highlighted_edits.highlights[0].1.background_color,
18314 Some(cx.theme().status().created_background)
18315 );
18316 },
18317 )
18318 .await;
18319}
18320
18321async fn assert_highlighted_edits(
18322 text: &str,
18323 edits: Vec<(Range<Point>, String)>,
18324 include_deletions: bool,
18325 cx: &mut TestAppContext,
18326 assertion_fn: impl Fn(HighlightedText, &App),
18327) {
18328 let window = cx.add_window(|window, cx| {
18329 let buffer = MultiBuffer::build_simple(text, cx);
18330 Editor::new(EditorMode::full(), buffer, None, window, cx)
18331 });
18332 let cx = &mut VisualTestContext::from_window(*window, cx);
18333
18334 let (buffer, snapshot) = window
18335 .update(cx, |editor, _window, cx| {
18336 (
18337 editor.buffer().clone(),
18338 editor.buffer().read(cx).snapshot(cx),
18339 )
18340 })
18341 .unwrap();
18342
18343 let edits = edits
18344 .into_iter()
18345 .map(|(range, edit)| {
18346 (
18347 snapshot.anchor_after(range.start)..snapshot.anchor_before(range.end),
18348 edit,
18349 )
18350 })
18351 .collect::<Vec<_>>();
18352
18353 let text_anchor_edits = edits
18354 .clone()
18355 .into_iter()
18356 .map(|(range, edit)| (range.start.text_anchor..range.end.text_anchor, edit))
18357 .collect::<Vec<_>>();
18358
18359 let edit_preview = window
18360 .update(cx, |_, _window, cx| {
18361 buffer
18362 .read(cx)
18363 .as_singleton()
18364 .unwrap()
18365 .read(cx)
18366 .preview_edits(text_anchor_edits.into(), cx)
18367 })
18368 .unwrap()
18369 .await;
18370
18371 cx.update(|_window, cx| {
18372 let highlighted_edits = inline_completion_edit_text(
18373 &snapshot.as_singleton().unwrap().2,
18374 &edits,
18375 &edit_preview,
18376 include_deletions,
18377 cx,
18378 );
18379 assertion_fn(highlighted_edits, cx)
18380 });
18381}
18382
18383#[track_caller]
18384fn assert_breakpoint(
18385 breakpoints: &BTreeMap<Arc<Path>, Vec<SourceBreakpoint>>,
18386 path: &Arc<Path>,
18387 expected: Vec<(u32, Breakpoint)>,
18388) {
18389 if expected.len() == 0usize {
18390 assert!(!breakpoints.contains_key(path), "{}", path.display());
18391 } else {
18392 let mut breakpoint = breakpoints
18393 .get(path)
18394 .unwrap()
18395 .into_iter()
18396 .map(|breakpoint| {
18397 (
18398 breakpoint.row,
18399 Breakpoint {
18400 message: breakpoint.message.clone(),
18401 state: breakpoint.state,
18402 condition: breakpoint.condition.clone(),
18403 hit_condition: breakpoint.hit_condition.clone(),
18404 },
18405 )
18406 })
18407 .collect::<Vec<_>>();
18408
18409 breakpoint.sort_by_key(|(cached_position, _)| *cached_position);
18410
18411 assert_eq!(expected, breakpoint);
18412 }
18413}
18414
18415fn add_log_breakpoint_at_cursor(
18416 editor: &mut Editor,
18417 log_message: &str,
18418 window: &mut Window,
18419 cx: &mut Context<Editor>,
18420) {
18421 let (anchor, bp) = editor
18422 .breakpoints_at_cursors(window, cx)
18423 .first()
18424 .and_then(|(anchor, bp)| {
18425 if let Some(bp) = bp {
18426 Some((*anchor, bp.clone()))
18427 } else {
18428 None
18429 }
18430 })
18431 .unwrap_or_else(|| {
18432 let cursor_position: Point = editor.selections.newest(cx).head();
18433
18434 let breakpoint_position = editor
18435 .snapshot(window, cx)
18436 .display_snapshot
18437 .buffer_snapshot
18438 .anchor_before(Point::new(cursor_position.row, 0));
18439
18440 (breakpoint_position, Breakpoint::new_log(&log_message))
18441 });
18442
18443 editor.edit_breakpoint_at_anchor(
18444 anchor,
18445 bp,
18446 BreakpointEditAction::EditLogMessage(log_message.into()),
18447 cx,
18448 );
18449}
18450
18451#[gpui::test]
18452async fn test_breakpoint_toggling(cx: &mut TestAppContext) {
18453 init_test(cx, |_| {});
18454
18455 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18456 let fs = FakeFs::new(cx.executor());
18457 fs.insert_tree(
18458 path!("/a"),
18459 json!({
18460 "main.rs": sample_text,
18461 }),
18462 )
18463 .await;
18464 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18465 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18466 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18467
18468 let fs = FakeFs::new(cx.executor());
18469 fs.insert_tree(
18470 path!("/a"),
18471 json!({
18472 "main.rs": sample_text,
18473 }),
18474 )
18475 .await;
18476 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18477 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18478 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18479 let worktree_id = workspace
18480 .update(cx, |workspace, _window, cx| {
18481 workspace.project().update(cx, |project, cx| {
18482 project.worktrees(cx).next().unwrap().read(cx).id()
18483 })
18484 })
18485 .unwrap();
18486
18487 let buffer = project
18488 .update(cx, |project, cx| {
18489 project.open_buffer((worktree_id, "main.rs"), cx)
18490 })
18491 .await
18492 .unwrap();
18493
18494 let (editor, cx) = cx.add_window_view(|window, cx| {
18495 Editor::new(
18496 EditorMode::full(),
18497 MultiBuffer::build_from_buffer(buffer, cx),
18498 Some(project.clone()),
18499 window,
18500 cx,
18501 )
18502 });
18503
18504 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18505 let abs_path = project.read_with(cx, |project, cx| {
18506 project
18507 .absolute_path(&project_path, cx)
18508 .map(|path_buf| Arc::from(path_buf.to_owned()))
18509 .unwrap()
18510 });
18511
18512 // assert we can add breakpoint on the first line
18513 editor.update_in(cx, |editor, window, cx| {
18514 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18515 editor.move_to_end(&MoveToEnd, window, cx);
18516 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18517 });
18518
18519 let breakpoints = editor.update(cx, |editor, cx| {
18520 editor
18521 .breakpoint_store()
18522 .as_ref()
18523 .unwrap()
18524 .read(cx)
18525 .all_breakpoints(cx)
18526 .clone()
18527 });
18528
18529 assert_eq!(1, breakpoints.len());
18530 assert_breakpoint(
18531 &breakpoints,
18532 &abs_path,
18533 vec![
18534 (0, Breakpoint::new_standard()),
18535 (3, Breakpoint::new_standard()),
18536 ],
18537 );
18538
18539 editor.update_in(cx, |editor, window, cx| {
18540 editor.move_to_beginning(&MoveToBeginning, window, cx);
18541 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18542 });
18543
18544 let breakpoints = editor.update(cx, |editor, cx| {
18545 editor
18546 .breakpoint_store()
18547 .as_ref()
18548 .unwrap()
18549 .read(cx)
18550 .all_breakpoints(cx)
18551 .clone()
18552 });
18553
18554 assert_eq!(1, breakpoints.len());
18555 assert_breakpoint(
18556 &breakpoints,
18557 &abs_path,
18558 vec![(3, Breakpoint::new_standard())],
18559 );
18560
18561 editor.update_in(cx, |editor, window, cx| {
18562 editor.move_to_end(&MoveToEnd, window, cx);
18563 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18564 });
18565
18566 let breakpoints = editor.update(cx, |editor, cx| {
18567 editor
18568 .breakpoint_store()
18569 .as_ref()
18570 .unwrap()
18571 .read(cx)
18572 .all_breakpoints(cx)
18573 .clone()
18574 });
18575
18576 assert_eq!(0, breakpoints.len());
18577 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18578}
18579
18580#[gpui::test]
18581async fn test_log_breakpoint_editing(cx: &mut TestAppContext) {
18582 init_test(cx, |_| {});
18583
18584 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18585
18586 let fs = FakeFs::new(cx.executor());
18587 fs.insert_tree(
18588 path!("/a"),
18589 json!({
18590 "main.rs": sample_text,
18591 }),
18592 )
18593 .await;
18594 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18595 let (workspace, cx) =
18596 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
18597
18598 let worktree_id = workspace.update(cx, |workspace, cx| {
18599 workspace.project().update(cx, |project, cx| {
18600 project.worktrees(cx).next().unwrap().read(cx).id()
18601 })
18602 });
18603
18604 let buffer = project
18605 .update(cx, |project, cx| {
18606 project.open_buffer((worktree_id, "main.rs"), cx)
18607 })
18608 .await
18609 .unwrap();
18610
18611 let (editor, cx) = cx.add_window_view(|window, cx| {
18612 Editor::new(
18613 EditorMode::full(),
18614 MultiBuffer::build_from_buffer(buffer, cx),
18615 Some(project.clone()),
18616 window,
18617 cx,
18618 )
18619 });
18620
18621 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18622 let abs_path = project.read_with(cx, |project, cx| {
18623 project
18624 .absolute_path(&project_path, cx)
18625 .map(|path_buf| Arc::from(path_buf.to_owned()))
18626 .unwrap()
18627 });
18628
18629 editor.update_in(cx, |editor, window, cx| {
18630 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18631 });
18632
18633 let breakpoints = editor.update(cx, |editor, cx| {
18634 editor
18635 .breakpoint_store()
18636 .as_ref()
18637 .unwrap()
18638 .read(cx)
18639 .all_breakpoints(cx)
18640 .clone()
18641 });
18642
18643 assert_breakpoint(
18644 &breakpoints,
18645 &abs_path,
18646 vec![(0, Breakpoint::new_log("hello world"))],
18647 );
18648
18649 // Removing a log message from a log breakpoint should remove it
18650 editor.update_in(cx, |editor, window, cx| {
18651 add_log_breakpoint_at_cursor(editor, "", window, cx);
18652 });
18653
18654 let breakpoints = editor.update(cx, |editor, cx| {
18655 editor
18656 .breakpoint_store()
18657 .as_ref()
18658 .unwrap()
18659 .read(cx)
18660 .all_breakpoints(cx)
18661 .clone()
18662 });
18663
18664 assert_breakpoint(&breakpoints, &abs_path, vec![]);
18665
18666 editor.update_in(cx, |editor, window, cx| {
18667 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18668 editor.move_to_end(&MoveToEnd, window, cx);
18669 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18670 // Not adding a log message to a standard breakpoint shouldn't remove it
18671 add_log_breakpoint_at_cursor(editor, "", window, cx);
18672 });
18673
18674 let breakpoints = editor.update(cx, |editor, cx| {
18675 editor
18676 .breakpoint_store()
18677 .as_ref()
18678 .unwrap()
18679 .read(cx)
18680 .all_breakpoints(cx)
18681 .clone()
18682 });
18683
18684 assert_breakpoint(
18685 &breakpoints,
18686 &abs_path,
18687 vec![
18688 (0, Breakpoint::new_standard()),
18689 (3, Breakpoint::new_standard()),
18690 ],
18691 );
18692
18693 editor.update_in(cx, |editor, window, cx| {
18694 add_log_breakpoint_at_cursor(editor, "hello world", window, cx);
18695 });
18696
18697 let breakpoints = editor.update(cx, |editor, cx| {
18698 editor
18699 .breakpoint_store()
18700 .as_ref()
18701 .unwrap()
18702 .read(cx)
18703 .all_breakpoints(cx)
18704 .clone()
18705 });
18706
18707 assert_breakpoint(
18708 &breakpoints,
18709 &abs_path,
18710 vec![
18711 (0, Breakpoint::new_standard()),
18712 (3, Breakpoint::new_log("hello world")),
18713 ],
18714 );
18715
18716 editor.update_in(cx, |editor, window, cx| {
18717 add_log_breakpoint_at_cursor(editor, "hello Earth!!", window, cx);
18718 });
18719
18720 let breakpoints = editor.update(cx, |editor, cx| {
18721 editor
18722 .breakpoint_store()
18723 .as_ref()
18724 .unwrap()
18725 .read(cx)
18726 .all_breakpoints(cx)
18727 .clone()
18728 });
18729
18730 assert_breakpoint(
18731 &breakpoints,
18732 &abs_path,
18733 vec![
18734 (0, Breakpoint::new_standard()),
18735 (3, Breakpoint::new_log("hello Earth!!")),
18736 ],
18737 );
18738}
18739
18740/// This also tests that Editor::breakpoint_at_cursor_head is working properly
18741/// we had some issues where we wouldn't find a breakpoint at Point {row: 0, col: 0}
18742/// or when breakpoints were placed out of order. This tests for a regression too
18743#[gpui::test]
18744async fn test_breakpoint_enabling_and_disabling(cx: &mut TestAppContext) {
18745 init_test(cx, |_| {});
18746
18747 let sample_text = "First line\nSecond line\nThird line\nFourth line".to_string();
18748 let fs = FakeFs::new(cx.executor());
18749 fs.insert_tree(
18750 path!("/a"),
18751 json!({
18752 "main.rs": sample_text,
18753 }),
18754 )
18755 .await;
18756 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18757 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18758 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18759
18760 let fs = FakeFs::new(cx.executor());
18761 fs.insert_tree(
18762 path!("/a"),
18763 json!({
18764 "main.rs": sample_text,
18765 }),
18766 )
18767 .await;
18768 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
18769 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
18770 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
18771 let worktree_id = workspace
18772 .update(cx, |workspace, _window, cx| {
18773 workspace.project().update(cx, |project, cx| {
18774 project.worktrees(cx).next().unwrap().read(cx).id()
18775 })
18776 })
18777 .unwrap();
18778
18779 let buffer = project
18780 .update(cx, |project, cx| {
18781 project.open_buffer((worktree_id, "main.rs"), cx)
18782 })
18783 .await
18784 .unwrap();
18785
18786 let (editor, cx) = cx.add_window_view(|window, cx| {
18787 Editor::new(
18788 EditorMode::full(),
18789 MultiBuffer::build_from_buffer(buffer, cx),
18790 Some(project.clone()),
18791 window,
18792 cx,
18793 )
18794 });
18795
18796 let project_path = editor.update(cx, |editor, cx| editor.project_path(cx).unwrap());
18797 let abs_path = project.read_with(cx, |project, cx| {
18798 project
18799 .absolute_path(&project_path, cx)
18800 .map(|path_buf| Arc::from(path_buf.to_owned()))
18801 .unwrap()
18802 });
18803
18804 // assert we can add breakpoint on the first line
18805 editor.update_in(cx, |editor, window, cx| {
18806 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18807 editor.move_to_end(&MoveToEnd, window, cx);
18808 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18809 editor.move_up(&MoveUp, window, cx);
18810 editor.toggle_breakpoint(&actions::ToggleBreakpoint, window, cx);
18811 });
18812
18813 let breakpoints = editor.update(cx, |editor, cx| {
18814 editor
18815 .breakpoint_store()
18816 .as_ref()
18817 .unwrap()
18818 .read(cx)
18819 .all_breakpoints(cx)
18820 .clone()
18821 });
18822
18823 assert_eq!(1, breakpoints.len());
18824 assert_breakpoint(
18825 &breakpoints,
18826 &abs_path,
18827 vec![
18828 (0, Breakpoint::new_standard()),
18829 (2, Breakpoint::new_standard()),
18830 (3, Breakpoint::new_standard()),
18831 ],
18832 );
18833
18834 editor.update_in(cx, |editor, window, cx| {
18835 editor.move_to_beginning(&MoveToBeginning, window, cx);
18836 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18837 editor.move_to_end(&MoveToEnd, window, cx);
18838 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18839 // Disabling a breakpoint that doesn't exist should do nothing
18840 editor.move_up(&MoveUp, window, cx);
18841 editor.move_up(&MoveUp, window, cx);
18842 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18843 });
18844
18845 let breakpoints = editor.update(cx, |editor, cx| {
18846 editor
18847 .breakpoint_store()
18848 .as_ref()
18849 .unwrap()
18850 .read(cx)
18851 .all_breakpoints(cx)
18852 .clone()
18853 });
18854
18855 let disable_breakpoint = {
18856 let mut bp = Breakpoint::new_standard();
18857 bp.state = BreakpointState::Disabled;
18858 bp
18859 };
18860
18861 assert_eq!(1, breakpoints.len());
18862 assert_breakpoint(
18863 &breakpoints,
18864 &abs_path,
18865 vec![
18866 (0, disable_breakpoint.clone()),
18867 (2, Breakpoint::new_standard()),
18868 (3, disable_breakpoint.clone()),
18869 ],
18870 );
18871
18872 editor.update_in(cx, |editor, window, cx| {
18873 editor.move_to_beginning(&MoveToBeginning, window, cx);
18874 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18875 editor.move_to_end(&MoveToEnd, window, cx);
18876 editor.enable_breakpoint(&actions::EnableBreakpoint, window, cx);
18877 editor.move_up(&MoveUp, window, cx);
18878 editor.disable_breakpoint(&actions::DisableBreakpoint, window, cx);
18879 });
18880
18881 let breakpoints = editor.update(cx, |editor, cx| {
18882 editor
18883 .breakpoint_store()
18884 .as_ref()
18885 .unwrap()
18886 .read(cx)
18887 .all_breakpoints(cx)
18888 .clone()
18889 });
18890
18891 assert_eq!(1, breakpoints.len());
18892 assert_breakpoint(
18893 &breakpoints,
18894 &abs_path,
18895 vec![
18896 (0, Breakpoint::new_standard()),
18897 (2, disable_breakpoint),
18898 (3, Breakpoint::new_standard()),
18899 ],
18900 );
18901}
18902
18903#[gpui::test]
18904async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
18905 init_test(cx, |_| {});
18906 let capabilities = lsp::ServerCapabilities {
18907 rename_provider: Some(lsp::OneOf::Right(lsp::RenameOptions {
18908 prepare_provider: Some(true),
18909 work_done_progress_options: Default::default(),
18910 })),
18911 ..Default::default()
18912 };
18913 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18914
18915 cx.set_state(indoc! {"
18916 struct Fˇoo {}
18917 "});
18918
18919 cx.update_editor(|editor, _, cx| {
18920 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18921 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
18922 editor.highlight_background::<DocumentHighlightRead>(
18923 &[highlight_range],
18924 |c| c.editor_document_highlight_read_background,
18925 cx,
18926 );
18927 });
18928
18929 let mut prepare_rename_handler = cx
18930 .set_request_handler::<lsp::request::PrepareRenameRequest, _, _>(
18931 move |_, _, _| async move {
18932 Ok(Some(lsp::PrepareRenameResponse::Range(lsp::Range {
18933 start: lsp::Position {
18934 line: 0,
18935 character: 7,
18936 },
18937 end: lsp::Position {
18938 line: 0,
18939 character: 10,
18940 },
18941 })))
18942 },
18943 );
18944 let prepare_rename_task = cx
18945 .update_editor(|e, window, cx| e.rename(&Rename, window, cx))
18946 .expect("Prepare rename was not started");
18947 prepare_rename_handler.next().await.unwrap();
18948 prepare_rename_task.await.expect("Prepare rename failed");
18949
18950 let mut rename_handler =
18951 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
18952 let edit = lsp::TextEdit {
18953 range: lsp::Range {
18954 start: lsp::Position {
18955 line: 0,
18956 character: 7,
18957 },
18958 end: lsp::Position {
18959 line: 0,
18960 character: 10,
18961 },
18962 },
18963 new_text: "FooRenamed".to_string(),
18964 };
18965 Ok(Some(lsp::WorkspaceEdit::new(
18966 // Specify the same edit twice
18967 std::collections::HashMap::from_iter(Some((url, vec![edit.clone(), edit]))),
18968 )))
18969 });
18970 let rename_task = cx
18971 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
18972 .expect("Confirm rename was not started");
18973 rename_handler.next().await.unwrap();
18974 rename_task.await.expect("Confirm rename failed");
18975 cx.run_until_parked();
18976
18977 // Despite two edits, only one is actually applied as those are identical
18978 cx.assert_editor_state(indoc! {"
18979 struct FooRenamedˇ {}
18980 "});
18981}
18982
18983#[gpui::test]
18984async fn test_rename_without_prepare(cx: &mut TestAppContext) {
18985 init_test(cx, |_| {});
18986 // These capabilities indicate that the server does not support prepare rename.
18987 let capabilities = lsp::ServerCapabilities {
18988 rename_provider: Some(lsp::OneOf::Left(true)),
18989 ..Default::default()
18990 };
18991 let mut cx = EditorLspTestContext::new_rust(capabilities, cx).await;
18992
18993 cx.set_state(indoc! {"
18994 struct Fˇoo {}
18995 "});
18996
18997 cx.update_editor(|editor, _window, cx| {
18998 let highlight_range = Point::new(0, 7)..Point::new(0, 10);
18999 let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
19000 editor.highlight_background::<DocumentHighlightRead>(
19001 &[highlight_range],
19002 |c| c.editor_document_highlight_read_background,
19003 cx,
19004 );
19005 });
19006
19007 cx.update_editor(|e, window, cx| e.rename(&Rename, window, cx))
19008 .expect("Prepare rename was not started")
19009 .await
19010 .expect("Prepare rename failed");
19011
19012 let mut rename_handler =
19013 cx.set_request_handler::<lsp::request::Rename, _, _>(move |url, _, _| async move {
19014 let edit = lsp::TextEdit {
19015 range: lsp::Range {
19016 start: lsp::Position {
19017 line: 0,
19018 character: 7,
19019 },
19020 end: lsp::Position {
19021 line: 0,
19022 character: 10,
19023 },
19024 },
19025 new_text: "FooRenamed".to_string(),
19026 };
19027 Ok(Some(lsp::WorkspaceEdit::new(
19028 std::collections::HashMap::from_iter(Some((url, vec![edit]))),
19029 )))
19030 });
19031 let rename_task = cx
19032 .update_editor(|e, window, cx| e.confirm_rename(&ConfirmRename, window, cx))
19033 .expect("Confirm rename was not started");
19034 rename_handler.next().await.unwrap();
19035 rename_task.await.expect("Confirm rename failed");
19036 cx.run_until_parked();
19037
19038 // Correct range is renamed, as `surrounding_word` is used to find it.
19039 cx.assert_editor_state(indoc! {"
19040 struct FooRenamedˇ {}
19041 "});
19042}
19043
19044#[gpui::test]
19045async fn test_tree_sitter_brackets_newline_insertion(cx: &mut TestAppContext) {
19046 init_test(cx, |_| {});
19047 let mut cx = EditorTestContext::new(cx).await;
19048
19049 let language = Arc::new(
19050 Language::new(
19051 LanguageConfig::default(),
19052 Some(tree_sitter_html::LANGUAGE.into()),
19053 )
19054 .with_brackets_query(
19055 r#"
19056 ("<" @open "/>" @close)
19057 ("</" @open ">" @close)
19058 ("<" @open ">" @close)
19059 ("\"" @open "\"" @close)
19060 ((element (start_tag) @open (end_tag) @close) (#set! newline.only))
19061 "#,
19062 )
19063 .unwrap(),
19064 );
19065 cx.update_buffer(|buffer, cx| buffer.set_language(Some(language), cx));
19066
19067 cx.set_state(indoc! {"
19068 <span>ˇ</span>
19069 "});
19070 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19071 cx.assert_editor_state(indoc! {"
19072 <span>
19073 ˇ
19074 </span>
19075 "});
19076
19077 cx.set_state(indoc! {"
19078 <span><span></span>ˇ</span>
19079 "});
19080 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19081 cx.assert_editor_state(indoc! {"
19082 <span><span></span>
19083 ˇ</span>
19084 "});
19085
19086 cx.set_state(indoc! {"
19087 <span>ˇ
19088 </span>
19089 "});
19090 cx.update_editor(|e, window, cx| e.newline(&Newline, window, cx));
19091 cx.assert_editor_state(indoc! {"
19092 <span>
19093 ˇ
19094 </span>
19095 "});
19096}
19097
19098#[gpui::test(iterations = 10)]
19099async fn test_apply_code_lens_actions_with_commands(cx: &mut gpui::TestAppContext) {
19100 init_test(cx, |_| {});
19101
19102 let fs = FakeFs::new(cx.executor());
19103 fs.insert_tree(
19104 path!("/dir"),
19105 json!({
19106 "a.ts": "a",
19107 }),
19108 )
19109 .await;
19110
19111 let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
19112 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19113 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19114
19115 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19116 language_registry.add(Arc::new(Language::new(
19117 LanguageConfig {
19118 name: "TypeScript".into(),
19119 matcher: LanguageMatcher {
19120 path_suffixes: vec!["ts".to_string()],
19121 ..Default::default()
19122 },
19123 ..Default::default()
19124 },
19125 Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
19126 )));
19127 let mut fake_language_servers = language_registry.register_fake_lsp(
19128 "TypeScript",
19129 FakeLspAdapter {
19130 capabilities: lsp::ServerCapabilities {
19131 code_lens_provider: Some(lsp::CodeLensOptions {
19132 resolve_provider: Some(true),
19133 }),
19134 execute_command_provider: Some(lsp::ExecuteCommandOptions {
19135 commands: vec!["_the/command".to_string()],
19136 ..lsp::ExecuteCommandOptions::default()
19137 }),
19138 ..lsp::ServerCapabilities::default()
19139 },
19140 ..FakeLspAdapter::default()
19141 },
19142 );
19143
19144 let (buffer, _handle) = project
19145 .update(cx, |p, cx| {
19146 p.open_local_buffer_with_lsp(path!("/dir/a.ts"), cx)
19147 })
19148 .await
19149 .unwrap();
19150 cx.executor().run_until_parked();
19151
19152 let fake_server = fake_language_servers.next().await.unwrap();
19153
19154 let buffer_snapshot = buffer.update(cx, |buffer, _| buffer.snapshot());
19155 let anchor = buffer_snapshot.anchor_at(0, text::Bias::Left);
19156 drop(buffer_snapshot);
19157 let actions = cx
19158 .update_window(*workspace, |_, window, cx| {
19159 project.code_actions(&buffer, anchor..anchor, window, cx)
19160 })
19161 .unwrap();
19162
19163 fake_server
19164 .set_request_handler::<lsp::request::CodeLensRequest, _, _>(|_, _| async move {
19165 Ok(Some(vec![
19166 lsp::CodeLens {
19167 range: lsp::Range::default(),
19168 command: Some(lsp::Command {
19169 title: "Code lens command".to_owned(),
19170 command: "_the/command".to_owned(),
19171 arguments: None,
19172 }),
19173 data: None,
19174 },
19175 lsp::CodeLens {
19176 range: lsp::Range::default(),
19177 command: Some(lsp::Command {
19178 title: "Command not in capabilities".to_owned(),
19179 command: "not in capabilities".to_owned(),
19180 arguments: None,
19181 }),
19182 data: None,
19183 },
19184 lsp::CodeLens {
19185 range: lsp::Range {
19186 start: lsp::Position {
19187 line: 1,
19188 character: 1,
19189 },
19190 end: lsp::Position {
19191 line: 1,
19192 character: 1,
19193 },
19194 },
19195 command: Some(lsp::Command {
19196 title: "Command not in range".to_owned(),
19197 command: "_the/command".to_owned(),
19198 arguments: None,
19199 }),
19200 data: None,
19201 },
19202 ]))
19203 })
19204 .next()
19205 .await;
19206
19207 let actions = actions.await.unwrap();
19208 assert_eq!(
19209 actions.len(),
19210 1,
19211 "Should have only one valid action for the 0..0 range"
19212 );
19213 let action = actions[0].clone();
19214 let apply = project.update(cx, |project, cx| {
19215 project.apply_code_action(buffer.clone(), action, true, cx)
19216 });
19217
19218 // Resolving the code action does not populate its edits. In absence of
19219 // edits, we must execute the given command.
19220 fake_server.set_request_handler::<lsp::request::CodeLensResolve, _, _>(
19221 |mut lens, _| async move {
19222 let lens_command = lens.command.as_mut().expect("should have a command");
19223 assert_eq!(lens_command.title, "Code lens command");
19224 lens_command.arguments = Some(vec![json!("the-argument")]);
19225 Ok(lens)
19226 },
19227 );
19228
19229 // While executing the command, the language server sends the editor
19230 // a `workspaceEdit` request.
19231 fake_server
19232 .set_request_handler::<lsp::request::ExecuteCommand, _, _>({
19233 let fake = fake_server.clone();
19234 move |params, _| {
19235 assert_eq!(params.command, "_the/command");
19236 let fake = fake.clone();
19237 async move {
19238 fake.server
19239 .request::<lsp::request::ApplyWorkspaceEdit>(
19240 lsp::ApplyWorkspaceEditParams {
19241 label: None,
19242 edit: lsp::WorkspaceEdit {
19243 changes: Some(
19244 [(
19245 lsp::Url::from_file_path(path!("/dir/a.ts")).unwrap(),
19246 vec![lsp::TextEdit {
19247 range: lsp::Range::new(
19248 lsp::Position::new(0, 0),
19249 lsp::Position::new(0, 0),
19250 ),
19251 new_text: "X".into(),
19252 }],
19253 )]
19254 .into_iter()
19255 .collect(),
19256 ),
19257 ..Default::default()
19258 },
19259 },
19260 )
19261 .await
19262 .into_response()
19263 .unwrap();
19264 Ok(Some(json!(null)))
19265 }
19266 }
19267 })
19268 .next()
19269 .await;
19270
19271 // Applying the code lens command returns a project transaction containing the edits
19272 // sent by the language server in its `workspaceEdit` request.
19273 let transaction = apply.await.unwrap();
19274 assert!(transaction.0.contains_key(&buffer));
19275 buffer.update(cx, |buffer, cx| {
19276 assert_eq!(buffer.text(), "Xa");
19277 buffer.undo(cx);
19278 assert_eq!(buffer.text(), "a");
19279 });
19280}
19281
19282#[gpui::test]
19283async fn test_editor_restore_data_different_in_panes(cx: &mut TestAppContext) {
19284 init_test(cx, |_| {});
19285
19286 let fs = FakeFs::new(cx.executor());
19287 let main_text = r#"fn main() {
19288println!("1");
19289println!("2");
19290println!("3");
19291println!("4");
19292println!("5");
19293}"#;
19294 let lib_text = "mod foo {}";
19295 fs.insert_tree(
19296 path!("/a"),
19297 json!({
19298 "lib.rs": lib_text,
19299 "main.rs": main_text,
19300 }),
19301 )
19302 .await;
19303
19304 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19305 let (workspace, cx) =
19306 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19307 let worktree_id = workspace.update(cx, |workspace, cx| {
19308 workspace.project().update(cx, |project, cx| {
19309 project.worktrees(cx).next().unwrap().read(cx).id()
19310 })
19311 });
19312
19313 let expected_ranges = vec![
19314 Point::new(0, 0)..Point::new(0, 0),
19315 Point::new(1, 0)..Point::new(1, 1),
19316 Point::new(2, 0)..Point::new(2, 2),
19317 Point::new(3, 0)..Point::new(3, 3),
19318 ];
19319
19320 let pane_1 = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19321 let editor_1 = workspace
19322 .update_in(cx, |workspace, window, cx| {
19323 workspace.open_path(
19324 (worktree_id, "main.rs"),
19325 Some(pane_1.downgrade()),
19326 true,
19327 window,
19328 cx,
19329 )
19330 })
19331 .unwrap()
19332 .await
19333 .downcast::<Editor>()
19334 .unwrap();
19335 pane_1.update(cx, |pane, cx| {
19336 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19337 open_editor.update(cx, |editor, cx| {
19338 assert_eq!(
19339 editor.display_text(cx),
19340 main_text,
19341 "Original main.rs text on initial open",
19342 );
19343 assert_eq!(
19344 editor
19345 .selections
19346 .all::<Point>(cx)
19347 .into_iter()
19348 .map(|s| s.range())
19349 .collect::<Vec<_>>(),
19350 vec![Point::zero()..Point::zero()],
19351 "Default selections on initial open",
19352 );
19353 })
19354 });
19355 editor_1.update_in(cx, |editor, window, cx| {
19356 editor.change_selections(None, window, cx, |s| {
19357 s.select_ranges(expected_ranges.clone());
19358 });
19359 });
19360
19361 let pane_2 = workspace.update_in(cx, |workspace, window, cx| {
19362 workspace.split_pane(pane_1.clone(), SplitDirection::Right, window, cx)
19363 });
19364 let editor_2 = workspace
19365 .update_in(cx, |workspace, window, cx| {
19366 workspace.open_path(
19367 (worktree_id, "main.rs"),
19368 Some(pane_2.downgrade()),
19369 true,
19370 window,
19371 cx,
19372 )
19373 })
19374 .unwrap()
19375 .await
19376 .downcast::<Editor>()
19377 .unwrap();
19378 pane_2.update(cx, |pane, cx| {
19379 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19380 open_editor.update(cx, |editor, cx| {
19381 assert_eq!(
19382 editor.display_text(cx),
19383 main_text,
19384 "Original main.rs text on initial open in another panel",
19385 );
19386 assert_eq!(
19387 editor
19388 .selections
19389 .all::<Point>(cx)
19390 .into_iter()
19391 .map(|s| s.range())
19392 .collect::<Vec<_>>(),
19393 vec![Point::zero()..Point::zero()],
19394 "Default selections on initial open in another panel",
19395 );
19396 })
19397 });
19398
19399 editor_2.update_in(cx, |editor, window, cx| {
19400 editor.fold_ranges(expected_ranges.clone(), false, window, cx);
19401 });
19402
19403 let _other_editor_1 = workspace
19404 .update_in(cx, |workspace, window, cx| {
19405 workspace.open_path(
19406 (worktree_id, "lib.rs"),
19407 Some(pane_1.downgrade()),
19408 true,
19409 window,
19410 cx,
19411 )
19412 })
19413 .unwrap()
19414 .await
19415 .downcast::<Editor>()
19416 .unwrap();
19417 pane_1
19418 .update_in(cx, |pane, window, cx| {
19419 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19420 .unwrap()
19421 })
19422 .await
19423 .unwrap();
19424 drop(editor_1);
19425 pane_1.update(cx, |pane, cx| {
19426 pane.active_item()
19427 .unwrap()
19428 .downcast::<Editor>()
19429 .unwrap()
19430 .update(cx, |editor, cx| {
19431 assert_eq!(
19432 editor.display_text(cx),
19433 lib_text,
19434 "Other file should be open and active",
19435 );
19436 });
19437 assert_eq!(pane.items().count(), 1, "No other editors should be open");
19438 });
19439
19440 let _other_editor_2 = workspace
19441 .update_in(cx, |workspace, window, cx| {
19442 workspace.open_path(
19443 (worktree_id, "lib.rs"),
19444 Some(pane_2.downgrade()),
19445 true,
19446 window,
19447 cx,
19448 )
19449 })
19450 .unwrap()
19451 .await
19452 .downcast::<Editor>()
19453 .unwrap();
19454 pane_2
19455 .update_in(cx, |pane, window, cx| {
19456 pane.close_inactive_items(&CloseInactiveItems::default(), window, cx)
19457 .unwrap()
19458 })
19459 .await
19460 .unwrap();
19461 drop(editor_2);
19462 pane_2.update(cx, |pane, cx| {
19463 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19464 open_editor.update(cx, |editor, cx| {
19465 assert_eq!(
19466 editor.display_text(cx),
19467 lib_text,
19468 "Other file should be open and active in another panel too",
19469 );
19470 });
19471 assert_eq!(
19472 pane.items().count(),
19473 1,
19474 "No other editors should be open in another pane",
19475 );
19476 });
19477
19478 let _editor_1_reopened = workspace
19479 .update_in(cx, |workspace, window, cx| {
19480 workspace.open_path(
19481 (worktree_id, "main.rs"),
19482 Some(pane_1.downgrade()),
19483 true,
19484 window,
19485 cx,
19486 )
19487 })
19488 .unwrap()
19489 .await
19490 .downcast::<Editor>()
19491 .unwrap();
19492 let _editor_2_reopened = workspace
19493 .update_in(cx, |workspace, window, cx| {
19494 workspace.open_path(
19495 (worktree_id, "main.rs"),
19496 Some(pane_2.downgrade()),
19497 true,
19498 window,
19499 cx,
19500 )
19501 })
19502 .unwrap()
19503 .await
19504 .downcast::<Editor>()
19505 .unwrap();
19506 pane_1.update(cx, |pane, cx| {
19507 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19508 open_editor.update(cx, |editor, cx| {
19509 assert_eq!(
19510 editor.display_text(cx),
19511 main_text,
19512 "Previous editor in the 1st panel had no extra text manipulations and should get none on reopen",
19513 );
19514 assert_eq!(
19515 editor
19516 .selections
19517 .all::<Point>(cx)
19518 .into_iter()
19519 .map(|s| s.range())
19520 .collect::<Vec<_>>(),
19521 expected_ranges,
19522 "Previous editor in the 1st panel had selections and should get them restored on reopen",
19523 );
19524 })
19525 });
19526 pane_2.update(cx, |pane, cx| {
19527 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19528 open_editor.update(cx, |editor, cx| {
19529 assert_eq!(
19530 editor.display_text(cx),
19531 r#"fn main() {
19532⋯rintln!("1");
19533⋯intln!("2");
19534⋯ntln!("3");
19535println!("4");
19536println!("5");
19537}"#,
19538 "Previous editor in the 2nd pane had folds and should restore those on reopen in the same pane",
19539 );
19540 assert_eq!(
19541 editor
19542 .selections
19543 .all::<Point>(cx)
19544 .into_iter()
19545 .map(|s| s.range())
19546 .collect::<Vec<_>>(),
19547 vec![Point::zero()..Point::zero()],
19548 "Previous editor in the 2nd pane had no selections changed hence should restore none",
19549 );
19550 })
19551 });
19552}
19553
19554#[gpui::test]
19555async fn test_editor_does_not_restore_data_when_turned_off(cx: &mut TestAppContext) {
19556 init_test(cx, |_| {});
19557
19558 let fs = FakeFs::new(cx.executor());
19559 let main_text = r#"fn main() {
19560println!("1");
19561println!("2");
19562println!("3");
19563println!("4");
19564println!("5");
19565}"#;
19566 let lib_text = "mod foo {}";
19567 fs.insert_tree(
19568 path!("/a"),
19569 json!({
19570 "lib.rs": lib_text,
19571 "main.rs": main_text,
19572 }),
19573 )
19574 .await;
19575
19576 let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
19577 let (workspace, cx) =
19578 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19579 let worktree_id = workspace.update(cx, |workspace, cx| {
19580 workspace.project().update(cx, |project, cx| {
19581 project.worktrees(cx).next().unwrap().read(cx).id()
19582 })
19583 });
19584
19585 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19586 let editor = workspace
19587 .update_in(cx, |workspace, window, cx| {
19588 workspace.open_path(
19589 (worktree_id, "main.rs"),
19590 Some(pane.downgrade()),
19591 true,
19592 window,
19593 cx,
19594 )
19595 })
19596 .unwrap()
19597 .await
19598 .downcast::<Editor>()
19599 .unwrap();
19600 pane.update(cx, |pane, cx| {
19601 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19602 open_editor.update(cx, |editor, cx| {
19603 assert_eq!(
19604 editor.display_text(cx),
19605 main_text,
19606 "Original main.rs text on initial open",
19607 );
19608 })
19609 });
19610 editor.update_in(cx, |editor, window, cx| {
19611 editor.fold_ranges(vec![Point::new(0, 0)..Point::new(0, 0)], false, window, cx);
19612 });
19613
19614 cx.update_global(|store: &mut SettingsStore, cx| {
19615 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19616 s.restore_on_file_reopen = Some(false);
19617 });
19618 });
19619 editor.update_in(cx, |editor, window, cx| {
19620 editor.fold_ranges(
19621 vec![
19622 Point::new(1, 0)..Point::new(1, 1),
19623 Point::new(2, 0)..Point::new(2, 2),
19624 Point::new(3, 0)..Point::new(3, 3),
19625 ],
19626 false,
19627 window,
19628 cx,
19629 );
19630 });
19631 pane.update_in(cx, |pane, window, cx| {
19632 pane.close_all_items(&CloseAllItems::default(), window, cx)
19633 .unwrap()
19634 })
19635 .await
19636 .unwrap();
19637 pane.update(cx, |pane, _| {
19638 assert!(pane.active_item().is_none());
19639 });
19640 cx.update_global(|store: &mut SettingsStore, cx| {
19641 store.update_user_settings::<WorkspaceSettings>(cx, |s| {
19642 s.restore_on_file_reopen = Some(true);
19643 });
19644 });
19645
19646 let _editor_reopened = workspace
19647 .update_in(cx, |workspace, window, cx| {
19648 workspace.open_path(
19649 (worktree_id, "main.rs"),
19650 Some(pane.downgrade()),
19651 true,
19652 window,
19653 cx,
19654 )
19655 })
19656 .unwrap()
19657 .await
19658 .downcast::<Editor>()
19659 .unwrap();
19660 pane.update(cx, |pane, cx| {
19661 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19662 open_editor.update(cx, |editor, cx| {
19663 assert_eq!(
19664 editor.display_text(cx),
19665 main_text,
19666 "No folds: even after enabling the restoration, previous editor's data should not be saved to be used for the restoration"
19667 );
19668 })
19669 });
19670}
19671
19672#[gpui::test]
19673async fn test_hide_mouse_context_menu_on_modal_opened(cx: &mut TestAppContext) {
19674 struct EmptyModalView {
19675 focus_handle: gpui::FocusHandle,
19676 }
19677 impl EventEmitter<DismissEvent> for EmptyModalView {}
19678 impl Render for EmptyModalView {
19679 fn render(&mut self, _: &mut Window, _: &mut Context<'_, Self>) -> impl IntoElement {
19680 div()
19681 }
19682 }
19683 impl Focusable for EmptyModalView {
19684 fn focus_handle(&self, _cx: &App) -> gpui::FocusHandle {
19685 self.focus_handle.clone()
19686 }
19687 }
19688 impl workspace::ModalView for EmptyModalView {}
19689 fn new_empty_modal_view(cx: &App) -> EmptyModalView {
19690 EmptyModalView {
19691 focus_handle: cx.focus_handle(),
19692 }
19693 }
19694
19695 init_test(cx, |_| {});
19696
19697 let fs = FakeFs::new(cx.executor());
19698 let project = Project::test(fs, [], cx).await;
19699 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19700 let buffer = cx.update(|cx| MultiBuffer::build_simple("hello world!", cx));
19701 let cx = &mut VisualTestContext::from_window(*workspace.deref(), cx);
19702 let editor = cx.new_window_entity(|window, cx| {
19703 Editor::new(
19704 EditorMode::full(),
19705 buffer,
19706 Some(project.clone()),
19707 window,
19708 cx,
19709 )
19710 });
19711 workspace
19712 .update(cx, |workspace, window, cx| {
19713 workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
19714 })
19715 .unwrap();
19716 editor.update_in(cx, |editor, window, cx| {
19717 editor.open_context_menu(&OpenContextMenu, window, cx);
19718 assert!(editor.mouse_context_menu.is_some());
19719 });
19720 workspace
19721 .update(cx, |workspace, window, cx| {
19722 workspace.toggle_modal(window, cx, |_, cx| new_empty_modal_view(cx));
19723 })
19724 .unwrap();
19725 cx.read(|cx| {
19726 assert!(editor.read(cx).mouse_context_menu.is_none());
19727 });
19728}
19729
19730#[gpui::test]
19731async fn test_html_linked_edits_on_completion(cx: &mut TestAppContext) {
19732 init_test(cx, |_| {});
19733
19734 let fs = FakeFs::new(cx.executor());
19735 fs.insert_file(path!("/file.html"), Default::default())
19736 .await;
19737
19738 let project = Project::test(fs, [path!("/").as_ref()], cx).await;
19739
19740 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19741 let html_language = Arc::new(Language::new(
19742 LanguageConfig {
19743 name: "HTML".into(),
19744 matcher: LanguageMatcher {
19745 path_suffixes: vec!["html".to_string()],
19746 ..LanguageMatcher::default()
19747 },
19748 brackets: BracketPairConfig {
19749 pairs: vec![BracketPair {
19750 start: "<".into(),
19751 end: ">".into(),
19752 close: true,
19753 ..Default::default()
19754 }],
19755 ..Default::default()
19756 },
19757 ..Default::default()
19758 },
19759 Some(tree_sitter_html::LANGUAGE.into()),
19760 ));
19761 language_registry.add(html_language);
19762 let mut fake_servers = language_registry.register_fake_lsp(
19763 "HTML",
19764 FakeLspAdapter {
19765 capabilities: lsp::ServerCapabilities {
19766 completion_provider: Some(lsp::CompletionOptions {
19767 resolve_provider: Some(true),
19768 ..Default::default()
19769 }),
19770 ..Default::default()
19771 },
19772 ..Default::default()
19773 },
19774 );
19775
19776 let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
19777 let cx = &mut VisualTestContext::from_window(*workspace, cx);
19778
19779 let worktree_id = workspace
19780 .update(cx, |workspace, _window, cx| {
19781 workspace.project().update(cx, |project, cx| {
19782 project.worktrees(cx).next().unwrap().read(cx).id()
19783 })
19784 })
19785 .unwrap();
19786 project
19787 .update(cx, |project, cx| {
19788 project.open_local_buffer_with_lsp(path!("/file.html"), cx)
19789 })
19790 .await
19791 .unwrap();
19792 let editor = workspace
19793 .update(cx, |workspace, window, cx| {
19794 workspace.open_path((worktree_id, "file.html"), None, true, window, cx)
19795 })
19796 .unwrap()
19797 .await
19798 .unwrap()
19799 .downcast::<Editor>()
19800 .unwrap();
19801
19802 let fake_server = fake_servers.next().await.unwrap();
19803 editor.update_in(cx, |editor, window, cx| {
19804 editor.set_text("<ad></ad>", window, cx);
19805 editor.change_selections(None, window, cx, |selections| {
19806 selections.select_ranges([Point::new(0, 3)..Point::new(0, 3)]);
19807 });
19808 let Some((buffer, _)) = editor
19809 .buffer
19810 .read(cx)
19811 .text_anchor_for_position(editor.selections.newest_anchor().start, cx)
19812 else {
19813 panic!("Failed to get buffer for selection position");
19814 };
19815 let buffer = buffer.read(cx);
19816 let buffer_id = buffer.remote_id();
19817 let opening_range =
19818 buffer.anchor_before(Point::new(0, 1))..buffer.anchor_after(Point::new(0, 3));
19819 let closing_range =
19820 buffer.anchor_before(Point::new(0, 6))..buffer.anchor_after(Point::new(0, 8));
19821 let mut linked_ranges = HashMap::default();
19822 linked_ranges.insert(
19823 buffer_id,
19824 vec![(opening_range.clone(), vec![closing_range.clone()])],
19825 );
19826 editor.linked_edit_ranges = LinkedEditingRanges(linked_ranges);
19827 });
19828 let mut completion_handle =
19829 fake_server.set_request_handler::<lsp::request::Completion, _, _>(move |_, _| async move {
19830 Ok(Some(lsp::CompletionResponse::Array(vec![
19831 lsp::CompletionItem {
19832 label: "head".to_string(),
19833 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
19834 lsp::InsertReplaceEdit {
19835 new_text: "head".to_string(),
19836 insert: lsp::Range::new(
19837 lsp::Position::new(0, 1),
19838 lsp::Position::new(0, 3),
19839 ),
19840 replace: lsp::Range::new(
19841 lsp::Position::new(0, 1),
19842 lsp::Position::new(0, 3),
19843 ),
19844 },
19845 )),
19846 ..Default::default()
19847 },
19848 ])))
19849 });
19850 editor.update_in(cx, |editor, window, cx| {
19851 editor.show_completions(&ShowCompletions { trigger: None }, window, cx);
19852 });
19853 cx.run_until_parked();
19854 completion_handle.next().await.unwrap();
19855 editor.update(cx, |editor, _| {
19856 assert!(
19857 editor.context_menu_visible(),
19858 "Completion menu should be visible"
19859 );
19860 });
19861 editor.update_in(cx, |editor, window, cx| {
19862 editor.confirm_completion(&ConfirmCompletion::default(), window, cx)
19863 });
19864 cx.executor().run_until_parked();
19865 editor.update(cx, |editor, cx| {
19866 assert_eq!(editor.text(cx), "<head></head>");
19867 });
19868}
19869
19870#[gpui::test]
19871async fn test_invisible_worktree_servers(cx: &mut TestAppContext) {
19872 init_test(cx, |_| {});
19873
19874 let fs = FakeFs::new(cx.executor());
19875 fs.insert_tree(
19876 path!("/root"),
19877 json!({
19878 "a": {
19879 "main.rs": "fn main() {}",
19880 },
19881 "foo": {
19882 "bar": {
19883 "external_file.rs": "pub mod external {}",
19884 }
19885 }
19886 }),
19887 )
19888 .await;
19889
19890 let project = Project::test(fs, [path!("/root/a").as_ref()], cx).await;
19891 let language_registry = project.read_with(cx, |project, _| project.languages().clone());
19892 language_registry.add(rust_lang());
19893 let _fake_servers = language_registry.register_fake_lsp(
19894 "Rust",
19895 FakeLspAdapter {
19896 ..FakeLspAdapter::default()
19897 },
19898 );
19899 let (workspace, cx) =
19900 cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
19901 let worktree_id = workspace.update(cx, |workspace, cx| {
19902 workspace.project().update(cx, |project, cx| {
19903 project.worktrees(cx).next().unwrap().read(cx).id()
19904 })
19905 });
19906
19907 let assert_language_servers_count =
19908 |expected: usize, context: &str, cx: &mut VisualTestContext| {
19909 project.update(cx, |project, cx| {
19910 let current = project
19911 .lsp_store()
19912 .read(cx)
19913 .as_local()
19914 .unwrap()
19915 .language_servers
19916 .len();
19917 assert_eq!(expected, current, "{context}");
19918 });
19919 };
19920
19921 assert_language_servers_count(
19922 0,
19923 "No servers should be running before any file is open",
19924 cx,
19925 );
19926 let pane = workspace.update(cx, |workspace, _| workspace.active_pane().clone());
19927 let main_editor = workspace
19928 .update_in(cx, |workspace, window, cx| {
19929 workspace.open_path(
19930 (worktree_id, "main.rs"),
19931 Some(pane.downgrade()),
19932 true,
19933 window,
19934 cx,
19935 )
19936 })
19937 .unwrap()
19938 .await
19939 .downcast::<Editor>()
19940 .unwrap();
19941 pane.update(cx, |pane, cx| {
19942 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19943 open_editor.update(cx, |editor, cx| {
19944 assert_eq!(
19945 editor.display_text(cx),
19946 "fn main() {}",
19947 "Original main.rs text on initial open",
19948 );
19949 });
19950 assert_eq!(open_editor, main_editor);
19951 });
19952 assert_language_servers_count(1, "First *.rs file starts a language server", cx);
19953
19954 let external_editor = workspace
19955 .update_in(cx, |workspace, window, cx| {
19956 workspace.open_abs_path(
19957 PathBuf::from("/root/foo/bar/external_file.rs"),
19958 OpenOptions::default(),
19959 window,
19960 cx,
19961 )
19962 })
19963 .await
19964 .expect("opening external file")
19965 .downcast::<Editor>()
19966 .expect("downcasted external file's open element to editor");
19967 pane.update(cx, |pane, cx| {
19968 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19969 open_editor.update(cx, |editor, cx| {
19970 assert_eq!(
19971 editor.display_text(cx),
19972 "pub mod external {}",
19973 "External file is open now",
19974 );
19975 });
19976 assert_eq!(open_editor, external_editor);
19977 });
19978 assert_language_servers_count(
19979 1,
19980 "Second, external, *.rs file should join the existing server",
19981 cx,
19982 );
19983
19984 pane.update_in(cx, |pane, window, cx| {
19985 pane.close_active_item(&CloseActiveItem::default(), window, cx)
19986 })
19987 .unwrap()
19988 .await
19989 .unwrap();
19990 pane.update_in(cx, |pane, window, cx| {
19991 pane.navigate_backward(window, cx);
19992 });
19993 cx.run_until_parked();
19994 pane.update(cx, |pane, cx| {
19995 let open_editor = pane.active_item().unwrap().downcast::<Editor>().unwrap();
19996 open_editor.update(cx, |editor, cx| {
19997 assert_eq!(
19998 editor.display_text(cx),
19999 "pub mod external {}",
20000 "External file is open now",
20001 );
20002 });
20003 });
20004 assert_language_servers_count(
20005 1,
20006 "After closing and reopening (with navigate back) of an external file, no extra language servers should appear",
20007 cx,
20008 );
20009
20010 cx.update(|_, cx| {
20011 workspace::reload(&workspace::Reload::default(), cx);
20012 });
20013 assert_language_servers_count(
20014 1,
20015 "After reloading the worktree with local and external files opened, only one project should be started",
20016 cx,
20017 );
20018}
20019
20020fn empty_range(row: usize, column: usize) -> Range<DisplayPoint> {
20021 let point = DisplayPoint::new(DisplayRow(row as u32), column as u32);
20022 point..point
20023}
20024
20025fn assert_selection_ranges(marked_text: &str, editor: &mut Editor, cx: &mut Context<Editor>) {
20026 let (text, ranges) = marked_text_ranges(marked_text, true);
20027 assert_eq!(editor.text(cx), text);
20028 assert_eq!(
20029 editor.selections.ranges(cx),
20030 ranges,
20031 "Assert selections are {}",
20032 marked_text
20033 );
20034}
20035
20036pub fn handle_signature_help_request(
20037 cx: &mut EditorLspTestContext,
20038 mocked_response: lsp::SignatureHelp,
20039) -> impl Future<Output = ()> + use<> {
20040 let mut request =
20041 cx.set_request_handler::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
20042 let mocked_response = mocked_response.clone();
20043 async move { Ok(Some(mocked_response)) }
20044 });
20045
20046 async move {
20047 request.next().await;
20048 }
20049}
20050
20051/// Handle completion request passing a marked string specifying where the completion
20052/// should be triggered from using '|' character, what range should be replaced, and what completions
20053/// should be returned using '<' and '>' to delimit the range.
20054///
20055/// Also see `handle_completion_request_with_insert_and_replace`.
20056#[track_caller]
20057pub fn handle_completion_request(
20058 cx: &mut EditorLspTestContext,
20059 marked_string: &str,
20060 completions: Vec<&'static str>,
20061 counter: Arc<AtomicUsize>,
20062) -> impl Future<Output = ()> {
20063 let complete_from_marker: TextRangeMarker = '|'.into();
20064 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20065 let (_, mut marked_ranges) = marked_text_ranges_by(
20066 marked_string,
20067 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20068 );
20069
20070 let complete_from_position =
20071 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20072 let replace_range =
20073 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20074
20075 let mut request =
20076 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20077 let completions = completions.clone();
20078 counter.fetch_add(1, atomic::Ordering::Release);
20079 async move {
20080 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20081 assert_eq!(
20082 params.text_document_position.position,
20083 complete_from_position
20084 );
20085 Ok(Some(lsp::CompletionResponse::Array(
20086 completions
20087 .iter()
20088 .map(|completion_text| lsp::CompletionItem {
20089 label: completion_text.to_string(),
20090 text_edit: Some(lsp::CompletionTextEdit::Edit(lsp::TextEdit {
20091 range: replace_range,
20092 new_text: completion_text.to_string(),
20093 })),
20094 ..Default::default()
20095 })
20096 .collect(),
20097 )))
20098 }
20099 });
20100
20101 async move {
20102 request.next().await;
20103 }
20104}
20105
20106/// Similar to `handle_completion_request`, but a [`CompletionTextEdit::InsertAndReplace`] will be
20107/// given instead, which also contains an `insert` range.
20108///
20109/// This function uses the cursor position to mimic what Rust-Analyzer provides as the `insert` range,
20110/// that is, `replace_range.start..cursor_pos`.
20111pub fn handle_completion_request_with_insert_and_replace(
20112 cx: &mut EditorLspTestContext,
20113 marked_string: &str,
20114 completions: Vec<&'static str>,
20115 counter: Arc<AtomicUsize>,
20116) -> impl Future<Output = ()> {
20117 let complete_from_marker: TextRangeMarker = '|'.into();
20118 let replace_range_marker: TextRangeMarker = ('<', '>').into();
20119 let (_, mut marked_ranges) = marked_text_ranges_by(
20120 marked_string,
20121 vec![complete_from_marker.clone(), replace_range_marker.clone()],
20122 );
20123
20124 let complete_from_position =
20125 cx.to_lsp(marked_ranges.remove(&complete_from_marker).unwrap()[0].start);
20126 let replace_range =
20127 cx.to_lsp_range(marked_ranges.remove(&replace_range_marker).unwrap()[0].clone());
20128
20129 let mut request =
20130 cx.set_request_handler::<lsp::request::Completion, _, _>(move |url, params, _| {
20131 let completions = completions.clone();
20132 counter.fetch_add(1, atomic::Ordering::Release);
20133 async move {
20134 assert_eq!(params.text_document_position.text_document.uri, url.clone());
20135 assert_eq!(
20136 params.text_document_position.position, complete_from_position,
20137 "marker `|` position doesn't match",
20138 );
20139 Ok(Some(lsp::CompletionResponse::Array(
20140 completions
20141 .iter()
20142 .map(|completion_text| lsp::CompletionItem {
20143 label: completion_text.to_string(),
20144 text_edit: Some(lsp::CompletionTextEdit::InsertAndReplace(
20145 lsp::InsertReplaceEdit {
20146 insert: lsp::Range {
20147 start: replace_range.start,
20148 end: complete_from_position,
20149 },
20150 replace: replace_range,
20151 new_text: completion_text.to_string(),
20152 },
20153 )),
20154 ..Default::default()
20155 })
20156 .collect(),
20157 )))
20158 }
20159 });
20160
20161 async move {
20162 request.next().await;
20163 }
20164}
20165
20166fn handle_resolve_completion_request(
20167 cx: &mut EditorLspTestContext,
20168 edits: Option<Vec<(&'static str, &'static str)>>,
20169) -> impl Future<Output = ()> {
20170 let edits = edits.map(|edits| {
20171 edits
20172 .iter()
20173 .map(|(marked_string, new_text)| {
20174 let (_, marked_ranges) = marked_text_ranges(marked_string, false);
20175 let replace_range = cx.to_lsp_range(marked_ranges[0].clone());
20176 lsp::TextEdit::new(replace_range, new_text.to_string())
20177 })
20178 .collect::<Vec<_>>()
20179 });
20180
20181 let mut request =
20182 cx.set_request_handler::<lsp::request::ResolveCompletionItem, _, _>(move |_, _, _| {
20183 let edits = edits.clone();
20184 async move {
20185 Ok(lsp::CompletionItem {
20186 additional_text_edits: edits,
20187 ..Default::default()
20188 })
20189 }
20190 });
20191
20192 async move {
20193 request.next().await;
20194 }
20195}
20196
20197pub(crate) fn update_test_language_settings(
20198 cx: &mut TestAppContext,
20199 f: impl Fn(&mut AllLanguageSettingsContent),
20200) {
20201 cx.update(|cx| {
20202 SettingsStore::update_global(cx, |store, cx| {
20203 store.update_user_settings::<AllLanguageSettings>(cx, f);
20204 });
20205 });
20206}
20207
20208pub(crate) fn update_test_project_settings(
20209 cx: &mut TestAppContext,
20210 f: impl Fn(&mut ProjectSettings),
20211) {
20212 cx.update(|cx| {
20213 SettingsStore::update_global(cx, |store, cx| {
20214 store.update_user_settings::<ProjectSettings>(cx, f);
20215 });
20216 });
20217}
20218
20219pub(crate) fn init_test(cx: &mut TestAppContext, f: fn(&mut AllLanguageSettingsContent)) {
20220 cx.update(|cx| {
20221 assets::Assets.load_test_fonts(cx);
20222 let store = SettingsStore::test(cx);
20223 cx.set_global(store);
20224 theme::init(theme::LoadThemes::JustBase, cx);
20225 release_channel::init(SemanticVersion::default(), cx);
20226 client::init_settings(cx);
20227 language::init(cx);
20228 Project::init_settings(cx);
20229 workspace::init_settings(cx);
20230 crate::init(cx);
20231 });
20232
20233 update_test_language_settings(cx, f);
20234}
20235
20236#[track_caller]
20237fn assert_hunk_revert(
20238 not_reverted_text_with_selections: &str,
20239 expected_hunk_statuses_before: Vec<DiffHunkStatusKind>,
20240 expected_reverted_text_with_selections: &str,
20241 base_text: &str,
20242 cx: &mut EditorLspTestContext,
20243) {
20244 cx.set_state(not_reverted_text_with_selections);
20245 cx.set_head_text(base_text);
20246 cx.executor().run_until_parked();
20247
20248 let actual_hunk_statuses_before = cx.update_editor(|editor, window, cx| {
20249 let snapshot = editor.snapshot(window, cx);
20250 let reverted_hunk_statuses = snapshot
20251 .buffer_snapshot
20252 .diff_hunks_in_range(0..snapshot.buffer_snapshot.len())
20253 .map(|hunk| hunk.status().kind)
20254 .collect::<Vec<_>>();
20255
20256 editor.git_restore(&Default::default(), window, cx);
20257 reverted_hunk_statuses
20258 });
20259 cx.executor().run_until_parked();
20260 cx.assert_editor_state(expected_reverted_text_with_selections);
20261 assert_eq!(actual_hunk_statuses_before, expected_hunk_statuses_before);
20262}